Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preserve internal links using FPDI

Tags:

fpdf

tcpdf

fpdi

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/

like image 454
Eduardinho Teixeira Avatar asked Apr 30 '15 12:04

Eduardinho Teixeira


1 Answers

TCPDF + FPDI approach too keep internal and external links

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;
    }
}

Usage

Replace the Fpdi constructor with this:

$pdf = new TcpdfFpdiCustom();

Composer packages used:

"require": {
    "setasign/fpdi": "^2.3",
    "tecnickcom/tcpdf": "^6.4",
}
like image 53
igorsgm Avatar answered Oct 20 '22 12:10

igorsgm