* @copyright 2002-2025 Nicola Asuni - Tecnick.com LTD * @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT) * @link https://github.com/tecnickcom/tc-lib-pdf * * This file is part of tc-lib-pdf software library. */ namespace Com\Tecnick\Pdf; use Com\Tecnick\Pdf\Exception as PdfException; use Com\Tecnick\Pdf\Font\Output as OutFont; /** * Com\Tecnick\Pdf\Output * * Output PDF data * * @since 2002-08-03 * @category Library * @package Pdf * @author Nicola Asuni * @copyright 2002-2025 Nicola Asuni - Tecnick.com LTD * @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT) * @link https://github.com/tecnickcom/tc-lib-pdf * * @phpstan-type TFourFloat array{ * float, * float, * float, * float, * } * * @phpstan-type TAnnotQuadPoint array{ * float, * float, * float, * float, * float, * float, * float, * float, * } * * @phpstan-type TAnnotBorderStyle array{ * 'type': string, * 'w': int, * 's': string, * 'd': array, * } * * @phpstan-type TAnnotBorderEffect array{ * 's'?: string, * 'i'?: float, * } * * @phpstan-type TAnnotMeasure array{ * 'type'?: string, * 'subtype'?: string, * } * * @phpstan-type TAnnotMarkup array{ * 't'?: string, * 'popup'?: array, * 'ca'?: float, * 'rc'?: string, * 'creationdate'?: string, * 'irt'?: array, * 'subj'?: string, * 'rt'?: string, * 'it'?: string, * 'exdata'?: array{ * 'type'?: string, * 'subtype': string, * }, * } * * @phpstan-type TAnnotStates array{ * 'marked'?: string, * 'review'?: string, * } * * @phpstan-type TAnnotText array{ * 'subtype': string, * 'open'?: bool, * 'name'?: string, * 'state'?: string, * 'statemodel'?: string, * } * * @phpstan-type TUriAction array{ * 's': string, * 'uri': string, * 'ismap'?: bool, * } * * @phpstan-type TAnnotLink array{ * 'subtype': string, * 'a'?: TAnnotActionDict, * 'dest'?: string|array, * 'h'?: string, * 'pa'?: TUriAction, * 'quadpoints'?: array, * 'bs'?: TAnnotBorderStyle, * } * * @phpstan-type TAnnotFreeText array{ * 'subtype': string, * 'da': string, * 'q'?: int, * 'rc'?: string, * 'ds'?: string, * 'cl'?: array, * 'it'?: string, * 'be'?: TAnnotBorderEffect, * 'rd'?: TFourFloat, * 'bs'?: TAnnotBorderStyle, * 'le'?: string, * } * * @phpstan-type TAnnotLine array{ * 'subtype': string, * 'l': TFourFloat, * 'bs'?: TAnnotBorderStyle, * 'le'?: array{string, string}, * 'ic'?: TFourFloat, * 'll'?: float, * 'lle'?: float, * 'cap'?: bool, * 'it'?: string, * 'llo'?: float, * 'cp'?: string, * 'measure'?: TAnnotMeasure, * 'co'?: array{float, float}, * } * * @phpstan-type TAnnotSquare array{ * 'subtype': string, * 'bs'?: TAnnotBorderStyle, * 'ic'?: TFourFloat, * 'be'?: TAnnotBorderEffect, * 'rd'?: TFourFloat, * } * * @phpstan-type TAnnotCircle TAnnotSquare * * @phpstan-type TAnnotPolygon array{ * 'subtype': string, * 'vertices'?: array, * 'le'?: array{string, string}, * 'bs'?: TAnnotBorderStyle, * 'ic'?: TFourFloat, * 'be'?: TAnnotBorderEffect, * 'it'?: string, * 'measure'?: TAnnotMeasure, * } * * @phpstan-type TAnnotPolyline TAnnotPolygon * * @phpstan-type TAnnotTextMarkup array{ * 'subtype': string, * 'quadpoints': array, * } * * @phpstan-type TAnnotCaret array{ * 'subtype': string, * 'rd'?: TFourFloat, * 'sy'?: string, * } * * @phpstan-type TAnnotRubberStamp array{ * 'subtype': string, * 'name'?: string, * } * * @phpstan-type TAnnotInk array{ * 'subtype': string, * 'inklist'?: array>, * 'bs'?: TAnnotBorderStyle, * } * * @phpstan-type TAnnotPopup array{ * 'subtype': string, * 'parent'?: array, * 'open'?: bool, * } * * @phpstan-type TAnnotFileAttachment array{ * 'subtype': string, * 'fs'?: string, * 'name'?: string, * } * * @phpstan-type TAnnotSound array{ * 'subtype': string, * 'sound': string, * 'name'?: string, * } * * @phpstan-type TAnnotMovieDict array{ * 'f': string, * 'aspect'?: array{float, float}, * 'rotate'?: int, * 'poster'?: bool|string, * } * * @phpstan-type TAnnotMovieActDict array{ * 'start'?: int|string|array{int|string, int}, * 'duration'?: int|string|array{int|string, int}, * 'rate'?: float, * 'volume'?: float, * 'showcontrols'?: bool, * 'mode'?: string, * 'synchronous'?: bool, * 'fwscale'?: array{int, int}, * 'fwposition'?: array{float, float}, * } * * @phpstan-type TAnnotMovie array{ * 'subtype': string, * 't'?: string, * 'movie'?: TAnnotMovieDict, * 'a'?: bool|TAnnotMovieActDict, * } * * @phpstan-type TAnnotIconFitDict array{ * 'sw'?: string, * 's'?: string, * 'a'?: array{float, float}, * 'fb'?: bool, * } * * @phpstan-type TAnnotMKDict array{ * 'r'?: int, * 'bc'?: TFourFloat, * 'bg'?: array{float}, * 'ca'?: string, * 'rc'?: string, * 'ac'?: string, * 'i'?: string, * 'ri'?: string, * 'ix'?: string, * 'if'?: TAnnotIconFitDict, * 'tp'?: int, * } * * @phpstan-type TAnnotActionDict array{ * 'type'?: string, * 's'?: string, * 'next'?: array>, * } * * @phpstan-type TAnnotAdditionalActionDict array{ * 'e'?: TAnnotActionDict, * 'x'?: TAnnotActionDict, * 'd'?: TAnnotActionDict, * 'u'?: TAnnotActionDict, * 'fo'?: TAnnotActionDict, * 'bi'?: TAnnotActionDict, * 'po'?: TAnnotActionDict, * 'pc'?: TAnnotActionDict, * 'pv'?: TAnnotActionDict, * 'pi'?: TAnnotActionDict, * } * * @phpstan-type TAnnotScreen array{ * 'subtype': string, * 't'?: string, * 'mk'?: TAnnotMKDict, * 'a'?: TAnnotActionDict, * 'aa'?: TAnnotAdditionalActionDict, * } * * @phpstan-type TAnnotWidget array{ * 'subtype': string, * 'h'?: string, * 'mk'?: TAnnotMKDict, * 'a'?: TAnnotActionDict, * 'aa'?: TAnnotAdditionalActionDict, * 'bs'?: TAnnotBorderStyle, * 'parent'?: array, * } * * @phpstan-type TAnnotFixedPrintDict array{ * 'type': string, * 'matrix'?: array{float, float, float, float, float, float}, * 'h'?: float, * 'v'?: float, * } * * @phpstan-type TAnnotWatermark array{ * 'subtype': string, * 'fixedprint'?: TAnnotFixedPrintDict, * } * * @phpstan-type TAnnotRedact array{ * 'subtype': string, * 'quadpoints'?: array, * 'ic'?: TFourFloat, * 'ro'?: string, * 'overlaytext'?: string, * 'repeat'?: bool, * 'da'?: string, * 'q'?: int, * } * * @phpstan-type TAnnotOptsA TAnnotText|TAnnotLink|TAnnotFreeText * @phpstan-type TAnnotOptsB TAnnotLine|TAnnotSquare|TAnnotCircle|TAnnotPolygon|TAnnotPolyline * @phpstan-type TAnnotOptsC TAnnotTextMarkup|TAnnotCaret|TAnnotRubberStamp|TAnnotInk|TAnnotPopup * @phpstan-type TAnnotOptsD TAnnotFileAttachment|TAnnotSound|TAnnotMovie * @phpstan-type TAnnotOptsE TAnnotScreen|TAnnotWidget|TAnnotWatermark|TAnnotRedact * * @phpstan-type TAnnotOpts TAnnotOptsA|TAnnotOptsB|TAnnotOptsC|TAnnotOptsD|TAnnotOptsE * * @phpstan-type TAnnot array{ * 'n': int, * 'x': float, * 'y': float, * 'w': float, * 'h': float, * 'txt': string, * 'opt': TAnnotOpts, * } * * @phpstan-type TGTransparency array{ * 'CS': string, * 'I': bool, * 'K': bool, * } * * @phpstan-type TXOBject array{ * 'spot_colors': array, * 'extgstate': array, * 'gradient': array, * 'font': array, * 'image': array, * 'xobject': array, * 'annotations': array, * 'transparency'?: ?TGTransparency, * 'id': string, * 'outdata': string, * 'n': int, * 'x': float, * 'y': float, * 'w': float, * 'h': float, * } * * @phpstan-type TOutline array{ * 't': string, * 'u': string, * 'l': int, * 'p': int, * 'x': float, * 'y': float, * 's': string, * 'c': string, * 'parent': int, * 'last': int, * 'first': int, * 'prev': int, * 'next': int, * } * * @phpstan-type TSignature array{ * 'appearance': array{ * 'empty': array, * 'name': string, * 'page': int, * 'rect': string, * }, * 'approval': string, * 'cert_type': int, * 'extracerts': ?string, * 'info': array{ * 'ContactInfo': string, * 'Location': string, * 'Name': string, * 'Reason': string, * }, * 'password': string, * 'privkey': string, * 'signcert': string, * } * * @phpstan-type TSignTimeStamp array{ * 'enabled': bool, * 'host': string, * 'username': string, * 'password': string, * 'cert': string, * } * * @phpstan-type TUserRights array{ * 'annots': string, * 'document': string, * 'ef': string, * 'enabled': bool, * 'form': string, * 'formex': string, * 'signature': string, * } * * @phpstan-type TEmbeddedFile array{ * 'a': int, * 'f': int, * 'n': int, * 'file': string, * 'content': string, * } * * @phpstan-type TObjID array{ * 'catalog': int, * 'dests': int, * 'form': array, * 'info': int, * 'pages': int, * 'resdic': int, * 'signature': int, * 'srgbicc': int, * 'xmp': int, * } * * @SuppressWarnings("PHPMD") */ abstract class Output extends \Com\Tecnick\Pdf\MetaInfo { /** * Object to export fornt data. * * @var OutFont */ protected OutFont $outfont; /** * PDF layers. * * @var array */ protected array $pdflayer = []; /** * Language array. * * @var array */ protected array $lang = []; /** * Fonts used in annotations. * * @var array */ protected array $annotation_fonts = []; /** * Destinations. * * @var array */ protected array $dests = []; /** * Links. * * @var array */ protected array $links = []; /** * Radio Button Groups. * * @var array */ protected array $radiobuttonGroups = []; /** * Javascript block to add. */ protected string $javascript = ''; /** * Javascript objects. * * @var array */ protected array $jsobjects = []; /** * Returns the RAW PDF string. */ public function getOutPDFString(): string { $out = $this->getOutPDFHeader() . $this->getOutPDFBody(); $startxref = strlen($out); $offset = $this->getPDFObjectOffsets($out); $out .= $this->getOutPDFXref($offset) . $this->getOutPDFTrailer() . 'startxref' . "\n" . $startxref . "\n" . '%%EOF' . "\n"; return $this->signDocument($out); } /** * Returns the PDF document header. */ protected function getOutPDFHeader(): string { return '%PDF-' . $this->pdfver . "\n" . "%\xE2\xE3\xCF\xD3\n"; } /** * Returns the raw PDF Body section. */ protected function getOutPDFBody(): string { $out = $this->page->getPdfPages($this->pon); $this->objid['pages'] = $this->page->getRootObjID(); $out .= $this->graph->getOutExtGState($this->pon); $this->pon = $this->graph->getObjectNumber(); $out .= $this->getOutOCG(); $this->outfont = new OutFont( $this->font->getFonts(), $this->pon, $this->encrypt, ); $out .= $this->outfont->getFontsBlock(); $this->pon = $this->outfont->getObjectNumber(); $out .= $this->image->getOutImagesBlock($this->pon); $this->pon = $this->image->getObjectNumber(); $out .= $this->color->getPdfSpotObjects($this->pon); $out .= $this->graph->getOutGradientShaders($this->pon); $this->pon = $this->graph->getObjectNumber(); $out .= $this->getOutXObjects(); $out .= $this->getOutResourcesDict(); $out .= $this->getOutDestinations(); $out .= $this->getOutEmbeddedFiles(); $out .= $this->getOutAnnotations(); $out .= $this->getOutJavascript(); $out .= $this->getOutBookmarks(); $enc = $this->encrypt->getEncryptionData(); if ($enc['encrypted']) { $out .= $this->encrypt->getPdfEncryptionObj($this->pon); } $out .= $this->getOutSignatureFields(); $out .= $this->getOutSignature(); $out .= $this->getOutMetaInfo(); $out .= $this->getOutXMP(); $out .= $this->getOutICC(); return $out . $this->getOutCatalog(); } /** * Returns the ordered offset array for each object. * * @param string $data Raw PDF data * * @return array - Ordered offset array for each PDF object */ protected function getPDFObjectOffsets(string $data): array { preg_match_all('/(([0-9]+)[\s][0-9]+[\s]obj[\n])/i', $data, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); $offset = []; foreach ($matches as $match) { $offset[($match[2][0])] = $match[2][1]; } ksort($offset); return $offset; } /** * Returns the PDF XREF section. * * @param array $offset Ordered offset array for each PDF object */ protected function getOutPDFXref(array $offset): string { $out = 'xref' . "\n" . '0 ' . ($this->pon + 1) . "\n" . '0000000000 65535 f ' . "\n"; $freegen = ($this->pon + 2); $lastobj = array_key_last($offset); for ($idx = 1; $idx <= $lastobj; ++$idx) { if (isset($offset[$idx])) { $out .= sprintf('%010d 00000 n ' . "\n", $offset[$idx]); } else { $out .= sprintf('0000000000 %05d f ' . "\n", $freegen); ++$freegen; } } return $out; } /** * Returns the PDF Trailer section. */ protected function getOutPDFTrailer(): string { $out = 'trailer' . "\n" . '<<' . ' /Size ' . ($this->pon + 1) . ' /Root ' . $this->objid['catalog'] . ' 0 R' . ' /Info ' . $this->objid['info'] . ' 0 R'; $enc = $this->encrypt->getEncryptionData(); if (! empty($enc['objid'])) { $out .= ' /Encrypt ' . $enc['objid'] . ' 0 R'; } return $out . (' /ID [ <' . $this->fileid . '> <' . $this->fileid . '> ]' . ' >>' . "\n"); } /** * Returns the PDF object to include a standard sRGB_IEC61966-2.1 blackscaled ICC colour profile. */ protected function getOutICC(): string { if ($this->pdfa === 0 && ! $this->sRGB) { return ''; } $oid = ++$this->pon; $this->objid['srgbicc'] = $oid; $out = $oid . ' 0 obj' . "\n"; $icc = file_get_contents(__DIR__ . '/include/sRGB.icc.z'); if ($icc === false) { throw new PdfException('Unable to read sRGB.icc.z file'); } $icc = $this->encrypt->encryptString($icc, $oid); return $out . '<< /N 3 /Filter /FlateDecode /Length ' . strlen($icc) . ' >>' . ' stream' . "\n" . $icc . "\n" . 'endstream' . "\n" . 'endobj' . "\n"; } /** * Get OutputIntents for sRGB IEC61966-2.1 if required. */ protected function getOutputIntentsSrgb(): string { if (empty($this->objid['srgbicc'])) { return ''; } $oid = $this->objid['catalog']; return ' /OutputIntents [<< /Type /OutputIntent /S /GTS_PDFA1 /OutputCondition ' . $this->getOutTextString('sRGB IEC61966-2.1', $oid, true) . ' /OutputConditionIdentifier ' . $this->getOutTextString('sRGB IEC61966-2.1', $oid, true) . ' /RegistryName ' . $this->getOutTextString('http://www.color.org', $oid, true) . ' /Info ' . $this->getOutTextString('sRGB IEC61966-2.1', $oid, true) . ' /DestOutputProfile ' . $this->objid['srgbicc'] . ' 0 R' . ' >>]'; } /** * Get OutputIntents for PDF-X if required. */ protected function getOutputIntentsPdfX(): string { $oid = $this->objid['catalog']; return ' /OutputIntents [<< /Type /OutputIntent /S /GTS_PDFX /OutputConditionIdentifier ' . $this->getOutTextString('OFCOM_PO_P1_F60_95', $oid, true) . ' /RegistryName ' . $this->getOutTextString('http://www.color.org', $oid, true) . ' /Info ' . $this->getOutTextString('OFCOM_PO_P1_F60_95', $oid, true) . ' >>]'; } protected function getOutputIntents(): string { if (empty($this->objid['catalog'])) { return ''; } if ($this->pdfx) { $this->getOutputIntentsPdfX(); } return $this->getOutputIntentsSrgb(); } /** * Get the PDF layers. */ protected function getPDFLayers(): string { if (empty($this->pdflayer) || empty($this->objid['catalog'])) { return ''; } $oid = $this->objid['catalog']; $lyrobjs = ''; $lyrobjs_off = ''; $lyrobjs_lock = ''; foreach ($this->pdflayer as $layer) { $layer_obj_ref = ' ' . $layer['objid'] . ' 0 R'; $lyrobjs .= $layer_obj_ref; if ($layer['view'] === false) { $lyrobjs_off .= $layer_obj_ref; } if ($layer['lock']) { $lyrobjs_lock .= $layer_obj_ref; } } return ' /OCProperties << /OCGs [' . $lyrobjs . ' ]' . ' /D <<' . ' /Name ' . $this->getOutTextString('Layers', $oid, true) . ' /Creator ' . $this->getOutTextString($this->creator, $oid, true) . ' /BaseState /ON' . ' /OFF [' . $lyrobjs_off . ']' . ' /Locked [' . $lyrobjs_lock . ']' . ' /Intent /View' . ' /AS [' . ' << /Event /Print /OCGs [' . $lyrobjs . '] /Category [/Print] >>' . ' << /Event /View /OCGs [' . $lyrobjs . '] /Category [/View] >>' . ' ]' . ' /Order [' . $lyrobjs . ']' . ' /ListMode /AllPages' //.' /RBGroups ['..']' //.' /Locked ['..']' . ' >>' . ' >>'; } /** * Returns the PDF Catalog entry. */ protected function getOutCatalog(): string { $oid = ++$this->pon; $this->objid['catalog'] = $oid; $out = $oid . ' 0 obj' . "\n" . '<<' . ' /Type /Catalog' . ' /Version /' . $this->pdfver //.' /Extensions <<>>' . ' /Pages ' . $this->objid['pages'] . ' 0 R' //.' /PageLabels ' //... . ' /Names <<'; if ($this->pdfa === 0 && $this->jstree !== '') { $out .= ' /JavaScript ' . $this->jstree; } if ($this->embeddedfiles !== []) { $afnames = []; $afobjs = []; foreach ($this->embeddedfiles as $efname => $efdata) { $afnames[] = $this->getOutTextString($efname, $oid) . ' ' . $efdata['f'] . ' 0 R'; $afobjs[] = $efdata['f'] . ' 0 R'; } $out .= ' /AF [ ' . implode(' ', $afobjs) . ' ]'; $out .= ' /EmbeddedFiles << /Names [ ' . implode(' ', $afnames) . ' ] >>'; } $out .= ' >>'; if (! empty($this->objid['dests'])) { $out .= ' /Dests ' . ($this->objid['dests']) . ' 0 R'; } $out .= $this->getOutViewerPref(); if (! empty($this->display['layout'])) { $out .= ' /PageLayout /' . $this->display['layout']; } if ($this->outlines !== []) { $out .= ' /Outlines ' . $this->outlinerootoid . ' 0 R'; if (empty($this->display['mode'])) { $this->display['mode'] = 'UseOutlines'; } } if (! empty($this->display['mode'])) { $out .= ' /PageMode /' . $this->display['mode']; } //$out .= ' /Threads []'; $firstpage = $this->page->getPage(0); $fpo = $firstpage['n']; if ($this->display['zoom'] == 'fullpage') { $out .= ' /OpenAction [' . $fpo . ' 0 R /Fit]'; } elseif ($this->display['zoom'] == 'fullwidth') { $out .= ' /OpenAction [' . $fpo . ' 0 R /FitH null]'; } elseif ($this->display['zoom'] == 'real') { $out .= ' /OpenAction [' . $fpo . ' 0 R /XYZ null null 1]'; } elseif (! is_string($this->display['zoom'])) { $out .= sprintf(' /OpenAction [' . $fpo . ' 0 R /XYZ null null %F]', ($this->display['zoom'] / 100)); } //$out .= ' /AA <<>>'; //$out .= ' /URI <<>>'; $out .= ' /Metadata ' . $this->objid['xmp'] . ' 0 R'; //$out .= ' /StructTreeRoot <<>>'; //$out .= ' /MarkInfo <<>>'; if (! empty($this->lang['a_meta_language'])) { $out .= ' /Lang ' . $this->getOutTextString($this->lang['a_meta_language'], $oid, true); } //$out .= ' /SpiderInfo <<>>'; $out .= $this->getOutputIntents(); //$out .= ' /PieceInfo <<>>'; $out .= $this->getPDFLayers(); // AcroForm if ( ! empty($this->objid['form']) || ($this->sign && ($this->signature['cert_type'] >= 0)) || ! empty($this->signature['appearance']['empty']) ) { $out .= ' /AcroForm <<'; $objrefs = ''; if ($this->sign && ($this->signature['cert_type'] >= 0)) { // set reference for signature object $objrefs .= $this->objid['signature'] . ' 0 R'; } if (! empty($this->signature['appearance']['empty'])) { foreach ($this->signature['appearance']['empty'] as $esa) { // set reference for empty signature objects $objrefs .= ' ' . $esa['objid'] . ' 0 R'; } } if (! empty($this->objid['form'])) { foreach ($this->objid['form'] as $objid) { $objrefs .= ' ' . $objid . ' 0 R'; } } $out .= ' /Fields [' . $objrefs . ']'; // It's better to turn off this value and set the appearance stream for // each annotation (/AP) to avoid conflicts with signature fields. if (empty($this->signature['approval']) || ($this->signature['approval'] != 'A')) { $out .= ' /NeedAppearances false'; } if ($this->sign && ($this->signature['cert_type'] >= 0)) { if ($this->signature['cert_type'] > 0) { $out .= ' /SigFlags 3'; } else { $out .= ' /SigFlags 1'; } } //$out .= ' /CO '; if (! empty($this->annotation_fonts)) { $out .= ' /DR << /Font <<'; foreach ($this->annotation_fonts as $fontkey => $fontid) { $out .= ' /F' . $fontid . ' ' . $this->font->getFont($fontkey)['n'] . ' 0 R'; } $out .= ' >> >>'; } $font = $this->font->getFont('helvetica'); $out .= ' /DA ' . $this->encrypt->escapeDataString('/F' . $font['i'] . ' 0 Tf 0 g', $oid); $out .= ' /Q ' . (($this->rtl) ? '2' : '0'); //$out .= ' /XFA '; $out .= ' >>'; // signatures if ( $this->sign && ($this->signature['cert_type'] >= 0) && (empty($this->signature['approval']) || ($this->signature['approval'] != 'A')) ) { $out .= ' /Perms << '; if ($this->signature['cert_type'] > 0) { $out .= '/DocMDP '; } else { $out .= '/UR3 '; } $out .= ($this->objid['signature'] + 1) . ' 0 R >>'; } } //$out .= ' /Legal <<>>'; //$out .= ' /Requirements []'; //$out .= ' /Collection <<>>'; //$out .= ' /NeedsRendering true'; $out .= ' >>' . "\n" . 'endobj' . "\n"; return $out; } /** * Returns the PDF OCG entry. */ protected function getOutOCG(): string { if (empty($this->pdflayer)) { return ''; } $out = ''; foreach ($this->pdflayer as $key => $layer) { $oid = ++$this->pon; $this->pdflayer[$key]['objid'] = $oid; $out .= $oid . ' 0 obj' . "\n"; $out .= '<< /Type /OCG' . ' /Name ' . $this->getOutTextString($layer['name'], $oid, true); if (!empty($layer['intent'])) { $out .= ' /Intent [' . $layer['intent'] . ']'; } $out .= ' /Usage <<'; if (isset($layer['print'])) { $out .= ' /Print << /PrintState /' . $this->getOnOff($layer['print']) . ' >>'; } $out .= ' /View << /ViewState /' . $this->getOnOff($layer['view']) . ' >>'; // Other (not-implemented) possible /Usage entries: // CreatorInfo, Language, Export, Zoom, User, PageElement. $out .= ' >>'; // close /Usage $out .= ' >>' . "\n" . 'endobj' . "\n"; } return $out; } /** * Returns the PDF Annotation code for Apearance Stream XObjects entry. * * @param int $width annotation width * @param int $height annotation height * @param string $stream appearance stream */ protected function getOutAPXObjects( int $width = 0, int $height = 0, string $stream = '' ): string { $stream = trim($stream); $oid = ++$this->pon; $out = $oid . ' 0 obj' . "\n"; $tid = 'AX' . $oid; $this->xobjects[$tid] = [ 'spot_colors' => [], 'extgstate' => [], 'gradient' => [], 'font' => [], 'image' => [], 'xobject' => [], 'annotations' => [], 'id' => $tid, 'n' => $oid, 'x' => 0, 'w' => 0, 'y' => 0, 'h' => 0, 'outdata' => '', ]; $out .= '<< /Type /XObject /Subtype /Form /FormType 1'; if ($this->compress) { $stream = gzcompress($stream); if ($stream === false) { throw new PdfException('Unable to compress stream'); } $out .= ' /Filter /FlateDecode'; } $stream = $this->encrypt->encryptString($stream, $oid); $rect = sprintf('%F %F', $width, $height); return $out . ' /BBox [0 0 ' . $rect . ']' . ' /Matrix [1 0 0 1 0 0]' . ' /Resources 2 0 R' . ' /Length ' . strlen($stream) . ' >>' . ' stream' . "\n" . $stream . "\n" . 'endstream' . "\n" . 'endobj' . "\n"; } /** * Returns the PDF XObjects entry. */ protected function getOutXObjects(): string { $out = ''; foreach ($this->xobjects as $data) { if (empty($data['outdata'])) { continue; } $out .= $data['n'] . ' 0 obj' . "\n" . '<<' . ' /Type /XObject' . ' /Subtype /Form' . ' /FormType 1'; $stream = trim($data['outdata']); if ($this->compress) { $stream = gzcompress($stream); if ($stream === false) { throw new PdfException('Unable to compress stream'); } $out .= ' /Filter /FlateDecode'; } $out .= sprintf( ' /BBox [%F %F %F %F]', $this->toPoints($data['x']), $this->toPoints(-$data['y']), $this->toPoints(($data['w'] + $data['x'])), $this->toPoints(($data['h'] - $data['y'])) ); $out .= ' /Matrix [1 0 0 1 0 0]' . ' /Resources <<' . ' /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'; $out .= $this->graph->getOutExtGStateResourcesByKeys($data['extgstate']); $out .= $this->graph->getOutGradientResourcesByKeys($data['gradient']); $out .= $this->color->getPdfSpotResourcesByKeys($data['spot_colors']); $out .= $this->outfont->getOutFontDictByKeys($data['font']); if (! empty($data['image']) || ! empty($data['xobject'])) { $out .= ' /XObject <<'; $out .= $this->image->getXobjectDictByKeys($data['image']); if (! empty($data['xobject'])) { foreach ($data['xobject'] as $xid) { $out .= ' /' . $xid . ' ' . $this->xobjects[$xid]['n'] . ' 0 R'; } } $out .= ' >>'; } $out .= ' >>'; // end of /Resources. if (isset($data['transparency'])) { // set transparency group $out .= ' /Group << /Type /Group /S /Transparency'; if (!empty($data['transparency'])) { $out .= ' /CS /' . $data['transparency']['CS']; $out .= ' /I /' . (($data['transparency']['I'] === true) ? 'true' : 'false'); $out .= ' /K /' . (($data['transparency']['K'] === true) ? 'true' : 'false'); } $out .= ' >>'; } $stream = $this->encrypt->encryptString($stream, $data['n']); $out .= ' /Length ' . strlen($stream) . ' >>' . ' stream' . "\n" . $stream . "\n" . 'endstream' . "\n" . 'endobj' . "\n"; } return $out; } /** * Returns the PDF Resources Dictionary entry. */ protected function getOutResourcesDict(): string { $this->objid['resdic'] = $this->page->getResourceDictObjID(); return $this->objid['resdic'] . ' 0 obj' . "\n" . '<<' . ' /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]' . $this->outfont->getOutFontDict() . $this->getXObjectDict() . $this->getLayerDict() . $this->graph->getOutExtGStateResources() . $this->graph->getOutGradientResources() . $this->color->getPdfSpotResources() . ' >>' . "\n" . 'endobj' . "\n"; } /** * Returns the PDF Destinations entry. */ protected function getOutDestinations(): string { if (empty($this->dests)) { return ''; } $oid = ++$this->pon; $this->objid['dests'] = $oid; $out = $oid . ' 0 obj' . "\n" . '<< '; foreach ($this->dests as $name => $dst) { $page = $this->page->getPage($dst['p']); $poid = $page['n']; $pgx = $this->toPoints($dst['x']); $pgy = $this->toYPoints($dst['y'], $page['pheight']); $out .= ' /' . $name . ' ' . sprintf('[%u 0 R /XYZ %F %F null]', $poid, $pgx, $pgy); } return $out . ' >>' . "\n" . 'endobj' . "\n"; } /** * Returns the PDF Embedded Files entry. */ protected function getOutEmbeddedFiles(): string { if (($this->pdfa == 1) || ($this->pdfa == 2)) { // embedded files are not allowed in PDF/A mode version 1 and 2 return ''; } $out = ''; reset($this->embeddedfiles); foreach ($this->embeddedfiles as $name => $data) { if (!empty($data['content'])) { // if content is already set, use it $content = $data['content']; } else { try { $content = $this->file->fileGetContents($data['file']); } catch (Exception) { continue; // silently skip the file } } $rawsize = strlen($content); if ($rawsize <= 0) { continue; // silently skip the file } // update name tree $oid = $data['f']; // embedded file specification object $out .= $oid . ' 0 obj' . "\n" . '<<' . ' /Type /Filespec /F ' . $this->getOutTextString($name, $oid) . ' /UF ' . $this->getOutTextString($name, $oid) . ' /AFRelationship /Source' . ' /EF <>' . ' >>' . "\n" . 'endobj' . "\n"; // embedded file object $filter = ''; if ($this->pdfa == 3) { $filter = ' /Subtype /text#2Fxml'; } elseif ($this->compress) { $content = gzcompress($content); if ($content === false) { throw new PdfException('Unable to compress content'); } $filter = ' /Filter /FlateDecode'; } $stream = $this->encrypt->encryptString($content, $data['n']); $out .= "\n" . $data['n'] . ' 0 obj' . "\n" . '<<' . ' /Type /EmbeddedFile' . $filter . ' /Length ' . strlen($stream) . ' /Params <>' . ' >>' . ' stream' . "\n" . $stream . "\n" . 'endstream' . "\n" . 'endobj' . "\n"; } return $out; } /** * Convert a color array into a string representation for annotations. * The number of array elements determines the colour space in which the colour shall be defined: * 0 No colour; transparent * 1 DeviceGray * 3 DeviceRGB * 4 DeviceCMYK * * @param array $color Array of colors. */ protected static function getColorStringFromArray(array $color): string { $col = array_values($color); $out = '['; match (count($color)) { 4 => $out .= sprintf( '%F %F %F %F', (max(0, min(100, (float) $col[0])) / 100), (max(0, min(100, (float) $col[1])) / 100), (max(0, min(100, (float) $col[2])) / 100), (max(0, min(100, (float) $col[3])) / 100) ), 3 => $out .= sprintf( '%F %F %F', (max(0, min(255, (float) $col[0])) / 255), (max(0, min(255, (float) $col[1])) / 255), (max(0, min(255, (float) $col[2])) / 255) ), 1 => $out .= sprintf( '%F', (max(0, min(255, (float) $col[0])) / 255) ), default => $out . ']', }; return $out . ']'; } /** * Returns the PDF Annotations entry. */ protected function getOutAnnotations(): string { $out = ''; $pages = $this->page->getPages(); foreach ($pages as $num => $page) { foreach ($page['annotrefs'] as $key => $oid) { if (empty($this->annotation[$oid])) { continue; } $annot = $this->annotation[$oid]; $annot['opt'] = array_change_key_case($annot['opt'], CASE_LOWER); $out .= $this->getAnnotationRadiobuttonGroups($annot); // @phpstan-ignore-line $orx = $this->toPoints($annot['x']); $ory = $this->toYPoints(($annot['y'] + $annot['h']), $page['pheight']); $width = $this->toPoints($annot['w']); $height = $this->toPoints($annot['h']); $rect = sprintf('%F %F %F %F', $orx, $ory, $orx + $width, $ory + $height); $out .= ((int) $oid) . ' 0 obj' . "\n" // @phpstan-ignore-line . '<<' . ' /Type /Annot' . ' /Subtype /' . $annot['opt']['subtype'] . ' /Rect [' . $rect . ']'; $ft = ['Btn', 'Tx', 'Ch', 'Sig']; $formfield = (! empty($annot['opt']['ft']) && in_array($annot['opt']['ft'], $ft)); if ($formfield) { $out .= ' /FT /' . $annot['opt']['ft']; // @phpstan-ignore-line } if ($annot['opt']['subtype'] !== 'Link') { $out .= ' /Contents ' . $this->getOutTextString($annot['txt'], $oid, true); } $out .= ' /P ' . $page['n'] . ' 0 R' . ' /NM ' . $this->encrypt->escapeDataString(sprintf('%04u-%04u', $page['num'], $key), $oid) . ' /M ' . $this->getOutDateTimeString($this->docmodtime, $oid) . $this->getOutAnnotationFlags($annot) // @phpstan-ignore-line . $this->getAnnotationAppearanceStream($annot, (int) $width, (int) $height) // @phpstan-ignore-line . $this->getAnnotationBorder($annot); // @phpstan-ignore-line if (! empty($annot['opt']['c']) && is_string($annot['opt']['c'])) { $out .= ' /C [ ' . $this->color->getPdfRgbComponents($annot['opt']['c']) . ' ]'; } //$out .= ' /StructParent '; //$out .= ' /OC '; $out .= $this->getOutAnnotationMarkups($annot, $oid) // @phpstan-ignore-line . $this->getOutAnnotationOptSubtype($annot, $num, $oid, $key) // @phpstan-ignore-line . ' >>' . "\n" . 'endobj' . "\n"; if (! $formfield) { continue; } if (isset($this->radiobuttonGroups[$annot['txt']])) { continue; } $this->objid['form'][] = $oid; } } return $out; } /** * Returns the Annotation code for Radio buttons. * * @param TAnnot $annot Array containing page annotations. */ protected function getAnnotationRadiobuttonGroups(array $annot): string { $out = ''; if ( empty($this->radiobuttonGroups[$annot['txt']]) || ! is_array($this->radiobuttonGroups[$annot['txt']]) ) { return $out; } $oid = $this->radiobuttonGroups[$annot['txt']]['n']; $out = $oid . ' 0 obj' . "\n" . '<<' . ' /Type /Annot' . ' /Subtype /Widget' . ' /Rect [0 0 0 0]'; if ($this->radiobuttonGroups[$annot['txt']]['#readonly#']) { // read only $out .= ' /F 68 /Ff 49153'; } else { $out .= ' /F 4 /Ff 49152'; // default print for PDF/A } $out .= ' /T ' . $this->encrypt->escapeDataString($annot['txt'], $oid); if (! empty($annot['opt']['tu']) && is_string($annot['opt']['tu'])) { $out .= ' /TU ' . $this->encrypt->escapeDataString($annot['opt']['tu'], $oid); } $out .= ' /FT /Btn /Kids ['; $defval = ''; foreach ($this->radiobuttonGroups[$annot['txt']] as $data) { if (isset($data['kid']) && is_numeric($data['kid'])) { $out .= ' ' . $data['kid'] . ' 0 R'; if ($data['def'] !== 'Off') { $defval = $data['def']; } } } $out .= ' ]'; if (! empty($defval) && is_string($defval)) { $out .= ' /V /' . $defval; } $out .= ' >>' . "\n" . 'endobj' . "\n"; $this->objid['form'][] = $oid; // store object id to be used on Parent entry of Kids $this->radiobuttonGroups[$annot['txt']]['n'] = $oid; return $out; } /** * Returns the Annotation code for Appearance Stream. * * @param TAnnot $annot Array containing page annotations. * @param int $width Annotation width. * @param int $height Annotation height. */ protected function getAnnotationAppearanceStream( array $annot, int $width = 0, int $height = 0 ): string { $out = ''; if (! empty($annot['opt']['as']) && is_string($annot['opt']['as'])) { $out .= ' /AS /' . $annot['opt']['as']; } if (empty($annot['opt']['ap'])) { return $out; } $out .= ' /AP <<'; if (! is_array($annot['opt']['ap'])) { $out .= $annot['opt']['ap']; } else { foreach ($annot['opt']['ap'] as $mode => $def) { // $mode can be: n = normal; r = rollover; d = down; $out .= ' /' . strtoupper($mode); if (is_array($def)) { $out .= ' <<'; foreach ($def as $apstate => $stream) { // reference to XObject that define the appearance for this mode-state $apsobjid = $this->getOutAPXObjects($width, $height, $stream); $out .= ' /' . $apstate . ' ' . $apsobjid . ' 0 R'; } $out .= ' >>'; } else { // reference to XObject that define the appearance for this mode $apsobjid = $this->getOutAPXObjects($width, $height, $def); $out .= ' ' . $apsobjid . ' 0 R'; } } } return $out . ' >>'; } /** * Returns the Annotation code for Borders. * * @param TAnnot $annot Array containing page annotations. */ protected function getAnnotationBorder(array $annot): string { $out = ''; if ( ! empty($annot['opt']['bs']) && (is_array($annot['opt']['bs'])) ) { $out .= ' /BS << /Type /Border'; if (isset($annot['opt']['bs']['w']) && is_numeric($annot['opt']['bs']['w'])) { $out .= ' /W ' . (int) $annot['opt']['bs']['w']; } $bstyles = ['S', 'D', 'B', 'I', 'U']; if ( ! empty($annot['opt']['bs']['s']) && is_string($annot['opt']['bs']['s']) && in_array($annot['opt']['bs']['s'], $bstyles) ) { $out .= ' /S /' . $annot['opt']['bs']['s']; } if ( isset($annot['opt']['bs']['d']) && (is_array($annot['opt']['bs']['d'])) ) { $out .= ' /D ['; foreach ($annot['opt']['bs']['d'] as $cord) { if (is_numeric($cord)) { $out .= ' ' . (int) $cord; } } $out .= ']'; } $out .= ' >>'; } else { $out .= ' /Border ['; if ( isset($annot['opt']['border']) && (count($annot['opt']['border']) >= 3) && is_numeric($annot['opt']['border'][0]) && is_numeric($annot['opt']['border'][1]) && is_numeric($annot['opt']['border'][2]) ) { $out .= (int) $annot['opt']['border'][0] . ' ' . (int) $annot['opt']['border'][1] . ' ' . (int) $annot['opt']['border'][2]; if ( isset($annot['opt']['border'][3]) && is_array($annot['opt']['border'][3]) ) { $out .= ' ['; foreach ($annot['opt']['border'][3] as $dash) { if (is_numeric($dash)) { $out .= ' ' . (int) $dash; } } $out .= ' ]'; } } else { $out .= '0 0 0'; } $out .= ']'; } if (isset($annot['opt']['be']) && (is_array($annot['opt']['be']))) { $out .= ' /BE <<'; $bstyles = ['S', 'C']; if ( ! empty($annot['opt']['be']['s']) && is_string($annot['opt']['be']['s']) && in_array($annot['opt']['be']['s'], $bstyles) ) { $out .= ' /S /' . $annot['opt']['be']['s']; } else { $out .= ' /S /S'; } if ( isset($annot['opt']['be']['i']) && is_numeric($annot['opt']['be']['i']) && ($annot['opt']['be']['i'] >= 0) && ($annot['opt']['be']['i'] <= 2) ) { $out .= ' /I ' . sprintf(' %F', $annot['opt']['be']['i']); } $out .= '>>'; } return $out; } /** * Returns the Annotation code for Makups. * * @param TAnnot $annot Array containing page annotations. * @param int $oid Annotation Object ID. */ protected function getOutAnnotationMarkups( array $annot, int $oid ): string { $out = ''; $markups = [ 'text', 'freetext', 'line', 'square', 'circle', 'polygon', 'polyline', 'highlight', 'underline', 'squiggly', 'strikeout', 'stamp', 'caret', 'ink', 'fileattachment', 'sound', ]; if (empty($annot['opt']['subtype']) || ! in_array(strtolower($annot['opt']['subtype']), $markups)) { return $out; } if (! empty($annot['opt']['t']) && is_string($annot['opt']['t'])) { $out .= ' /T ' . $this->getOutTextString($annot['opt']['t'], $oid, true); } //$out .= ' /Popup '; if (isset($annot['opt']['ca'])) { $out .= ' /CA ' . sprintf('%F', (float) $annot['opt']['ca']); } if (isset($annot['opt']['rc'])) { $out .= ' /RC ' . $this->getOutTextString($annot['opt']['rc'], $oid, true); } $out .= ' /CreationDate ' . $this->getOutDateTimeString($this->doctime, $oid); //$out .= ' /IRT '; if (isset($annot['opt']['subj'])) { $out .= ' /Subj ' . $this->getOutTextString($annot['opt']['subj'], $oid, true); } //$out .= ' /RT '; //$out .= ' /IT '; //$out .= ' /ExData '; return $out; } /** * Returns the Annotation code for Flags. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationFlags(array $annot): string { $fval = 4; if (isset($annot['opt']['f'])) { $fval = $this->getAnnotationFlagsCode($annot['opt']['f']); } if ($this->pdfa > 0) { // force print flag for PDF/A mode $fval |= 4; } return ' /F ' . $fval; } /** * Returns the Annotation Flags code. * * @param int|array $flags Annotation flags. */ protected function getAnnotationFlagsCode(int|array $flags): int { if (! is_array($flags)) { return $flags; } $fval = 0; foreach ($flags as $flag) { $fval += match (strtolower($flag)) { 'invisible' => 1 << 0, 'hidden' => 1 << 1, 'print' => 1 << 2, 'nozoom' => 1 << 3, 'norotate' => 1 << 4, 'noview' => 1 << 5, 'readonly' => 1 << 6, 'locked' => 1 << 7, 'togglenoview' => 1 << 8, 'lockedcontents' => 1 << 9, default => 0, }; } return $fval; } /** * Returns the output code associated with the annotation opt.subtype. * * @param TAnnot $annot Array containing page annotations. * @param int $pagenum Page number. * @param int $oid Annotation Object ID. * @param int $key Annotation index in the current page. */ protected function getOutAnnotationOptSubtype(array $annot, int $pagenum, int $oid, int $key): string { return match (strtolower($annot['opt']['subtype'])) { '3d' => $this->getOutAnnotationOptSubtype3D($annot), 'caret' => $this->getOutAnnotationOptSubtypeCaret($annot), 'circle' => $this->getOutAnnotationOptSubtypeCircle($annot), 'fileattachment' => $this->getOutAnnotationOptSubtypeFileattachment($annot, $key), 'freetext' => $this->getOutAnnotationOptSubtypeFreetext($annot, $oid), 'highlight' => $this->getOutAnnotationOptSubtypeHighlight($annot), 'ink' => $this->getOutAnnotationOptSubtypeInk($annot), 'line' => $this->getOutAnnotationOptSubtypeLine($annot), 'link' => $this->getOutAnnotationOptSubtypeLink($annot, $pagenum, $oid), 'movie' => $this->getOutAnnotationOptSubtypeMovie($annot), 'polygon' => $this->getOutAnnotationOptSubtypePolygon($annot), 'polyline' => $this->getOutAnnotationOptSubtypePolyline($annot), 'popup' => $this->getOutAnnotationOptSubtypePopup($annot), 'printermark' => $this->getOutAnnotationOptSubtypePrintermark($annot), 'redact' => $this->getOutAnnotationOptSubtypeRedact($annot), 'screen' => $this->getOutAnnotationOptSubtypeScreen($annot), 'sound' => $this->getOutAnnotationOptSubtypeSound($annot), 'square' => $this->getOutAnnotationOptSubtypeSquare($annot), 'squiggly' => $this->getOutAnnotationOptSubtypeSquiggly($annot), 'stamp' => $this->getOutAnnotationOptSubtypeStamp($annot), 'strikeout' => $this->getOutAnnotationOptSubtypeStrikeout($annot), 'text' => $this->getOutAnnotationOptSubtypeText($annot), 'trapnet' => $this->getOutAnnotationOptSubtypeTrapnet($annot), 'underline' => $this->getOutAnnotationOptSubtypeUnderline($annot), 'watermark' => $this->getOutAnnotationOptSubtypeWatermark($annot), 'widget' => $this->getOutAnnotationOptSubtypeWidget($annot, $oid), default => '', }; } /** * Returns the output code associated with the annotation opt.subtype.text. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeText(array $annot): string { $out = ''; if (isset($annot['opt']['open'])) { $out .= ' /Open ' . ($annot['opt']['open'] === true ? 'true' : 'false'); } $iconsapp = ['Comment', 'Help', 'Insert', 'Key', 'NewParagraph', 'Note', 'Paragraph']; if ( isset($annot['opt']['name']) && in_array($annot['opt']['name'], $iconsapp) ) { $out .= ' /Name /' . $annot['opt']['name']; } else { $out .= ' /Name /Note'; } if (! isset($annot['opt']['state']) && ! isset($annot['opt']['statemodel'])) { return $out; } $statemodels = ['Marked', 'Review']; if ( isset($annot['opt']['statemodel']) && in_array($annot['opt']['statemodel'], $statemodels) ) { $out .= ' /StateModel /' . $annot['opt']['statemodel']; } else { $annot['opt']['statemodel'] = 'Marked'; $out .= ' /StateModel /' . $annot['opt']['statemodel']; } if ($annot['opt']['statemodel'] == 'Marked') { $states = ['Accepted', 'Unmarked']; } else { $states = ['Accepted', 'Rejected', 'Cancelled', 'Completed', 'None']; } if ( isset($annot['opt']['state']) && in_array($annot['opt']['state'], $states) ) { $out .= ' /State /' . $annot['opt']['state']; } elseif ($annot['opt']['statemodel'] == 'Marked') { $out .= ' /State /Unmarked'; } else { $out .= ' /State /None'; } return $out; } /** * Returns the output code associated with the annotation opt.subtype.link. * * @param TAnnot $annot Array containing page annotations. * @param int $pagenum Page number. * @param int $oid Annotation Object ID. */ protected function getOutAnnotationOptSubtypeLink( array $annot, int $pagenum, int $oid ): string { $out = ''; if (! empty($annot['txt'])) { switch ($annot['txt'][0]) { case '#': // internal destination $out .= ' /A << /S /GoTo /D /' . $this->encrypt->encodeNameObject(substr($annot['txt'], 1)) . '>>'; break; case '@': // internal link ID $l = $this->links[$annot['txt']]; $page = $this->page->getPage($l['p']); $y = $this->toYPoints($l['y'], $page['pheight']); $out .= sprintf(' /Dest [%u 0 R /XYZ 0 %F null]', $page['n'], $y); break; case '%': // embedded PDF file $filename = basename(substr($annot['txt'], 1)); $out .= ' /A << /S /GoToE /D [0 /Fit] /NewWindow true /T << /R /C /P ' . ($pagenum - 1) . ' /A ' . $this->embeddedfiles[$filename]['a'] . ' >>' . ' >>'; break; case '*': // embedded generic file $filename = basename(substr($annot['txt'], 1)); $jsa = 'var D=event.target.doc;var MyData=D.dataObjects;for (var i in MyData) if (MyData[i].path=="' . $filename . '")' . ' D.exportDataObject( { cName : MyData[i].name, nLaunch : 2});'; $out .= ' /A << /S /JavaScript /JS ' . $this->getOutTextString($jsa, $oid, true) . ' >>'; break; default: $parsedUrl = parse_url($annot['txt']); if ( empty($parsedUrl['scheme']) && (isset($parsedUrl['path']) && $parsedUrl['path'] !== '' && strtolower(substr($parsedUrl['path'], -4)) == '.pdf') ) { // relative link to a PDF file $dest = '[0 /Fit]'; // default page 0 if (! empty($parsedUrl['fragment'])) { // check for named destination $tmp = explode('=', $parsedUrl['fragment']); $dest = '(' . ((count($tmp) == 2) ? $tmp[1] : $tmp[0]) . ')'; } $out .= ' /A << /S /GoToR /D ' . $dest . ' /F ' . $this->encrypt->escapeDataString($this->unhtmlentities($parsedUrl['path']), $oid) . ' /NewWindow true' . ' >>'; } else { // external URI link $out .= ' /A << /S /URI /URI ' . $this->encrypt->escapeDataString($this->unhtmlentities($annot['txt']), $oid) . ' >>'; } break; } } $hmodes = ['N', 'I', 'O', 'P']; if (! empty($annot['opt']['h']) && in_array($annot['opt']['h'], $hmodes)) { $out .= ' /H /' . $annot['opt']['h']; } else { $out .= ' /H /I'; } //$out .= ' /PA '; //$out .= ' /Quadpoints '; return $out; } /** * Returns the output code associated with the annotation opt.subtype.freetext. * * @param TAnnot $annot Array containing page annotations. * @param int $oid Annotation Object ID. */ protected function getOutAnnotationOptSubtypeFreetext(array $annot, int $oid): string { $out = ''; if (! empty($annot['opt']['da'])) { $out .= ' /DA ' . $this->encrypt->escapeDataString($annot['opt']['da'], $oid); } if ( isset($annot['opt']['q']) && is_numeric($annot['opt']['q']) && ($annot['opt']['q'] >= 0) && ($annot['opt']['q'] <= 2) ) { $out .= ' /Q ' . (int) $annot['opt']['q']; } if (isset($annot['opt']['rc'])) { $out .= ' /RC ' . $this->getOutTextString($annot['opt']['rc'], $annot['n'], true); } if (isset($annot['opt']['ds'])) { $out .= ' /DS ' . $this->getOutTextString($annot['opt']['ds'], $annot['n'], true); } if (isset($annot['opt']['cl']) && is_array($annot['opt']['cl'])) { $out .= ' /CL ['; foreach ($annot['opt']['cl'] as $cl) { $out .= sprintf('%F ', $this->toPoints($cl)); } $out .= ']'; } $tfit = ['FreeText', 'FreeTextCallout', 'FreeTextTypeWriter']; if (isset($annot['opt']['it']) && in_array($annot['opt']['it'], $tfit)) { $out .= ' /IT /' . $annot['opt']['it']; } if ( isset($annot['opt']['rd']) && is_array($annot['opt']['rd']) && (count($annot['opt']['rd']) == 4) && is_numeric($annot['opt']['rd'][0]) && is_numeric($annot['opt']['rd'][1]) && is_numeric($annot['opt']['rd'][2]) && is_numeric($annot['opt']['rd'][3]) ) { $l = $this->toPoints((float) $annot['opt']['rd'][0]); $r = $this->toPoints((float) $annot['opt']['rd'][1]); $t = $this->toPoints((float) $annot['opt']['rd'][2]); $b = $this->toPoints((float) $annot['opt']['rd'][3]); $out .= ' /RD [' . sprintf('%F %F %F %F', $l, $r, $t, $b) . ']'; } $lineendings = [ 'Square', 'Circle', 'Diamond', 'OpenArrow', 'ClosedArrow', 'None', 'Butt', 'ROpenArrow', 'RClosedArrow', 'Slash', ]; if (isset($annot['opt']['le']) && in_array($annot['opt']['le'], $lineendings)) { $out .= ' /LE /' . $annot['opt']['le']; } return $out; } /** * Returns the output code associated with the annotation opt.subtype.line. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeLine(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.square. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeSquare(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.circle. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeCircle(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.polygon. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypePolygon(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.polyline. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypePolyline(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.highlight. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeHighlight(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeUnderline(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.squiggly. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeSquiggly(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.strikeout. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeStrikeout(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.stamp. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeStamp(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.caret. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeCaret(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.ink. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeInk(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.popup. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypePopup(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.fileattachment. * * @param TAnnot $annot Array containing page annotations. * @param int $key Annotation index in the current page. */ protected function getOutAnnotationOptSubtypeFileattachment( array $annot, int $key ): string { if (($this->pdfa == 1) || ($this->pdfa == 2) || ! isset($annot['opt']['fs'])) { // embedded files are not allowed in PDF/A mode version 1 and 2 return ''; } $filename = basename($annot['opt']['fs']); if (! isset($this->embeddedfiles[$filename]['f'])) { return ''; } $out = ' /FS ' . $this->embeddedfiles[$filename]['f'] . ' 0 R'; $iconsapp = ['Graph', 'Paperclip', 'PushPin', 'Tag']; if (isset($annot['opt']['name']) && in_array($annot['opt']['name'], $iconsapp)) { $out .= ' /Name /' . $annot['opt']['name']; } else { $out .= ' /Name /PushPin'; } // index (zero-based) of the annotation in the Annots array of this page $this->embeddedfiles[$filename]['a'] = $key; return $out; } /** * Returns the output code associated with the annotation opt.subtype.sound. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeSound(array $annot): string { $out = ''; if (empty($annot['opt']['fs'])) { return ''; } $filename = basename($annot['opt']['fs']); if (! isset($this->embeddedfiles[$filename]['f'])) { return ''; } // ... TO BE COMPLETED ... // /R /C /B /E /CO /CP $out = ' /Sound ' . $this->embeddedfiles[$filename]['f'] . ' 0 R'; $iconsapp = ['Speaker', 'Mic']; if (isset($annot['opt']['name']) && in_array($annot['opt']['name'], $iconsapp)) { $out .= ' /Name /' . $annot['opt']['name']; } else { $out .= ' /Name /Speaker'; } return $out; } /** * Returns the output code associated with the annotation opt.subtype.movie. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeMovie(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.widget. * * @param TAnnot $annot Array containing page annotations. * @param int $oid Annotation Object ID. */ protected function getOutAnnotationOptSubtypeWidget( array $annot, int $oid ): string { $out = ''; $hmode = ['N', 'I', 'O', 'P', 'T']; if (! empty($annot['opt']['h']) && in_array($annot['opt']['h'], $hmode)) { $out .= ' /H /' . $annot['opt']['h']; } if (! empty($annot['opt']['mk']) && is_array($annot['opt']['mk'])) { $out .= ' /MK <<'; if ( isset($annot['opt']['mk']['r']) && is_numeric($annot['opt']['mk']['r']) ) { $out .= ' /R ' . $annot['opt']['mk']['r']; } if (isset($annot['opt']['mk']['bc']) && (is_array($annot['opt']['mk']['bc']))) { $out .= ' /BC ' . static::getColorStringFromArray($annot['opt']['mk']['bc']); // @phpstan-ignore argument.type } if (isset($annot['opt']['mk']['bg']) && (is_array($annot['opt']['mk']['bg']))) { $out .= ' /BG ' . static::getColorStringFromArray($annot['opt']['mk']['bg']); // @phpstan-ignore argument.type } if ( isset($annot['opt']['mk']['ca']) && is_string($annot['opt']['mk']['ca']) ) { $out .= ' /CA ' . $annot['opt']['mk']['ca']; } if ( isset($annot['opt']['mk']['rc']) && is_string($annot['opt']['mk']['rc']) ) { $out .= ' /RC ' . $annot['opt']['mk']['rc']; } if ( isset($annot['opt']['mk']['ac']) && is_string($annot['opt']['mk']['ac']) ) { $out .= ' /AC ' . $annot['opt']['mk']['ac']; } if (isset($annot['opt']['mk']['i']) && is_string($annot['opt']['mk']['i'])) { $info = $this->image->getImageDataByKey($this->image->getKey($annot['opt']['mk']['i'])); if (! empty($info['obj'])) { $out .= ' /I ' . $info['obj'] . ' 0 R'; } } if (isset($annot['opt']['mk']['ri']) && is_string($annot['opt']['mk']['ri'])) { $info = $this->image->getImageDataByKey($this->image->getKey($annot['opt']['mk']['ri'])); if (! empty($info['obj'])) { $out .= ' /RI ' . $info['obj'] . ' 0 R'; } } if (isset($annot['opt']['mk']['ix']) && is_string($annot['opt']['mk']['ix'])) { $info = $this->image->getImageDataByKey($this->image->getKey($annot['opt']['mk']['ix'])); if (! empty($info['obj'])) { $out .= ' /IX ' . $info['obj'] . ' 0 R'; } } if (! empty($annot['opt']['mk']['if']) && is_array($annot['opt']['mk']['if'])) { $out .= ' /IF <<'; $if_sw = ['A', 'B', 'S', 'N']; if ( isset($annot['opt']['mk']['if']['sw']) && is_string($annot['opt']['mk']['if']['sw']) && in_array($annot['opt']['mk']['if']['sw'], $if_sw) ) { $out .= ' /SW /' . $annot['opt']['mk']['if']['sw']; } $if_s = ['A', 'P']; if ( isset($annot['opt']['mk']['if']['s']) && is_string($annot['opt']['mk']['if']['s']) && in_array($annot['opt']['mk']['if']['s'], $if_s) ) { $out .= ' /S /' . $annot['opt']['mk']['if']['s']; } if ( isset($annot['opt']['mk']['if']['a']) && (is_array($annot['opt']['mk']['if']['a'])) && (count($annot['opt']['mk']['if']['a']) == 2) && is_numeric($annot['opt']['mk']['if']['a'][0]) && is_numeric($annot['opt']['mk']['if']['a'][1]) ) { $out .= sprintf( ' /A [%F %F]', $annot['opt']['mk']['if']['a'][0], $annot['opt']['mk']['if']['a'][1] ); } if (isset($annot['opt']['mk']['if']['fb']) && ($annot['opt']['mk']['if']['fb'])) { $out .= ' /FB true'; } $out .= '>>'; } if ( isset($annot['opt']['mk']['tp']) && is_numeric($annot['opt']['mk']['tp']) && ($annot['opt']['mk']['tp'] >= 0) && ($annot['opt']['mk']['tp'] <= 6) ) { $out .= ' /TP ' . (int) $annot['opt']['mk']['tp']; } $out .= '>>'; } // --- Entries for field dictionaries --- if (isset($this->radiobuttonGroups[$annot['txt']]['n'])) { $out .= ' /Parent ' . $this->radiobuttonGroups[$annot['txt']]['n'] . ' 0 R'; } if (isset($annot['opt']['t']) && is_string($annot['opt']['t'])) { $out .= ' /T ' . $this->encrypt->escapeDataString($annot['opt']['t'], $oid); } if (isset($annot['opt']['tu']) && is_string($annot['opt']['tu'])) { $out .= ' /TU ' . $this->encrypt->escapeDataString($annot['opt']['tu'], $oid); } if (isset($annot['opt']['tm']) && is_string($annot['opt']['tm'])) { $out .= ' /TM ' . $this->encrypt->escapeDataString($annot['opt']['tm'], $oid); } if (isset($annot['opt']['ff'])) { if (is_array($annot['opt']['ff'])) { // array of bit settings $flag = 0; foreach ($annot['opt']['ff'] as $val) { $flag += 1 << ($val - 1); } } else { $flag = (int) $annot['opt']['ff']; } $out .= ' /Ff ' . $flag; } if (isset($annot['opt']['maxlen'])) { $out .= ' /MaxLen ' . (int) $annot['opt']['maxlen']; } if (isset($annot['opt']['v'])) { $out .= ' /V'; if (is_array($annot['opt']['v'])) { foreach ($annot['opt']['v'] as $optval) { if (is_float($optval)) { $optval = sprintf('%F', $optval); } $out .= ' ' . $optval; } } else { $out .= ' ' . $this->getOutTextString($annot['opt']['v'], $oid, true); } } if (isset($annot['opt']['dv'])) { $out .= ' /DV'; if (is_array($annot['opt']['dv'])) { foreach ($annot['opt']['dv'] as $optval) { if (is_float($optval)) { $optval = sprintf('%F', $optval); } $out .= ' ' . $optval; } } else { $out .= ' ' . $this->getOutTextString($annot['opt']['dv'], $oid, true); } } if (isset($annot['opt']['rv'])) { $out .= ' /RV'; if (is_array($annot['opt']['rv'])) { foreach ($annot['opt']['rv'] as $optval) { if (is_float($optval)) { $optval = sprintf('%F', $optval); } $out .= ' ' . $optval; } } else { $out .= ' ' . $this->getOutTextString($annot['opt']['rv'], $oid, true); } } if (! empty($annot['opt']['a'])) { $out .= ' /A << ' . $annot['opt']['a'] . ' >>'; } if (! empty($annot['opt']['aa'])) { $out .= ' /AA << ' . $annot['opt']['aa'] . ' >>'; } if (! empty($annot['opt']['da'])) { $out .= ' /DA ' . $this->encrypt->escapeDataString($annot['opt']['da'], $oid); } if ( isset($annot['opt']['q']) && is_numeric($annot['opt']['q']) && ($annot['opt']['q'] >= 0) && ($annot['opt']['q'] <= 2) ) { $out .= ' /Q ' . (int) $annot['opt']['q']; } if (! empty($annot['opt']['opt']) && is_array($annot['opt']['opt'])) { $out .= ' /Opt ['; foreach ($annot['opt']['opt'] as $copt) { if (is_array($copt)) { if ((count($copt) != 2) || ! is_string($copt[0]) || ! is_string($copt[1])) { continue; } $out .= ' [' . $this->getOutTextString($copt[0], $oid, true) . ' ' . $this->getOutTextString($copt[1], $oid, true) . ']'; } else { $out .= ' ' . $this->getOutTextString($copt, $oid, true); } } $out .= ']'; } if (isset($annot['opt']['ti'])) { $out .= ' /TI ' . (int) $annot['opt']['ti']; } if (! empty($annot['opt']['i']) && is_array($annot['opt']['i'])) { $out .= ' /I ['; foreach ($annot['opt']['i'] as $copt) { $out .= (int) $copt . ' '; } $out .= ']'; } return $out; } /** * Returns the output code associated with the annotation opt.subtype.screen. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeScreen(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.printermark. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypePrintermark(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.redact. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeRedact(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.trapnet. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeTrapnet(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.watermark. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtypeWatermark(array $annot): string { // @TODO return ''; } /** * Returns the output code associated with the annotation opt.subtype.3d. * * @param TAnnot $annot Array containing page annotations. */ protected function getOutAnnotationOptSubtype3D(array $annot): string { // @TODO return ''; } /** * Returns the PDF Javascript entry. */ protected function getOutJavascript(): string { if (($this->pdfa > 0) || (empty($this->javascript) && empty($this->jsobjects))) { return ''; } if (strpos($this->javascript, 'this.addField') > 0) { if (! $this->userrights['enabled']) { // $this->setUserRights(); } // The following two lines are used to avoid form fields duplication after saving. // The addField method only works when releasing user rights (UR3). $pattern = "ftcpdfdocsaved=this.addField('%s','%s',%d,[%F,%F,%F,%F]);"; $jsa = sprintf($pattern, 'tcpdfdocsaved', 'text', 0, 0, 1, 0, 1); $jsb = "getField('tcpdfdocsaved').value='saved';"; $this->javascript = $jsa . "\n" . $this->javascript . "\n" . $jsb; } $out = ''; // name tree for javascript $njs = '<< /Names ['; if (! empty($this->javascript)) { // default Javascript object $oid = ++$this->pon; $out .= $oid . ' 0 obj' . "\n" . '<<' . ' /S /JavaScript /JS ' . $this->getOutTextString($this->javascript, $oid, true) . ' >>' . "\n" . 'endobj' . "\n"; $njs .= ' (EmbeddedJS) ' . $oid . ' 0 R'; } foreach ($this->jsobjects as $key => $val) { if ($val['onload']) { // additional Javascript object $oid = ++$this->pon; $out .= $oid . ' 0 obj' . "\n" . '<< ' . '/S /JavaScript /JS ' . $this->getOutTextString($val['js'], $oid, true) . ' >>' . "\n" . 'endobj' . "\n"; $njs .= ' (JS' . $key . ') ' . $oid . ' 0 R'; } } $njs .= ' ] >>'; $this->jstree = $njs; return $out; } /** * Sort bookmarks by page and original position. */ protected function sortBookmarks(): void { $outline_p = []; $outline_k = []; foreach ($this->outlines as $key => $row) { $outline_p[$key] = $row['p']; $outline_k[$key] = $key; } // sort outlines by page and original position array_multisort($outline_p, SORT_NUMERIC, SORT_ASC, $outline_k, SORT_NUMERIC, SORT_ASC, $this->outlines); } /** * Process the bookmarks to get the previous and next one. * * @return int first bookmark object ID */ protected function processPrevNextBookmarks(): int { $numbookmarks = count($this->outlines); $this->sortBookmarks(); $lru = []; $level = 0; foreach ($this->outlines as $i => $o) { if ($o['l'] > 0) { $parent = $lru[($o['l'] - 1)]; // set parent and last pointers $this->outlines[$i]['parent'] = $parent; $this->outlines[$parent]['last'] = $i; // @phpstan-ignore assign.propertyType if ($o['l'] > $level) { // level increasing: set first pointer $this->outlines[$parent]['first'] = $i; // @phpstan-ignore assign.propertyType } } else { $this->outlines[$i]['parent'] = $numbookmarks; } if (($o['l'] <= $level) && ($i > 0)) { // set prev and next pointers $prev = $lru[$o['l']]; $this->outlines[$prev]['next'] = $i; // @phpstan-ignore assign.propertyType $this->outlines[$i]['prev'] = $prev; } $lru[$o['l']] = $i; $level = $o['l']; } return $lru[0]; } /** * Reverse function for htmlentities. * * @param string $text_to_convert Text to convert. * * @return string converted text string */ protected function unhtmlentities(string $text_to_convert): string { return html_entity_decode($text_to_convert, ENT_QUOTES, $this->encoding); } /** * Returns the PDF Bookmarks entry. */ protected function getOutBookmarks(): string { if ($this->outlines === []) { return ''; } $numbookmarks = is_countable($this->outlines) ? count($this->outlines) : 0; if ($numbookmarks <= 0) { return ''; } $root_oid = $this->processPrevNextBookmarks(); $first_oid = $this->pon + 1; $nltags = '/|<\/(blockquote|dd|dl|div|dt|h1|h2|h3|h4|h5|h6|hr|li|ol|p|pre|ul|tcpdf|table|tr|td)>/si'; $out = ''; foreach ($this->outlines as $outline) { // covert HTML title to string $search = [$nltags, "/[\r]+/si", "/[\n]+/si"]; $replace = ["\n", '', "\n"]; $title = preg_replace($search, $replace, $outline['t']); if ($title === null) { $title = ''; } $title = strip_tags($title); $title = preg_replace("/^\s+|\s+$/u", '', $title); if ($title === null) { $title = ''; } $oid = ++$this->pon; $out .= $oid . ' 0 obj' . "\n" . '<<' . ' /Title ' . $this->getOutTextString($title, $oid, true) . ' /Parent ' . ($first_oid + $outline['parent']) . ' 0 R'; if ($outline['prev'] >= 0) { $out .= ' /Prev ' . ($first_oid + $outline['prev']) . ' 0 R'; } if ($outline['next'] >= 0) { $out .= ' /Next ' . ($first_oid + $outline['next']) . ' 0 R'; } if ($outline['first'] >= 0) { $out .= ' /First ' . ($first_oid + $outline['first']) . ' 0 R'; } if ($outline['last'] >= 0) { $out .= ' /Last ' . ($first_oid + $outline['last']) . ' 0 R'; } if (! empty($outline['u'])) { // link switch ($outline['u'][0]) { case '#': // internal destination $out .= ' /Dest /' . $this->encrypt->encodeNameObject(substr($outline['u'], 1)); break; case '@': // internal link ID $l = $this->links[$outline['u']]; $page = $this->page->getPage($l['p']); $y = $this->toYPoints($l['y'], $page['pheight']); $out .= sprintf(' /Dest [%u 0 R /XYZ 0 %F null]', $page['n'], $y); break; case '%': // embedded PDF file $filename = basename(substr($outline['u'], 1)); $out .= ' /A << /S /GoToE /D [0 /Fit] /NewWindow true /T << /R /C /P ' . ($outline['p'] - 1) . ' /A ' . $this->embeddedfiles[$filename]['a'] . ' >>' . ' >>'; break; case '*': // embedded generic file $filename = basename(substr($outline['u'], 1)); $jsa = 'var D=event.target.doc;var MyData=D.dataObjects;' . 'for (var i in MyData) if (MyData[i].path=="' . $filename . '")' . ' D.exportDataObject( { cName : MyData[i].name, nLaunch : 2});'; $out .= ' /A <getOutTextString($jsa, $oid, true) . '>>'; break; default: // external URI link $out .= ' /A << /S /URI /URI ' . $this->encrypt->escapeDataString($this->unhtmlentities($outline['u']), $oid) . ' >>'; break; } } else { // link to a page $page = $this->page->getPage($outline['p']); $x = $this->toPoints($outline['x']); $y = $this->toYPoints($outline['y'], $page['pheight']); $out .= ' ' . sprintf('/Dest [%u 0 R /XYZ %F %F null]', $page['n'], $x, $y); } // set font style $style = 0; if (! empty($outline['s'])) { if (str_contains($outline['s'], 'B')) { $style |= 2; // bold } if (str_contains($outline['s'], 'I')) { $style |= 1; // oblique } } $out .= sprintf(' /F %d', $style); // set bookmark color if (empty($outline['c'])) { $out .= ' /C [0.0 0.0 0.0]'; // black } else { $out .= ' /C [ ' . $this->color->getPdfRgbComponents($outline['c']) . ' ]'; } $out .= ' /Count 0 >>' . "\n" . 'endobj' . "\n"; } //Outline root $this->outlinerootoid = ++$this->pon; return $out . $this->outlinerootoid . ' 0 obj' . "\n" . '<<' . ' /Type /Outlines' . ' /First ' . $first_oid . ' 0 R' . ' /Last ' . ($first_oid + $root_oid) . ' 0 R' . ' >>' . "\n" . 'endobj' . "\n"; } /** * Returns the PDF Signature Fields entry. */ protected function getOutSignatureFields(): string { if ($this->signature === []) { return ''; } $out = ''; foreach ($this->signature['appearance']['empty'] as $key => $esa) { $page = $this->page->getPage($esa['page']); $signame = $esa['name'] . sprintf(' [%03d]', ($key + 1)); $out .= $esa['objid'] . ' 0 obj' . "\n" . '<<' . ' /Type /Annot' . ' /Subtype /Widget' . ' /Rect [' . $esa['rect'] . ']' . ' /P ' . $page['n'] . ' 0 R' // link to signature appearance page . ' /F 4' . ' /FT /Sig' . ' /T ' . $this->getOutTextString($signame, $esa['objid'], true) . ' /Ff 0' . ' >>' . "\n" . 'endobj' . "\n"; } return $out; } /** * Sign the document. * * @param string $pdfdoc string containing the PDF document */ protected function signDocument(string $pdfdoc): string { if (! $this->sign) { return $pdfdoc; } // remove last newline $pdfdoc = substr($pdfdoc, 0, -1); // remove filler space $byterange_strlen = strlen($this::BYTERANGE); // define the ByteRange $byte_range = []; $byte_range[0] = 0; $byte_range[1] = strpos($pdfdoc, $this::BYTERANGE) + $byterange_strlen + 10; $byte_range[2] = $byte_range[1] + $this::SIGMAXLEN + 2; $byte_range[3] = strlen($pdfdoc) - $byte_range[2]; $pdfdoc = substr($pdfdoc, 0, $byte_range[1]) . substr($pdfdoc, $byte_range[2]); // replace the ByteRange $byterange = sprintf('/ByteRange[0 %u %u %u]', $byte_range[1], $byte_range[2], $byte_range[3]); $byterange .= str_repeat(' ', ($byterange_strlen - strlen($byterange))); $pdfdoc = str_replace($this::BYTERANGE, $byterange, $pdfdoc); // write the document to a temporary folder $tempdoc = $this->cache->getNewFileName('doc', $this->fileid); if ($tempdoc === false) { throw new PdfException('Unable to create temporary document file for signature'); } $f = $this->file->fopenLocal($tempdoc, 'wb'); $pdfdoc_length = strlen($pdfdoc); fwrite($f, $pdfdoc, $pdfdoc_length); fclose($f); // get digital signature via openssl library $tempsign = $this->cache->getNewFileName('sig', $this->fileid); if ($tempsign === false) { throw new PdfException('Unable to create temporary signature file'); } openssl_pkcs7_sign( $tempdoc, $tempsign, $this->signature['signcert'], [$this->signature['privkey'], $this->signature['password']], [], PKCS7_BINARY | PKCS7_DETACHED, $this->signature['extracerts'] ); // read signature $signature = $this->file->getFileData($tempsign); if ($signature === false) { throw new PdfException('Unable to read signature file'); } // extract signature $signature = substr($signature, $pdfdoc_length); $signature = substr($signature, (strpos($signature, "%%EOF\n\n------") + 13)); $tmparr = explode("\n\n", $signature); $signature = $tmparr[1]; // decode signature $signature = base64_decode(trim($signature)); if ($signature === false) { throw new PdfException('Unable to decode signature'); } // add TSA timestamp to signature $signature = $this->applySignatureTimestamp($signature); // convert signature to hex $signature = unpack('H*', $signature); if ($signature === false) { throw new PdfException('Unable to unpack signature'); } $signature = current($signature); if (! is_string($signature)) { throw new PdfException('Invalid signature'); } $signature = str_pad($signature, $this::SIGMAXLEN, '0'); // Add signature to the document return substr($pdfdoc, 0, $byte_range[1]) . '<' . $signature . '>' . substr($pdfdoc, $byte_range[1]); } /** * -- NOT YET IMPLEMENTED -- * Add TSA timestamp to the signature. * * @param string $signature Digital signature as binary string */ protected function applySignatureTimestamp(string $signature): string { if (!$this->sigtimestamp['enabled']) { return $signature; } // @TODO: Add TSA timestamp to the signature return $signature; } /** * Returns the PDF signarure entry. */ protected function getOutSignature(): string { if ((! $this->sign) || empty($this->signature['cert_type'])) { return ''; } // widget annotation for signature $soid = $this->objid['signature']; $oid = $soid + 1; $page = $this->page->getPage($this->signature['appearance']['page']); $out = $soid . ' 0 obj' . "\n" . '<<' . ' /Type /Annot' . ' /Subtype /Widget' . ' /Rect [' . $this->signature['appearance']['rect'] . ']' . ' /P ' . $page['n'] . ' 0 R' // link to signature appearance page . ' /F 4' . ' /FT /Sig' . ' /T ' . $this->getOutTextString($this->signature['appearance']['name'], $soid, true) . ' /Ff 0' . ' /V ' . $oid . ' 0 R' . ' >>' . "\n" . 'endobj' . "\n"; $out .= $oid . ' 0 obj' . "\n"; $out .= '<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached ' . $this::BYTERANGE . ' /Contents<' . str_repeat('0', $this::SIGMAXLEN) . '>'; if (empty($this->signature['approval']) || ($this->signature['approval'] != 'A')) { $out .= ' /Reference [ << /Type /SigRef'; if ($this->signature['cert_type'] > 0) { $out .= $this->getOutSignatureDocMDP(); } else { $out .= $this->getOutSignatureUserRights(); } // optional digest data (values must be calculated and replaced later) //$out .= ' /Data ********** 0 R' // .' /DigestMethod /MD5' // .' /DigestLocation[********** 34]' // .' /DigestValue<********************************>'; $out .= ' >> ]'; // end of reference } $out .= $this->getOutSignatureInfo($oid); return $out . ' /M ' . $this->getOutDateTimeString($this->docmodtime, $oid) . ' >>' . "\n" . 'endobj' . "\n"; } /** * Returns the PDF signarure entry. */ protected function getOutSignatureDocMDP(): string { if (empty($this->signature['cert_type'])) { return ''; } return ' /TransformMethod /DocMDP ' . '/TransformParams <<' . ' /Type /TransformParams' . ' /P ' . $this->signature['cert_type'] . ' /V /1.2' . ' >>'; } /** * Returns the PDF signarure entry. */ protected function getOutSignatureUserRights(): string { $out = ' /TransformMethod /UR3 /TransformParams << /Type /TransformParams /V /2.2'; if (! empty($this->userrights['document'])) { $out .= ' /Document[' . $this->userrights['document'] . ']'; } if (! empty($this->userrights['form'])) { $out .= ' /Form[' . $this->userrights['form'] . ']'; } if (! empty($this->userrights['signature'])) { $out .= ' /Signature[' . $this->userrights['signature'] . ']'; } if (! empty($this->userrights['annots'])) { $out .= ' /Annots[' . $this->userrights['annots'] . ']'; } if (! empty($this->userrights['ef'])) { $out .= ' /EF[' . $this->userrights['ef'] . ']'; } if (! empty($this->userrights['formex'])) { $out .= ' /FormEX[' . $this->userrights['formex'] . ']'; } return $out . ' >>'; } /** * Returns the PDF signarure info section. * * @param int $oid Object ID. */ protected function getOutSignatureInfo(int $oid): string { $out = ''; if (! empty($this->signature['info']['Name'])) { $out .= ' /Name ' . $this->getOutTextString($this->signature['info']['Name'], $oid, true); } if (! empty($this->signature['info']['Location'])) { $out .= ' /Location ' . $this->getOutTextString($this->signature['info']['Location'], $oid, true); } if (! empty($this->signature['info']['Reason'])) { $out .= ' /Reason ' . $this->getOutTextString($this->signature['info']['Reason'], $oid, true); } if (! empty($this->signature['info']['ContactInfo'])) { $out .= ' /ContactInfo ' . $this->getOutTextString($this->signature['info']['ContactInfo'], $oid, true); } return $out; } /** * Get the PDF output string for XObject resources dictionary. */ protected function getXObjectDict(): string { $out = ' /XObject <<'; foreach ($this->xobjects as $id => $oid) { $out .= ' /' . $id . ' ' . $oid['n'] . ' 0 R'; } $out .= $this->image->getXobjectDict(); return $out . ' >>'; } /** * Get the PDF output string for Layer resources dictionary. */ protected function getLayerDict(): string { if (empty($this->pdflayer)) { return ''; } $out = ' /Properties <<'; foreach ($this->pdflayer as $layer) { $out .= ' /' . $layer['layer'] . ' ' . $layer['objid'] . ' 0 R'; } return $out . ' >>'; } /** * Returns 'ON' if $val is true, 'OFF' otherwise. * * @param mixed $val Item to parse for boolean value. */ protected function getOnOff(mixed $val): string { if ((bool) $val) { return 'ON'; } return 'OFF'; } /** * Render the PDF in the browser or output the RAW data in the CLI. * * @param string $rawpdf Raw PDF data string from getOutPDFString(). * * @throw PdfException in case of error. */ public function renderPDF(string $rawpdf = ''): void { if (PHP_SAPI == 'cli') { echo $rawpdf; return; } if (headers_sent()) { throw new PdfException( 'The PDF file cannot be sent because some data has already been output to the browser.' ); } header('Content-Type: application/pdf'); header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0, max-age=1'); header('Pragma: public'); header('Expires: Sat, 01 Jan 2000 01:00:00 GMT'); // Date in the past header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); header( 'Content-Disposition: inline; filename="' . $this->encpdffilename . '"; filename*=UTF-8\'\'' . $this->encpdffilename ); if (empty($_SERVER['HTTP_ACCEPT_ENCODING'])) { // the content length may vary if the server is using compression header('Content-Length: ' . strlen($rawpdf)); } echo $rawpdf; } /** * Trigger the browser Download dialog to download the PDF document. * * @param string $rawpdf Raw PDF data string from getOutPDFString(). * * @throw PdfException in case of error. */ public function downloadPDF(string $rawpdf = ''): void { if (ob_get_contents()) { throw new PdfException( 'The PDF file cannot be sent, some data has already been output to the browser.' ); } if (headers_sent()) { throw new PdfException( 'The PDF file cannot be sent because some data has already been output to the browser.' ); } header('Content-Description: File Transfer'); header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0, max-age=1'); header('Pragma: public'); header('Expires: Sat, 01 Jan 2000 01:00:00 GMT'); // Date in the past header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // force download dialog header('Content-Type: application/pdf'); if (! str_contains(PHP_SAPI, 'cgi')) { header('Content-Type: application/force-download', false); header('Content-Type: application/octet-stream', false); header('Content-Type: application/download', false); } // use the Content-Disposition header to supply a recommended filename header( 'Content-Disposition: attachment; filename="' . $this->encpdffilename . '";' . " filename*=UTF-8''" . $this->encpdffilename ); header('Content-Transfer-Encoding: binary'); if (empty($_SERVER['HTTP_ACCEPT_ENCODING'])) { // the content length may vary if the server is using compression header('Content-Length: ' . strlen($rawpdf)); } echo $rawpdf; } /** * Save the PDF document to a local file. * * @param string $path Path to the output file. * @param string $rawpdf Raw PDF data string from getOutPDFString(). */ public function savePDF( string $path = '', string $rawpdf = '' ): void { $filepath = implode('/', [realpath($path), $this->pdffilename]); $fhd = $this->file->fopenLocal($filepath, 'wb'); if (! $fhd) { throw new PdfException('Unable to create output file: ' . $filepath); } fwrite($fhd, $rawpdf, strlen($rawpdf)); fclose($fhd); } /** * Returns the PDF as base64 mime multi-part email attachment (RFC 2045). * * @param string $rawpdf Raw PDF data string from getOutPDFString(). * * @return string Email attachment as raw string. */ public function getMIMEAttachmentPDF(string $rawpdf = ''): string { return 'Content-Type: application/pdf;' . "\r\n" . ' name="' . $this->encpdffilename . '"' . "\r\n" . 'Content-Transfer-Encoding: base64' . "\r\n" . 'Content-Disposition: attachment;' . "\r\n" . ' filename="' . $this->encpdffilename . '"' . "\r\n\r\n" . chunk_split(base64_encode($rawpdf), 76, "\r\n"); } }