Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.10% covered (success)
93.10%
54 / 58
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
CrossReferenceStream
93.10% covered (success)
93.10%
54 / 58
75.00% covered (warning)
75.00%
6 / 8
24.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addInUseEntry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addFreeEntry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addCompressedEntry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 packAllEntries
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
6
 bytesNeeded
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
5.26
 packField
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 toPdf
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
8.01
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Core\Document;
6
7use Phpdftk\Pdf\Core\PdfArray;
8use Phpdftk\Pdf\Core\PdfDictionary;
9use Phpdftk\Pdf\Core\PdfName;
10use Phpdftk\Pdf\Core\PdfNumber;
11use Phpdftk\Pdf\Core\PdfReference;
12use Phpdftk\Pdf\Core\PdfStream;
13use Phpdftk\Pdf\Core\PdfVersion;
14use Phpdftk\Pdf\Core\RequiresPdfVersion;
15
16/**
17 * Cross-reference stream (/Type /XRef) — ISO 32000-2 §7.5.8.
18 *
19 * A PDF 1.5+ alternative to the classic xref table. Holds xref entries as
20 * binary data inside a stream object whose dictionary also carries the
21 * trailer entries (Size, Root, Info, ID, Prev, Encrypt).
22 *
23 * Stream entries are variable-width records described by the /W array:
24 *   [type, field2, field3]
25 * Three standard entry types:
26 *   0 = free   (next free obj num, generation to reuse)
27 *   1 = in use (byte offset, generation)
28 *   2 = compressed (obj num of containing ObjStm, index within stream)
29 */
30#[RequiresPdfVersion(PdfVersion::V1_5)]
31class CrossReferenceStream extends PdfStream
32{
33    public const PDF_TYPE = 'XRef';
34
35    public int $size = 0;                       // /Size  (required)
36    public ?PdfArray $index = null;             // /Index (optional)
37    public ?int $prev = null;                   // /Prev  (optional)
38    public ?PdfArray $w = null;                 // /W     (required)
39    public ?PdfReference $root = null;          // /Root
40    public ?PdfReference $info = null;          // /Info
41    public ?PdfArray $id = null;                // /ID
42    public ?PdfReference $encrypt = null;       // /Encrypt
43
44    /** @var list<array{int, int, int}> Raw entries: [type, field2, field3] triples */
45    private array $rawEntries = [];
46
47    /** @var int[] Field widths in bytes [w0, w1, w2] */
48    private array $fieldWidths = [1, 4, 2];
49
50    public function __construct()
51    {
52        parent::__construct(new PdfDictionary(), '');
53    }
54
55    /**
56     * Append a type-1 (in-use) entry.
57     */
58    public function addInUseEntry(int $offset, int $generation = 0): void
59    {
60        $this->rawEntries[] = [1, $offset, $generation];
61    }
62
63    /**
64     * Append a type-0 (free) entry.
65     */
66    public function addFreeEntry(int $nextFree = 0, int $generation = 65535): void
67    {
68        $this->rawEntries[] = [0, $nextFree, $generation];
69    }
70
71    /**
72     * Append a type-2 (compressed) entry referring to an object stream.
73     */
74    public function addCompressedEntry(int $objStmObjNum, int $indexInStream): void
75    {
76        $this->rawEntries[] = [2, $objStmObjNum, $indexInStream];
77    }
78
79    /**
80     * Auto-detect optimal /W field widths based on the maximum values
81     * in the recorded entries, then pack all entries into stream data.
82     *
83     * Called automatically by toPdf(); can also be called manually
84     * to inspect the packed data before serialization.
85     */
86    public function packAllEntries(): void
87    {
88        if (empty($this->rawEntries)) {
89            return;
90        }
91
92        // Find max values for each field
93        $maxField2 = 0;
94        $maxField3 = 0;
95        foreach ($this->rawEntries as [$type, $f2, $f3]) {
96            if ($f2 > $maxField2) {
97                $maxField2 = $f2;
98            }
99            if ($f3 > $maxField3) {
100                $maxField3 = $f3;
101            }
102        }
103
104        // Determine minimum bytes needed for each field
105        $this->fieldWidths = [
106            1, // type is always 1 byte
107            self::bytesNeeded($maxField2),
108            self::bytesNeeded($maxField3),
109        ];
110
111        $this->w = new PdfArray([
112            new PdfNumber($this->fieldWidths[0]),
113            new PdfNumber($this->fieldWidths[1]),
114            new PdfNumber($this->fieldWidths[2]),
115        ]);
116
117        // Pack all entries with the computed widths
118        $this->data = '';
119        foreach ($this->rawEntries as [$type, $f2, $f3]) {
120            $this->data .= self::packField($type, $this->fieldWidths[0]);
121            $this->data .= self::packField($f2, $this->fieldWidths[1]);
122            $this->data .= self::packField($f3, $this->fieldWidths[2]);
123        }
124    }
125
126    /**
127     * Determine minimum bytes to represent a value.
128     */
129    private static function bytesNeeded(int $value): int
130    {
131        if ($value <= 0xFF) {
132            return 1;
133        }
134        if ($value <= 0xFFFF) {
135            return 2;
136        }
137        if ($value <= 0xFFFFFF) {
138            return 3;
139        }
140        return 4;
141    }
142
143    /**
144     * Pack an integer into a big-endian byte string of the given width.
145     */
146    private static function packField(int $value, int $width): string
147    {
148        $result = '';
149        for ($i = $width - 1; $i >= 0; $i--) {
150            $result .= chr(($value >> ($i * 8)) & 0xFF);
151        }
152        return $result;
153    }
154
155    public function toPdf(): string
156    {
157        // Pack entries with auto-detected optimal field widths
158        $this->packAllEntries();
159
160        $this->dictionary = new PdfDictionary();
161        $this->dictionary->set('Type', new PdfName(self::PDF_TYPE));
162        $this->dictionary->set('Size', new PdfNumber($this->size));
163        if ($this->index !== null) {
164            $this->dictionary->set('Index', $this->index);
165        }
166        if ($this->prev !== null) {
167            $this->dictionary->set('Prev', new PdfNumber($this->prev));
168        }
169        if ($this->w !== null) {
170            $this->dictionary->set('W', $this->w);
171        }
172        if ($this->root !== null) {
173            $this->dictionary->set('Root', $this->root);
174        }
175        if ($this->info !== null) {
176            $this->dictionary->set('Info', $this->info);
177        }
178        if ($this->id !== null) {
179            $this->dictionary->set('ID', $this->id);
180        }
181        if ($this->encrypt !== null) {
182            $this->dictionary->set('Encrypt', $this->encrypt);
183        }
184
185        return parent::toPdf();
186    }
187}