Preserve internal links using FPDI
Asked Answered
A

2

9

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/

Anoxemia answered 30/4, 2015 at 12:56 Comment(3)
You may try an old version of FPDI (< 1.5) with this class.Plow
Did you get anywhere with this (other than downgrading FPDI back to < 1.5)?Biblio
Hello, this is very good question. Any progress on this?Astute
O
3

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",
}
Odeen answered 13/4, 2021 at 8:58 Comment(0)
T
0

I have been working with TCPDF,FPDF, FPDI, Imagick, Ghostscript for last 4 years and I do understand the challenge you are facing with but unfortunetly the technology is not there yet. So the answer is NO.

Tailstock answered 8/9, 2016 at 20:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.