xref: /plugin/dw2pdf/vendor/setasign/fpdi/src/PdfReader/PdfReader.php (revision 01d4b863f647689752623420d92bf6c538d91460) !
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\PdfParser\CrossReference\CrossReferenceException;
14use setasign\Fpdi\PdfParser\PdfParser;
15use setasign\Fpdi\PdfParser\PdfParserException;
16use setasign\Fpdi\PdfParser\Type\PdfArray;
17use setasign\Fpdi\PdfParser\Type\PdfDictionary;
18use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
19use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
20use setasign\Fpdi\PdfParser\Type\PdfNumeric;
21use setasign\Fpdi\PdfParser\Type\PdfType;
22use setasign\Fpdi\PdfParser\Type\PdfTypeException;
23
24/**
25 * A PDF reader class
26 */
27class PdfReader
28{
29    /**
30     * @var PdfParser
31     */
32    protected $parser;
33
34    /**
35     * @var int
36     */
37    protected $pageCount;
38
39    /**
40     * Indirect objects of resolved pages.
41     *
42     * @var PdfIndirectObjectReference[]|PdfIndirectObject[]
43     */
44    protected $pages = [];
45
46    /**
47     * PdfReader constructor.
48     *
49     * @param PdfParser $parser
50     */
51    public function __construct(PdfParser $parser)
52    {
53        $this->parser = $parser;
54    }
55
56    /**
57     * PdfReader destructor.
58     */
59    public function __destruct()
60    {
61        if ($this->parser !== null) {
62            $this->parser->cleanUp();
63        }
64    }
65
66    /**
67     * Get the pdf parser instance.
68     *
69     * @return PdfParser
70     */
71    public function getParser()
72    {
73        return $this->parser;
74    }
75
76    /**
77     * Get the PDF version.
78     *
79     * @return string
80     * @throws PdfParserException
81     */
82    public function getPdfVersion()
83    {
84        return \implode('.', $this->parser->getPdfVersion());
85    }
86
87    /**
88     * Get the page count.
89     *
90     * @return int
91     * @throws PdfTypeException
92     * @throws CrossReferenceException
93     * @throws PdfParserException
94     */
95    public function getPageCount()
96    {
97        if ($this->pageCount === null) {
98            $catalog = $this->parser->getCatalog();
99
100            $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser);
101            $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser);
102
103            $this->pageCount = PdfNumeric::ensure($count)->value;
104        }
105
106        return $this->pageCount;
107    }
108
109    /**
110     * Get a page instance.
111     *
112     * @param int|numeric-string $pageNumber
113     * @return Page
114     * @throws PdfTypeException
115     * @throws CrossReferenceException
116     * @throws PdfParserException
117     * @throws \InvalidArgumentException
118     */
119    public function getPage($pageNumber)
120    {
121        if (!\is_numeric($pageNumber)) {
122            throw new \InvalidArgumentException(
123                'Page number needs to be a number.'
124            );
125        }
126
127        if ($pageNumber < 1 || $pageNumber > $this->getPageCount()) {
128            throw new \InvalidArgumentException(
129                \sprintf(
130                    'Page number "%s" out of available page range (1 - %s)',
131                    $pageNumber,
132                    $this->getPageCount()
133                )
134            );
135        }
136
137        $this->readPages();
138
139        $page = $this->pages[$pageNumber - 1];
140
141        if ($page instanceof PdfIndirectObjectReference) {
142            $alreadyReadKids = [];
143            $readPages = function ($kids) use (&$readPages, &$alreadyReadKids) {
144                $kids = PdfArray::ensure($kids);
145
146                /** @noinspection LoopWhichDoesNotLoopInspection */
147                foreach ($kids->value as $reference) {
148                    $reference = PdfIndirectObjectReference::ensure($reference);
149
150                    if (\in_array($reference->value, $alreadyReadKids, true)) {
151                        throw new PdfReaderException('Recursive pages dictionary detected.');
152                    }
153                    $alreadyReadKids[] = $reference->value;
154
155                    $object = $this->parser->getIndirectObject($reference->value);
156                    $type = PdfDictionary::get($object->value, 'Type');
157
158                    if ($type->value === 'Pages') {
159                        return $readPages(PdfDictionary::get($object->value, 'Kids'));
160                    }
161
162                    return $object;
163                }
164
165                throw new PdfReaderException(
166                    'Kids array cannot be empty.',
167                    PdfReaderException::KIDS_EMPTY
168                );
169            };
170
171            $page = $this->parser->getIndirectObject($page->value);
172            $dict = PdfType::resolve($page, $this->parser);
173            $type = PdfDictionary::get($dict, 'Type');
174
175            if ($type->value === 'Pages') {
176                $kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser);
177                try {
178                    $alreadyReadKids[] = $page->objectNumber;
179                    $page = $this->pages[$pageNumber - 1] = $readPages($kids);
180                } catch (PdfReaderException $e) {
181                    if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) {
182                        throw $e;
183                    }
184
185                    // let's reset the pages array and read all page objects
186                    $this->pages = [];
187                    $this->readPages(true);
188                    // @phpstan-ignore-next-line
189                    $page = $this->pages[$pageNumber - 1];
190                }
191            } else {
192                $this->pages[$pageNumber - 1] = $page;
193            }
194        }
195
196        return new Page($page, $this->parser);
197    }
198
199    /**
200     * Walk the page tree and resolve all indirect objects of all pages.
201     *
202     * @param bool $readAll
203     * @throws CrossReferenceException
204     * @throws PdfParserException
205     * @throws PdfTypeException
206     */
207    protected function readPages($readAll = false)
208    {
209        if (\count($this->pages) > 0) {
210            return;
211        }
212
213        $expectedPageCount = $this->getPageCount();
214        $alreadyReadKids = [];
215        $readPages = function ($kids, $count) use (&$readPages, &$alreadyReadKids, $readAll, $expectedPageCount) {
216            $kids = PdfArray::ensure($kids);
217            $isLeaf = ($count->value === \count($kids->value));
218
219            foreach ($kids->value as $reference) {
220                $reference = PdfIndirectObjectReference::ensure($reference);
221
222                if (!$readAll && $isLeaf) {
223                    $this->pages[] = $reference;
224                    continue;
225                }
226
227                if (\in_array($reference->value, $alreadyReadKids, true)) {
228                    throw new PdfReaderException('Recursive pages dictionary detected.');
229                }
230                $alreadyReadKids[] = $reference->value;
231
232                $object = $this->parser->getIndirectObject($reference->value);
233                $type = PdfDictionary::get($object->value, 'Type');
234
235                if ($type->value === 'Pages') {
236                    $readPages(
237                        PdfType::resolve(PdfDictionary::get($object->value, 'Kids'), $this->parser),
238                        PdfType::resolve(PdfDictionary::get($object->value, 'Count'), $this->parser)
239                    );
240                } else {
241                    $this->pages[] = $object;
242                }
243
244                // stop if all pages are read - faulty documents exists with additional entries with invalid data.
245                if (count($this->pages) === $expectedPageCount) {
246                    break;
247                }
248            }
249        };
250
251        $catalog = $this->parser->getCatalog();
252        $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser);
253        $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser);
254        $kids = PdfType::resolve(PdfDictionary::get($pages, 'Kids'), $this->parser);
255        $readPages($kids, $count);
256    }
257}
258