From a612b13d3622ce0f31d678f42fd0a18068102f5c Mon Sep 17 00:00:00 2001 From: nicolaasuni Date: Thu, 22 Aug 2024 19:17:30 +0100 Subject: [PATCH] Add support for Layers, Links and Destinations --- VERSION | 2 +- examples/index.php | 70 +++++++++++++++++++--- src/Base.php | 2 +- src/Output.php | 142 +++++++++++++++++++++++---------------------- src/Tcpdf.php | 133 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 272 insertions(+), 77 deletions(-) diff --git a/VERSION b/VERSION index d4856d9..18f6eaf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.71 +8.0.72 diff --git a/examples/index.php b/examples/index.php index 305f5c4..fdcc505 100644 --- a/examples/index.php +++ b/examples/index.php @@ -947,6 +947,8 @@ $page09 = $pdf->page->add(); +$dest_barcode_page = $pdf->setNamedDestination('barcode'); + $pdf->graph->setPageWidth($page09['width']); $pdf->graph->setPageHeight($page09['height']); @@ -1011,6 +1013,9 @@ $page11 = $pdf->page->add(); +// Add an internal link to this page +$page11_link = $pdf->addInternalLink(); + $pdf->graph->setPageWidth($page11['width']); $pdf->graph->setPageHeight($page11['height']); @@ -1067,23 +1072,19 @@ 'color' => 'red', ], ); - $pdf->page->addContent($txt2); // get the coordinates of the box containing the last added text string. $bbox = $pdf->getLastBBox(); -$aoid = $pdf->setAnnotation( +$aoid1 = $pdf->setLink( $bbox['x'], $bbox['y'], $bbox['w'], $bbox['h'], 'https://tcpdf.org', - [ - 'subtype' => 'Link', - ] ); -$pdf->page->addAnnotRef($aoid); +$pdf->page->addAnnotRef($aoid1); // ----------------------------------------------- @@ -1446,7 +1447,7 @@ $timg = $pdf->image->add('../vendor/tecnickcom/tc-lib-pdf-image/test/images/200x100_RGB.png'); $pdf->addXObjectImageID($tid, $timg); -$xcnz .= $pdf->image->getSetImage($timg, 10, 10, 80, 80, 80); +$xcnz = $pdf->image->getSetImage($timg, 10, 10, 80, 80, 80); $pdf->addXObjectContent($tid, $xcnz); $pdf->exitXObjectTemplate(); @@ -1465,6 +1466,61 @@ // ---------- +// Layers + +$pageV01 = $pdf->page->add(); + +$pdf->page->addContent($bfont4['out']); + +$txtV1 = 'LAYERS: You can limit the visibility of PDF objects to screen or printer by using the newLayer() method. +Check the print preview of this document to display the alternative text.'; +$txtboxV1 = $pdf->getTextCell($txtV1, 15, 15, 150, valign: 'T', halign: 'L'); +$pdf->page->addContent($txtboxV1); + +$lyr01 = $pdf->newLayer('screen', [], false, true, false); +$pdf->page->addContent($lyr01); +$txtV2 = 'This line is for display on screen only.'; +$txtboxV2 = $pdf->getTextCell($txtV2, 15, 45, 150, valign: 'T', halign: 'L'); +$pdf->page->addContent($txtboxV2); +$pdf->page->addContent($pdf->closeLayer()); + + +$lyr02 = $pdf->newLayer('print', [], true, false, false); +$pdf->page->addContent($lyr02); +$txtV3 = 'This line is for print only.'; +$txtboxV3 = $pdf->getTextCell($txtV3, 15, 55, 150, valign: 'T', halign: 'L'); +$pdf->page->addContent($txtboxV3); +$pdf->page->addContent($pdf->closeLayer()); + +// Links + +$txtlnk1 = $pdf->getTextCell("Link to page 11", 15, 70, 150, valign: 'T', halign: 'L'); +$pdf->page->addContent($txtlnk1); +$bbox = $pdf->getLastBBox(); +$lnk1 = $pdf->setLink( + $bbox['x'], + $bbox['y'], + $bbox['w'], + $bbox['h'], + $page11_link, +); +$pdf->page->addAnnotRef($lnk1); + +$txtlnk2 = $pdf->getTextCell("Link dest to barcode page", 15, 80, 150, valign: 'T', halign: 'L'); +$pdf->page->addContent($txtlnk2); +$bbox = $pdf->getLastBBox(); +$lnk2 = $pdf->setLink( + $bbox['x'], + $bbox['y'], + $bbox['w'], + $bbox['h'], + $dest_barcode_page, +); +$pdf->page->addAnnotRef($lnk2); + +// ---------- + + // ============================================================= // ---------- diff --git a/src/Base.php b/src/Base.php index d11acc2..e447adb 100644 --- a/src/Base.php +++ b/src/Base.php @@ -136,7 +136,7 @@ abstract class Base /** * TCPDF version. */ - protected string $version = '8.0.71'; + protected string $version = '8.0.72'; /** * Time is seconds since EPOCH when the document was created. diff --git a/src/Output.php b/src/Output.php index 63ce630..07546b6 100644 --- a/src/Output.php +++ b/src/Output.php @@ -365,7 +365,7 @@ * 'prev': int, * 's': string, * 't': string, - * 'u': int, + * 'u': string, * 'x': float, * 'y': float, * } @@ -449,11 +449,12 @@ abstract class Output extends \Com\Tecnick\Pdf\MetaInfo * * @var array */ protected array $pdflayer = []; @@ -476,7 +477,6 @@ abstract class Output extends \Com\Tecnick\Pdf\MetaInfo * Destinations. * * @var array + */ + protected array $links = []; + /** * Radio Button Groups. * @@ -496,17 +506,6 @@ abstract class Output extends \Com\Tecnick\Pdf\MetaInfo */ protected array $radiobuttonGroups = []; - /** - * Links. - * - * @var array - */ - protected array $links = []; - /** * Javascript block to add. */ @@ -945,15 +944,24 @@ protected function getOutOCG(): string $oid = ++$this->pon; $this->pdflayer[$key]['objid'] = $oid; $out .= $oid . ' 0 obj' . "\n"; - $out .= '<< /Type /OCG /Name ' . $this->getOutTextString($layer['name'], $oid, true) - . ' /Usage <<'; + + $out .= '<< /Type /OCG' + . ' /Name ' . $this->getOutTextString($layer['name'], $oid, true); + + if (!empty($layer['intent'])) { + $out .= ' /Intent [' . $layer['intent'] . ']'; + } + + $out .= ' /Usage <<'; if (isset($layer['print']) && ($layer['print'] !== null)) { - $out .= ' /Print <getOnOff($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 .= ' /View <getOnOff($layer['view']) . '>>' - . ' >>' - . ' >>' . "\n" + $out .= ' >>' . "\n" . 'endobj' . "\n"; } @@ -1743,12 +1751,18 @@ protected function getOutAnnotationOptSubtypeLink( int $oid ): string { $out = ''; - if (! empty($annot['txt']) && is_string($annot['txt'])) { + 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) @@ -1790,15 +1804,8 @@ protected function getOutAnnotationOptSubtypeLink( . $this->encrypt->escapeDataString($this->unhtmlentities($annot['txt']), $oid) . ' >>'; } - break; } - } elseif (! empty($this->links[$annot['txt']])) { - // internal link ID - $l = $this->links[$annot['txt']]; - $page = $this->page->getPage($l['p']); - $y = $this->toYPoints($l['y'], $page['height']); - $out .= sprintf(' /Dest [%u 0 R /XYZ 0 %F null]', $page['n'], $y); } $hmodes = ['N', 'I', 'O', 'P']; @@ -2581,49 +2588,48 @@ protected function getOutBookmarks(): string if (! empty($outline['u'])) { // link - if (is_string($outline['u'])) { - switch ($outline['u'][0]) { - case '#': - // internal destination - $out .= ' /Dest /' . $this->encrypt->encodeNameObject(substr($outline['u'], 1)); - 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; - } - } elseif (isset($this->links[$outline['u']])) { - // internal link ID - $l = $this->links[$outline['u']]; - $page = $this->page->getPage($l['p']); - $y = ($page['height'] - ($l['y'] * $this->kunit)); - $out .= sprintf(' /Dest [%u 0 R /XYZ 0 %F null]', $page['n'], $y); + 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 = ($outline['x'] * $this->kunit); - $y = ($page['height'] - ($outline['y'] * $this->kunit)); + $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); } diff --git a/src/Tcpdf.php b/src/Tcpdf.php index aed58ea..8b0e088 100644 --- a/src/Tcpdf.php +++ b/src/Tcpdf.php @@ -356,6 +356,85 @@ public function setAnnotation( return $oid; } + /** + * Creates a link in the specified area. + * A link annotation represents either a hypertext link to a destination elsewhere in the document. + * + * @param float $posx Abscissa of upper-left corner. + * @param float $posy Ordinate of upper-left corner. + * @param float $width Width. + * @param float $height Height. + * @param string $link URL to open when the link is clicked or an identifier returned by setInternalLink(). + * A single character prefix may be used to specify the link action: + * - '#' = internal destination + * - '%' = embedded PDF file + * - '*' = embedded generic file + * + * @return int Object ID (Add to a page via: $pdf->page->addAnnotRef($aoid);). + */ + public function setLink( + float $posx, + float $posy, + float $width, + float $height, + string $link, + ): int { + return $this->setAnnotation( + $posx, + $posy, + $width, + $height, + $link, + ['subtype' => 'Link'] + ); + } + + /** + * Defines the page and vertical position an internal link points to. + * + * @param int $page Page number. + * @param float $posy Vertical position. + * + * @return string Internal link identifier to be used with setLink(). + * + */ + public function addInternalLink(int $page = 0, float $posy = 0): string + { + $lnkid = '@' . (count($this->links) + 1); + $this->links[$lnkid] = [ + 'p' => ($page < 1) ? $this->page->getPage()['pid'] : $page, + 'y' => $posy, + ]; + return $lnkid; + } + + /** + * Add a named destination. + * + * @param string $name Named destination (must be unique). + * @param int $page Page number. + * @param float $posx Abscissa of upper-left corner. + * @param float $posy Ordinate of upper-left corner. + * + * @return string Destination name. + */ + public function setNamedDestination( + string $name, + int $page = 0, + float $posx = 0, + float $posy = 0, + ): string { + $ename = $this->encrypt->encodeNameObject($name); + $this->dests[$ename] = [ + 'p' => ($page < 1) ? $this->page->getPage()['pid'] : $page, + 'x' => $posx, + 'y' => $posy, + ]; + return '#' . $ename; + } + + // ===| SIGNATURE |===================================================== + /** * Set User's Rights for the PDF Reader. * WARNING: This is experimental and currently doesn't work because requires a private key. @@ -599,6 +678,8 @@ protected function setSignAnnotRefs(): void } } + // ===| XOBJECT |======================================================= + /** * Create a new XObject template and return the object id. * @@ -851,4 +932,56 @@ public function addXObjectSpotColorID(string $tid, string $key): void { $this->xobjects[$tid]['spot_colors'][] = $key; } + + // ===| LAYERS |======================================================== + + /** + * Creates and return a new PDF Layer. + * + * @param string $name Layer name (only a-z letters and numbers). Leave empty for automatic name. + * @param array{'view'?: bool, 'design'?: bool} $intent intended use of the graphics in the layer. + * @param bool $print Set the printability of the layer. + * @param bool $view Set the visibility of the layer. + * @param bool $lock Set the lock state of the layer. + * + * @return string + */ + public function newLayer( + string $name = '', + array $intent = [], + bool $print = true, + bool $view = true, + bool $lock = true, + ): string { + $layer = sprintf('LYR%03d', (count($this->pdflayer) + 1)); + $name = preg_replace('/[^a-zA-Z0-9_\-]/', '', $name); + if (empty($name)) { + $name = $layer; + } + + $intarr = []; + if (!empty($intent['view'])) { + $intarr[] = '/View'; + } + if (!empty($intent['design'])) { + $intarr[] = '/Design'; + } + + $this->pdflayer[] = array( + 'layer' => $layer, + 'name' => $name, + 'intent' => implode(' ', $intarr), + 'print' => $print, + 'view' => $view, + 'lock' => $lock, + 'objid' => 0, + ); + + return ' /OC /' . $layer . ' BDC' . "\n"; + } + + public function closeLayer(): string + { + return 'EMC' . "\n"; + } }