I'm trying to dynamically add some text to an existing pdf file.
I've tried both FPDF and TCPDF combined with FPDI to import the existing pdf. That's ok. But, as expected, all existing links from the original pdf are gone.
Then, I tried to preserve the links using this FPDI extension:
fpdi_with_annnots https://gist.github.com/andreyvit/2020422
At first, it was made to preserve only external links, but then, the creator modified to include also internal links. But this extension is old, no longer maintained and no longer works for ** INTERNAL links** (external links are preserved, that's ok!) with FPDI and TCPDF.
Someone tried (see Github link above) to make it work with TCPDF and changed this piece of code:
$this->PageLinks[$this->page][] = $link;
to this:
$this->Link(
$link[0]/$this->k,
($this->fhPt-$link[1]+$link[3])/$this->k,
$link[2]/$this->k,
-$link[3]/$this->k,
$link[4]
);
Then, after some time, someone said it needed to be changed to this:
$this->Link(
$link[0]/$this->k,
($this->hPt - $link[1])/$this->k,
$link[2]/$this->k,
$link[3]/$this->k,
$link[4]
);
But it also no longer works.
The question:
1) Does anyone know how to change this code to preserve internal links?
or:
2) Does anyone know an alternative to fpdi_with_annots that import, generates and preserves hyperlinks?
Tip: Maybe using "Bookmarks" extension for FPDF would help, instead of Addlink() and Setlink(): http://fpdf.de/downloads/addons/1/
This will preserve your internal and external links while processing your PDF. It's not fully tested yet but should work fine.
<?php
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\Type\PdfArray;
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
use setasign\Fpdi\PdfParser\Type\PdfType;
use setasign\Fpdi\PdfReader\PageBoundaries;
use setasign\Fpdi\Tcpdf\Fpdi;
class TcpdfFpdiCustom extends Fpdi
{
public $pagesList;
public function importPage($pageNumber, $box = PageBoundaries::CROP_BOX, $groupXObject = true)
{
$pageId = parent::importPage($pageNumber, $box, $groupXObject);
$links = [];
$reader = $this->getPdfReader($this->currentReaderId);
$parser = $reader->getParser();
if (empty($this->pagesList)) {
$this->readAllPages($parser);
}
$pageObj = $reader->getPage($pageNumber)->getPageObject();
$annotationsObject = PdfDictionary::get(PdfType::resolve($pageObj, $parser), 'Annots');
$annotations = PdfType::resolve($annotationsObject, $parser);
if ($annotations->value) {
foreach ($annotations->value as $annotationRef) {
$annotation = PdfType::resolve($annotationRef, $parser);
if ( PdfDictionary::get($annotation, 'Subtype')->value !== 'Link' )
continue;
$a = PdfDictionary::get($annotation, 'A');
if ( !$a || $a instanceof PdfNull )
continue;
$link = PdfType::resolve($a, $parser);
$linkType = PdfDictionary::get($link, 'S')->value;
if (in_array($linkType, ['URI', 'GoTo']) &&
($rect = PdfDictionary::get($annotation, 'Rect')) &&
$rect instanceof PdfArray
) {
$rect = $rect->value;
$links[] = [
$rect[0]->value,
$rect[1]->value,
$rect[2]->value - $rect[0]->value,
$rect[1]->value - $rect[3]->value,
$this->getAnnotationLink($link, $linkType)
];
}
}
}
$this->importedPages[$pageId]['links'] = $links;
return $pageId;
}
public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
{
$size = parent::useTemplate($tpl, $x, $y, $width, $height, $adjustPageSize);
$links = $this->importedPages[$tpl]['links'];
$pxToU = $this->pixelsToUnits(1);
foreach ($links as $link) {
// When is integer, it means that is an internal link
if (is_int($link[4])) {
$l = $this->AddLink();
$this->SetLink($l, 0, $link[4]);
$link[4] = $l;
}
$this->Link(
$link[0] * $pxToU,
$this->getPageHeight() - $link[1] * $pxToU,
$link[2] * $pxToU,
$link[3] * $pxToU,
$link[4]
);
}
return $size;
}
public function readAllPages(PdfParser $parser)
{
$readPages = function ($kids, $count) use (&$readPages, $parser) {
$kids = PdfArray::ensure($kids);
$isLeaf = ($count->value === \count($kids->value));
foreach ($kids->value as $reference) {
$reference = PdfIndirectObjectReference::ensure($reference);
if ($isLeaf) {
$this->pagesList[] = $reference;
continue;
}
$object = $parser->getIndirectObject($reference->value);
$type = PdfDictionary::get($object->value, 'Type');
if ($type->value === 'Pages') {
$readPages(PdfDictionary::get($object->value, 'Kids'), PdfDictionary::get($object->value, 'Count'));
} else {
$this->pagesList[] = $object;
}
}
};
$catalog = $parser->getCatalog();
$pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $parser);
$count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $parser);
$kids = PdfType::resolve(PdfDictionary::get($pages, 'Kids'), $parser);
$readPages($kids, $count);
}
public function getAnnotationLink(PdfType $link, string $linkType)
{
// External links
if ($linkType === 'URI') {
return PdfDictionary::get($link, 'URI')->value;
}
// Internal links
if (!empty($this->pagesList)) {
$pageObj = PdfDictionary::get($link, 'D')->value[0];
foreach ($this->pagesList as $index => $page) {
if ($page->generationNumber === $pageObj->generationNumber && $page->value === $pageObj->value) {
return $index + 1;
}
}
}
return null;
}
}
Replace the Fpdi constructor with this:
$pdf = new TcpdfFpdiCustom();
"require": {
"setasign/fpdi": "^2.3",
"tecnickcom/tcpdf": "^6.4",
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With