xref: /plugin/dw2pdf/vendor/setasign/fpdi/src/PdfParser/CrossReference/LineReader.php (revision 9b10ebbf2c34b11edf9f813f487020bcb74a94b3)
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 LineReader
18 *
19 * This reader class read all cross-reference entries in a single run.
20 * It supports reading cross-references with e.g. invalid data (e.g. entries with a length < or > 20 bytes).
21 */
22class LineReader extends AbstractReader implements ReaderInterface
23{
24    /**
25     * The object offsets.
26     *
27     * @var array
28     */
29    protected $offsets;
30
31    /**
32     * LineReader constructor.
33     *
34     * @param PdfParser $parser
35     * @throws CrossReferenceException
36     */
37    public function __construct(PdfParser $parser)
38    {
39        $this->read($this->extract($parser->getStreamReader()));
40        parent::__construct($parser);
41    }
42
43    /**
44     * @inheritdoc
45     * @return int|false
46     */
47    public function getOffsetFor($objectNumber)
48    {
49        if (isset($this->offsets[$objectNumber])) {
50            return $this->offsets[$objectNumber][0];
51        }
52
53        return false;
54    }
55
56    /**
57     * Get all found offsets.
58     *
59     * @return array
60     */
61    public function getOffsets()
62    {
63        return $this->offsets;
64    }
65
66    /**
67     * Extracts the cross reference data from the stream reader.
68     *
69     * @param StreamReader $reader
70     * @return string
71     * @throws CrossReferenceException
72     */
73    protected function extract(StreamReader $reader)
74    {
75        $bytesPerCycle = 100;
76        $reader->reset(null, $bytesPerCycle);
77
78        $cycles = 0;
79        do {
80            // 6 = length of "trailer" - 1
81            $pos = \max(($bytesPerCycle * $cycles) - 6, 0);
82            $trailerPos = \strpos($reader->getBuffer(false), 'trailer', $pos);
83            $cycles++;
84        } while ($trailerPos === false && $reader->increaseLength($bytesPerCycle) !== false);
85
86        if ($trailerPos === false) {
87            throw new CrossReferenceException(
88                'Unexpected end of cross reference. "trailer"-keyword not found.',
89                CrossReferenceException::NO_TRAILER_FOUND
90            );
91        }
92
93        $xrefContent = \substr($reader->getBuffer(false), 0, $trailerPos);
94        $reader->reset($reader->getPosition() + $trailerPos);
95
96        return $xrefContent;
97    }
98
99    /**
100     * Read the cross-reference entries.
101     *
102     * @param string $xrefContent
103     * @throws CrossReferenceException
104     */
105    protected function read($xrefContent)
106    {
107        // get eol markers in the first 100 bytes
108        \preg_match_all("/(\r\n|\n|\r)/", \substr($xrefContent, 0, 100), $m);
109
110        if (\count($m[0]) === 0) {
111            throw new CrossReferenceException(
112                'No data found in cross-reference.',
113                CrossReferenceException::INVALID_DATA
114            );
115        }
116
117        // count(array_count_values()) is faster then count(array_unique())
118        // @see https://github.com/symfony/symfony/pull/23731
119        // can be reverted for php7.2
120        $differentLineEndings = \count(\array_count_values($m[0]));
121        if ($differentLineEndings > 1) {
122            $lines = \preg_split("/(\r\n|\n|\r)/", $xrefContent, -1, PREG_SPLIT_NO_EMPTY);
123        } else {
124            $lines = \explode($m[0][0], $xrefContent);
125        }
126
127        unset($differentLineEndings, $m);
128        if (!\is_array($lines)) {
129            $this->offsets = [];
130            return;
131        }
132
133        $start = 0;
134        $offsets = [];
135
136        // trim all lines and remove empty lines
137        $lines = \array_filter(\array_map('\trim', $lines));
138        foreach ($lines as $line) {
139            $pieces = \explode(' ', $line);
140
141            switch (\count($pieces)) {
142                case 2:
143                    $start = (int) $pieces[0];
144                    break;
145
146                case 3:
147                    switch ($pieces[2]) {
148                        case 'n':
149                            $offsets[$start] = [(int) $pieces[0], (int) $pieces[1]];
150                            $start++;
151                            break 2;
152                        case 'f':
153                            $start++;
154                            break 2;
155                    }
156                    // fall through if pieces doesn't match
157
158                default:
159                    throw new CrossReferenceException(
160                        \sprintf('Unexpected data in xref table (%s)', \implode(' ', $pieces)),
161                        CrossReferenceException::INVALID_DATA
162                    );
163            }
164        }
165
166        $this->offsets = $offsets;
167    }
168}
169