xref: /plugin/dw2pdf/vendor/setasign/fpdi/src/PdfParser/CrossReference/FixedReader.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\PdfParser\CrossReference;
12
13use setasign\Fpdi\PdfParser\PdfParser;
14use setasign\Fpdi\PdfParser\StreamReader;
15
16/**
17 * Class FixedReader
18 *
19 * This reader allows a very less overhead parsing of single entries of the cross-reference, because the main entries
20 * are only read when needed and not in a single run.
21 */
22class FixedReader extends AbstractReader implements ReaderInterface
23{
24    /**
25     * @var StreamReader
26     */
27    protected $reader;
28
29    /**
30     * Data of subsections.
31     *
32     * @var array
33     */
34    protected $subSections;
35
36    /**
37     * FixedReader constructor.
38     *
39     * @param PdfParser $parser
40     * @throws CrossReferenceException
41     */
42    public function __construct(PdfParser $parser)
43    {
44        $this->reader = $parser->getStreamReader();
45        $this->read();
46        parent::__construct($parser);
47    }
48
49    /**
50     * Get all subsection data.
51     *
52     * @return array
53     */
54    public function getSubSections()
55    {
56        return $this->subSections;
57    }
58
59    /**
60     * @inheritdoc
61     * @return int|false
62     */
63    public function getOffsetFor($objectNumber)
64    {
65        foreach ($this->subSections as $offset => list($startObject, $objectCount)) {
66            /**
67             * @var int $startObject
68             * @var int $objectCount
69             */
70            if ($objectNumber >= $startObject && $objectNumber < ($startObject + $objectCount)) {
71                $position = $offset + 20 * ($objectNumber - $startObject);
72                $this->reader->ensure($position, 20);
73                $line = $this->reader->readBytes(20);
74                if ($line === false || $line[17] === 'f') {
75                    return false;
76                }
77
78                return (int) \substr($line, 0, 10);
79            }
80        }
81
82        return false;
83    }
84
85    /**
86     * Read the cross-reference.
87     *
88     * This reader will only read the subsections in this method. The offsets were resolved individually by this
89     * information.
90     *
91     * @throws CrossReferenceException
92     */
93    protected function read()
94    {
95        $subSections = [];
96
97        $startObject = $entryCount = $lastLineStart = null;
98        $validityChecked = false;
99        while (($line = $this->reader->readLine(20)) !== false) {
100            if (\strpos($line, 'trailer') !== false) {
101                $this->reader->reset($lastLineStart);
102                break;
103            }
104
105            // jump over if line content doesn't match the expected string
106            if (\sscanf($line, '%d %d', $startObject, $entryCount) !== 2) {
107                continue;
108            }
109
110            $oldPosition = $this->reader->getPosition();
111            $position = $oldPosition + $this->reader->getOffset();
112
113            if (!$validityChecked && $entryCount > 0) {
114                $nextLine = $this->reader->readBytes(21);
115                /* Check the next line for maximum of 20 bytes and not longer
116                 * By catching 21 bytes and trimming the length should be still 21.
117                 */
118                if (\strlen(\trim($nextLine)) !== 21) {
119                    throw new CrossReferenceException(
120                        'Cross-reference entries are larger than 20 bytes.',
121                        CrossReferenceException::ENTRIES_TOO_LARGE
122                    );
123                }
124
125                /* Check for less than 20 bytes: cut the line to 20 bytes and trim; have to result in exactly 18 bytes.
126                 * If it would have less bytes the substring would get the first bytes of the next line which would
127                 * evaluate to a 20 bytes long string after trimming.
128                 */
129                if (\strlen(\trim(\substr($nextLine, 0, 20))) !== 18) {
130                    throw new CrossReferenceException(
131                        'Cross-reference entries are less than 20 bytes.',
132                        CrossReferenceException::ENTRIES_TOO_SHORT
133                    );
134                }
135
136                $validityChecked = true;
137            }
138
139            $subSections[$position] = [$startObject, $entryCount];
140
141            $lastLineStart = $position + $entryCount * 20;
142            $this->reader->reset($lastLineStart);
143        }
144
145        // reset after the last correct parsed line
146        $this->reader->reset($lastLineStart);
147
148        if (\count($subSections) === 0) {
149            throw new CrossReferenceException(
150                'No entries found in cross-reference.',
151                CrossReferenceException::NO_ENTRIES
152            );
153        }
154
155        $this->subSections = $subSections;
156    }
157
158    /**
159     * Fixes an invalid object number shift.
160     *
161     * This method can be used to repair documents with an invalid subsection header:
162     *
163     * <code>
164     * xref
165     * 1 7
166     * 0000000000 65535 f
167     * 0000000009 00000 n
168     * 0000412075 00000 n
169     * 0000412172 00000 n
170     * 0000412359 00000 n
171     * 0000412417 00000 n
172     * 0000412468 00000 n
173     * </code>
174     *
175     * It shall only be called on the first table.
176     *
177     * @return bool
178     */
179    public function fixFaultySubSectionShift()
180    {
181        $subSections = $this->getSubSections();
182        if (\count($subSections) > 1) {
183            return false;
184        }
185
186        $subSection = \current($subSections);
187        if ($subSection[0] != 1) {
188            return false;
189        }
190
191        if ($this->getOffsetFor(1) === false) {
192            foreach ($subSections as $offset => list($startObject, $objectCount)) {
193                $this->subSections[$offset] = [$startObject - 1, $objectCount];
194            }
195            return true;
196        }
197
198        return false;
199    }
200}
201