1<?php
2/**
3 * This file is part of FPDI
4 *
5 * @package   setasign\Fpdi
6 * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com)
7 * @license   http://opensource.org/licenses/mit-license The MIT License
8 */
9
10namespace setasign\Fpdi\PdfParser\CrossReference;
11
12use setasign\Fpdi\PdfParser\PdfParser;
13use setasign\Fpdi\PdfParser\StreamReader;
14
15/**
16 * Class LineReader
17 *
18 * This reader class read all cross-reference entries in a single run.
19 * It supports reading cross-references with e.g. invalid data (e.g. entries with a length < or > 20 bytes).
20 *
21 * @package setasign\Fpdi\PdfParser\CrossReference
22 */
23class LineReader extends AbstractReader implements ReaderInterface
24{
25    /**
26     * The object offsets.
27     *
28     * @var array
29     */
30    protected $offsets;
31
32    /**
33     * LineReader constructor.
34     *
35     * @param PdfParser $parser
36     * @throws CrossReferenceException
37     */
38    public function __construct(PdfParser $parser)
39    {
40        $this->read($this->extract($parser->getStreamReader()));
41        parent::__construct($parser);
42    }
43
44    /**
45     * @inheritdoc
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        $cycles = -1;
76        $bytesPerCycle = 100;
77
78        $reader->reset(null, $bytesPerCycle);
79
80        while (
81            ($trailerPos = \strpos($reader->getBuffer(false), 'trailer', \max($bytesPerCycle * $cycles++, 0))) === false
82        ) {
83            if ($reader->increaseLength($bytesPerCycle) === false) {
84                break;
85            }
86        }
87
88        if ($trailerPos === false) {
89            throw new CrossReferenceException(
90                'Unexpected end of cross reference. "trailer"-keyword not found.',
91                CrossReferenceException::NO_TRAILER_FOUND
92            );
93        }
94
95        $xrefContent = \substr($reader->getBuffer(false), 0, $trailerPos);
96        $reader->reset($reader->getPosition() + $trailerPos);
97
98        return $xrefContent;
99    }
100
101    /**
102     * Read the cross-reference entries.
103     *
104     * @param string $xrefContent
105     * @throws CrossReferenceException
106     */
107    protected function read($xrefContent)
108    {
109        // get eol markers in the first 100 bytes
110        \preg_match_all("/(\r\n|\n|\r)/", \substr($xrefContent, 0, 100), $m);
111
112        if (\count($m[0]) === 0) {
113            throw new CrossReferenceException(
114                'No data found in cross-reference.',
115                CrossReferenceException::INVALID_DATA
116            );
117        }
118
119        // count(array_count_values()) is faster then count(array_unique())
120        // @see https://github.com/symfony/symfony/pull/23731
121        // can be reverted for php7.2
122        $differentLineEndings = \count(\array_count_values($m[0]));
123        if ($differentLineEndings > 1) {
124            $lines = \preg_split("/(\r\n|\n|\r)/", $xrefContent, -1, PREG_SPLIT_NO_EMPTY);
125        } else {
126            $lines = \explode($m[0][0], $xrefContent);
127        }
128
129        unset($differentLineEndings, $m);
130        $linesCount = \count($lines);
131        $start = null;
132        $entryCount = 0;
133
134        $offsets = [];
135
136        /** @noinspection ForeachInvariantsInspection */
137        for ($i = 0; $i < $linesCount; $i++) {
138            $line = \trim($lines[$i]);
139            if ($line) {
140                $pieces = \explode(' ', $line);
141
142                $c = \count($pieces);
143                switch ($c) {
144                    case 2:
145                        $start = (int) $pieces[0];
146                        $entryCount += (int) $pieces[1];
147                        break;
148
149                    /** @noinspection PhpMissingBreakStatementInspection */
150                    case 3:
151                        switch ($pieces[2]) {
152                            case 'n':
153                                $offsets[$start] = [(int) $pieces[0], (int) $pieces[1]];
154                                $start++;
155                                break 2;
156                            case 'f':
157                                $start++;
158                                break 2;
159                        }
160                        // fall through if pieces doesn't match
161
162                    default:
163                        throw new CrossReferenceException(
164                            \sprintf('Unexpected data in xref table (%s)', \implode(' ', $pieces)),
165                            CrossReferenceException::INVALID_DATA
166                        );
167                }
168            }
169        }
170
171        $this->offsets = $offsets;
172    }
173}
174