xref: /plugin/dw2pdf/vendor/setasign/fpdi/src/PdfReader/Page.php (revision 92200750cd9cfc4a9489fe7c04c6e41b5a3de6ee)
1<?php
2
3/**
4 * This file is part of FPDI
5 *
6 * @package   setasign\Fpdi
7 * @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
8 * @license   http://opensource.org/licenses/mit-license The MIT License
9 */
10
11namespace setasign\Fpdi\PdfReader;
12
13use setasign\Fpdi\FpdiException;
14use setasign\Fpdi\GraphicsState;
15use setasign\Fpdi\Math\Vector;
16use setasign\Fpdi\PdfParser\Filter\FilterException;
17use setasign\Fpdi\PdfParser\PdfParser;
18use setasign\Fpdi\PdfParser\PdfParserException;
19use setasign\Fpdi\PdfParser\Type\PdfArray;
20use setasign\Fpdi\PdfParser\Type\PdfDictionary;
21use setasign\Fpdi\PdfParser\Type\PdfHexString;
22use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
23use setasign\Fpdi\PdfParser\Type\PdfName;
24use setasign\Fpdi\PdfParser\Type\PdfNull;
25use setasign\Fpdi\PdfParser\Type\PdfNumeric;
26use setasign\Fpdi\PdfParser\Type\PdfStream;
27use setasign\Fpdi\PdfParser\Type\PdfString;
28use setasign\Fpdi\PdfParser\Type\PdfType;
29use setasign\Fpdi\PdfParser\Type\PdfTypeException;
30use setasign\Fpdi\PdfReader\DataStructure\Rectangle;
31use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
32
33/**
34 * Class representing a page of a PDF document
35 */
36class Page
37{
38    /**
39     * @var PdfIndirectObject
40     */
41    protected $pageObject;
42
43    /**
44     * @var PdfDictionary
45     */
46    protected $pageDictionary;
47
48    /**
49     * @var PdfParser
50     */
51    protected $parser;
52
53    /**
54     * Inherited attributes
55     *
56     * @var null|array
57     */
58    protected $inheritedAttributes;
59
60    /**
61     * Page constructor.
62     *
63     * @param PdfIndirectObject $page
64     * @param PdfParser $parser
65     */
66    public function __construct(PdfIndirectObject $page, PdfParser $parser)
67    {
68        $this->pageObject = $page;
69        $this->parser = $parser;
70    }
71
72    /**
73     * Get the indirect object of this page.
74     *
75     * @return PdfIndirectObject
76     */
77    public function getPageObject()
78    {
79        return $this->pageObject;
80    }
81
82    /**
83     * Get the dictionary of this page.
84     *
85     * @return PdfDictionary
86     * @throws PdfParserException
87     * @throws PdfTypeException
88     * @throws CrossReferenceException
89     */
90    public function getPageDictionary()
91    {
92        if ($this->pageDictionary === null) {
93            $this->pageDictionary = PdfDictionary::ensure(PdfType::resolve($this->getPageObject(), $this->parser));
94        }
95
96        return $this->pageDictionary;
97    }
98
99    /**
100     * Get a page attribute.
101     *
102     * @param string $name
103     * @param bool $inherited
104     * @return PdfType|null
105     * @throws PdfParserException
106     * @throws PdfTypeException
107     * @throws CrossReferenceException
108     */
109    public function getAttribute($name, $inherited = true)
110    {
111        $dict = $this->getPageDictionary();
112
113        if (isset($dict->value[$name])) {
114            return $dict->value[$name];
115        }
116
117        $inheritedKeys = ['Resources', 'MediaBox', 'CropBox', 'Rotate'];
118        if ($inherited && \in_array($name, $inheritedKeys, true)) {
119            if ($this->inheritedAttributes === null) {
120                $this->inheritedAttributes = [];
121                $inheritedKeys = \array_filter($inheritedKeys, function ($key) use ($dict) {
122                    return !isset($dict->value[$key]);
123                });
124
125                if (\count($inheritedKeys) > 0) {
126                    $parentDict = PdfType::resolve(PdfDictionary::get($dict, 'Parent'), $this->parser);
127                    while ($parentDict instanceof PdfDictionary) {
128                        foreach ($inheritedKeys as $index => $key) {
129                            if (isset($parentDict->value[$key])) {
130                                $this->inheritedAttributes[$key] = $parentDict->value[$key];
131                                unset($inheritedKeys[$index]);
132                            }
133                        }
134
135                        /** @noinspection NotOptimalIfConditionsInspection */
136                        if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) {
137                            $parentDict = PdfType::resolve(PdfDictionary::get($parentDict, 'Parent'), $this->parser);
138                        } else {
139                            break;
140                        }
141                    }
142                }
143            }
144
145            if (isset($this->inheritedAttributes[$name])) {
146                return $this->inheritedAttributes[$name];
147            }
148        }
149
150        return null;
151    }
152
153    /**
154     * Get the rotation value.
155     *
156     * @return int
157     * @throws PdfParserException
158     * @throws PdfTypeException
159     * @throws CrossReferenceException
160     */
161    public function getRotation()
162    {
163        $rotation = $this->getAttribute('Rotate');
164        if ($rotation === null) {
165            return 0;
166        }
167
168        $rotation = PdfNumeric::ensure(PdfType::resolve($rotation, $this->parser))->value % 360;
169
170        if ($rotation < 0) {
171            $rotation += 360;
172        }
173
174        return $rotation;
175    }
176
177    /**
178     * Get a boundary of this page.
179     *
180     * @param string $box
181     * @param bool $fallback
182     * @return bool|Rectangle
183     * @throws PdfParserException
184     * @throws PdfTypeException
185     * @throws CrossReferenceException
186     * @see PageBoundaries
187     */
188    public function getBoundary($box = PageBoundaries::CROP_BOX, $fallback = true)
189    {
190        $value = $this->getAttribute($box);
191
192        if ($value !== null) {
193            return Rectangle::byPdfArray($value, $this->parser);
194        }
195
196        if ($fallback === false) {
197            return false;
198        }
199
200        switch ($box) {
201            case PageBoundaries::BLEED_BOX:
202            case PageBoundaries::TRIM_BOX:
203            case PageBoundaries::ART_BOX:
204                return $this->getBoundary(PageBoundaries::CROP_BOX, true);
205            case PageBoundaries::CROP_BOX:
206                return $this->getBoundary(PageBoundaries::MEDIA_BOX, true);
207        }
208
209        return false;
210    }
211
212    /**
213     * Get the width and height of this page.
214     *
215     * @param string $box
216     * @param bool $fallback
217     * @return array|bool
218     * @throws PdfParserException
219     * @throws PdfTypeException
220     * @throws CrossReferenceException
221     */
222    public function getWidthAndHeight($box = PageBoundaries::CROP_BOX, $fallback = true)
223    {
224        $boundary = $this->getBoundary($box, $fallback);
225        if ($boundary === false) {
226            return false;
227        }
228
229        $rotation = $this->getRotation();
230        $interchange = ($rotation / 90) % 2;
231
232        return [
233            $interchange ? $boundary->getHeight() : $boundary->getWidth(),
234            $interchange ? $boundary->getWidth() : $boundary->getHeight()
235        ];
236    }
237
238    /**
239     * Get the raw content stream.
240     *
241     * @return string
242     * @throws PdfReaderException
243     * @throws PdfTypeException
244     * @throws FilterException
245     * @throws PdfParserException
246     */
247    public function getContentStream()
248    {
249        $dict = $this->getPageDictionary();
250        $contents = PdfType::resolve(PdfDictionary::get($dict, 'Contents'), $this->parser);
251        if ($contents instanceof PdfNull) {
252            return '';
253        }
254
255        if ($contents instanceof PdfArray) {
256            $result = [];
257            foreach ($contents->value as $content) {
258                $content = PdfType::resolve($content, $this->parser);
259                if (!($content instanceof PdfStream)) {
260                    continue;
261                }
262                $result[] = $content->getUnfilteredStream();
263            }
264
265            return \implode("\n", $result);
266        }
267
268        if ($contents instanceof PdfStream) {
269            return $contents->getUnfilteredStream();
270        }
271
272        throw new PdfReaderException(
273            'Array or stream expected.',
274            PdfReaderException::UNEXPECTED_DATA_TYPE
275        );
276    }
277
278    /**
279     * Get information of all external links on this page.
280     *
281     * All coordinates are normalized in view to rotation and translation of the boundary-box, so that their
282     * origin is lower-left.
283     *
284     * The URI is the binary value of the PDF string object. It can be in PdfDocEncoding or in UTF-16BE encoding.
285     *
286     * @return array
287     */
288    public function getExternalLinks($box = PageBoundaries::CROP_BOX)
289    {
290        try {
291            $dict = $this->getPageDictionary();
292            $annotations = PdfType::resolve(PdfDictionary::get($dict, 'Annots'), $this->parser);
293        } catch (FpdiException $e) {
294            return [];
295        }
296
297        if (!$annotations instanceof PdfArray) {
298            return [];
299        }
300
301        $links = [];
302
303        foreach ($annotations->value as $entry) {
304            try {
305                $annotation = PdfType::resolve($entry, $this->parser);
306
307                $value = PdfType::resolve(PdfDictionary::get($annotation, 'Subtype'), $this->parser);
308                if (!$value instanceof PdfName || $value->value !== 'Link') {
309                    continue;
310                }
311
312                $dest = PdfType::resolve(PdfDictionary::get($annotation, 'Dest'), $this->parser);
313                if (!$dest instanceof PdfNull) {
314                    continue;
315                }
316
317                $action = PdfType::resolve(PdfDictionary::get($annotation, 'A'), $this->parser);
318                if (!$action instanceof PdfDictionary) {
319                    continue;
320                }
321
322                $actionType = PdfType::resolve(PdfDictionary::get($action, 'S'), $this->parser);
323                if (!$actionType instanceof PdfName || $actionType->value !== 'URI') {
324                    continue;
325                }
326
327                $uri = PdfType::resolve(PdfDictionary::get($action, 'URI'), $this->parser);
328                if ($uri instanceof PdfString) {
329                    $uriValue = PdfString::unescape($uri->value);
330                } elseif ($uri instanceof PdfHexString) {
331                    $uriValue = \hex2bin($uri->value);
332                } else {
333                    continue;
334                }
335
336                $rect = PdfType::resolve(PdfDictionary::get($annotation, 'Rect'), $this->parser);
337                if (!$rect instanceof PdfArray || count($rect->value) !== 4) {
338                    continue;
339                }
340
341                $rect = Rectangle::byPdfArray($rect, $this->parser);
342                if ($rect->getWidth() === 0 || $rect->getHeight() === 0) {
343                    continue;
344                }
345
346                $bbox = $this->getBoundary($box);
347                $rotation = $this->getRotation();
348
349                $gs = new GraphicsState();
350                $gs->translate(-$bbox->getLlx(), -$bbox->getLly());
351                $gs->rotate($bbox->getLlx(), $bbox->getLly(), -$rotation);
352
353                switch ($rotation) {
354                    case 90:
355                        $gs->translate(-$bbox->getWidth(), 0);
356                        break;
357                    case 180:
358                        $gs->translate(-$bbox->getWidth(), -$bbox->getHeight());
359                        break;
360                    case 270:
361                        $gs->translate(0, -$bbox->getHeight());
362                        break;
363                }
364
365                $normalizedRect = Rectangle::byVectors(
366                    $gs->toUserSpace(new Vector($rect->getLlx(), $rect->getLly())),
367                    $gs->toUserSpace(new Vector($rect->getUrx(), $rect->getUry()))
368                );
369
370                $quadPoints = PdfType::resolve(PdfDictionary::get($annotation, 'QuadPoints'), $this->parser);
371                $normalizedQuadPoints = [];
372                if ($quadPoints instanceof PdfArray) {
373                    $quadPointsCount = count($quadPoints->value);
374                    if ($quadPointsCount % 8 === 0) {
375                        for ($i = 0; ($i + 1) < $quadPointsCount; $i += 2) {
376                            $x = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i], $this->parser));
377                            $y = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i + 1], $this->parser));
378
379                            $v = $gs->toUserSpace(new Vector($x->value, $y->value));
380                            $normalizedQuadPoints[] = $v->getX();
381                            $normalizedQuadPoints[] = $v->getY();
382                        }
383                    }
384                }
385
386                // we remove unsupported/unneeded values here
387                unset(
388                    $annotation->value['P'],
389                    $annotation->value['NM'],
390                    $annotation->value['AP'],
391                    $annotation->value['AS'],
392                    $annotation->value['Type'],
393                    $annotation->value['Subtype'],
394                    $annotation->value['Rect'],
395                    $annotation->value['A'],
396                    $annotation->value['QuadPoints'],
397                    $annotation->value['Rotate'],
398                    $annotation->value['M'],
399                    $annotation->value['StructParent'],
400                    $annotation->value['OC']
401                );
402
403                // ...and flatten the PDF object to eliminate any indirect references.
404                // Indirect references are a problem when writing the output in FPDF
405                // because FPDF uses pre-calculated object numbers while FPDI creates
406                // them at runtime.
407                $annotation = PdfType::flatten($annotation, $this->parser);
408
409                $links[] = [
410                    'rect' => $normalizedRect,
411                    'quadPoints' => $normalizedQuadPoints,
412                    'uri' => $uriValue,
413                    'pdfObject' => $annotation
414                ];
415            } catch (FpdiException $e) {
416                continue;
417            }
418        }
419
420        return $links;
421    }
422}
423