Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
93.10% |
54 / 58 |
|
75.00% |
6 / 8 |
CRAP | |
0.00% |
0 / 1 |
| CrossReferenceStream | |
93.10% |
54 / 58 |
|
75.00% |
6 / 8 |
24.19 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addInUseEntry | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addFreeEntry | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addCompressedEntry | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| packAllEntries | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
6 | |||
| bytesNeeded | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
5.26 | |||
| packField | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| toPdf | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
8.01 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Phpdftk\Pdf\Core\Document; |
| 6 | |
| 7 | use Phpdftk\Pdf\Core\PdfArray; |
| 8 | use Phpdftk\Pdf\Core\PdfDictionary; |
| 9 | use Phpdftk\Pdf\Core\PdfName; |
| 10 | use Phpdftk\Pdf\Core\PdfNumber; |
| 11 | use Phpdftk\Pdf\Core\PdfReference; |
| 12 | use Phpdftk\Pdf\Core\PdfStream; |
| 13 | use Phpdftk\Pdf\Core\PdfVersion; |
| 14 | use 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)] |
| 31 | class 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 | } |