Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
Stream
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
2 / 2
16
100.00% covered (success)
100.00%
1 / 1
 parse
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
13
 readBigEndian
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Extract\Xref;
16
17use Pop\Pdf\Extract\Exception;
18use Pop\Pdf\Extract\ObjectParser;
19use Pop\Pdf\Extract\Tokenizer;
20use Pop\Pdf\Extract\Filter\Budget;
21use Pop\Pdf\Extract\Filter\Registry;
22use Pop\Pdf\Extract\Value;
23
24/**
25 * Pdf extract xref stream class
26 *
27 * @category   Pop
28 * @package    Pop\Pdf
29 * @author     Nick Sagona, III <nick@popphp.org>
30 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
31 * @license    https://www.popphp.org/license     New BSD License
32 * @version    6.0.0
33 */
34class Stream
35{
36
37    /**
38     * Parse a cross-reference stream at a byte position
39     *
40     * @param  string  $data
41     * @param  int     $pos
42     * @param  ?Budget $budget
43     * @throws Exception
44     * @return array
45     */
46    public static function parse(string $data, int $pos, ?Budget $budget = null): array
47    {
48        $tokenizer = new Tokenizer($data, $pos);
49
50        $tokenizer->next(); // object number
51        $tokenizer->next(); // generation number
52        $objToken = $tokenizer->next();
53
54        if (($objToken['type'] !== 'keyword') || ($objToken['value'] !== 'obj')) {
55            throw new Exception('Error: Expected obj keyword for xref stream.');
56        }
57
58        $parser = new ObjectParser($tokenizer);
59        $value  = $parser->parseValue();
60
61        if (!($value instanceof Value\Stream)) {
62            throw new Exception('Error: Expected a stream object for xref stream.');
63        }
64
65        $dict = $value->dict;
66        $raw  = Registry::decode($value->raw, $dict['Filter'] ?? null, $dict['DecodeParms'] ?? null, $budget);
67
68        $w = $dict['W'] ?? null;
69        if (!is_array($w) || (count($w) < 3)) {
70            throw new Exception('Error: Malformed xref stream /W array.');
71        }
72
73        $index = $dict['Index'] ?? [0, $dict['Size'] ?? 0];
74
75        $offsets = [];
76        $rowLen  = $w[0] + $w[1] + $w[2];
77        $bytePos = 0;
78        $rawLen  = strlen($raw);
79
80        for ($i = 0; $i < count($index); $i += 2) {
81            $startObj = $index[$i];
82            $count    = $index[$i + 1];
83
84            for ($j = 0; $j < $count; $j++) {
85                if (($bytePos + $rowLen) > $rawLen) {
86                    break 2;
87                }
88
89                $type   = ($w[0] === 0) ? 1 : self::readBigEndian($raw, $bytePos, $w[0]);
90                $field2 = self::readBigEndian($raw, $bytePos + $w[0], $w[1]);
91                $field3 = self::readBigEndian($raw, $bytePos + $w[0] + $w[1], $w[2]);
92                $bytePos += $rowLen;
93
94                $objNum = $startObj + $j;
95
96                if (isset($offsets[$objNum])) {
97                    continue;
98                }
99
100                if ($type === 1) {
101                    $offsets[$objNum] = ['offset' => $field2];
102                } elseif ($type === 2) {
103                    $offsets[$objNum] = ['inStream' => $field2, 'index' => $field3];
104                }
105            }
106        }
107
108        return ['offsets' => $offsets, 'trailer' => $dict, 'endPos' => $tokenizer->getPosition()];
109    }
110
111    /**
112     * Read a big-endian integer of a given byte size from a string
113     *
114     * @param  string $data
115     * @param  int    $offset
116     * @param  int    $size
117     * @return int
118     */
119    protected static function readBigEndian(string $data, int $offset, int $size): int
120    {
121        if ($size === 0) {
122            return 0;
123        }
124
125        $value = 0;
126        for ($i = 0; $i < $size; $i++) {
127            $value = ($value << 8) | ord($data[$offset + $i]);
128        }
129
130        return $value;
131    }
132
133}