Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
17 / 17
CRAP
100.00% covered (success)
100.00%
1 / 1
StreamObject
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
17 / 17
48
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 setDefinition
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
12
 setStream
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 appendStream
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDefinition
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 encode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 decode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 isEncoded
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEncoding
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPalette
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isPalette
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isXObject
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getByteLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 calculateByteLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
11
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\Build\PdfObject;
16
17/**
18 * Pdf stream object class
19 *
20 * @category   Pop
21 * @package    Pop\Pdf
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    6.0.0
26 */
27class StreamObject extends AbstractObject
28{
29
30    /**
31     * PDF stream object index
32     * @var ?int
33     */
34    protected ?int $index = 5;
35
36    /**
37     * PDF stream object definition
38     * @var ?string
39     */
40    protected ?string $definition = null;
41
42    /**
43     * PDF stream object stream
44     * @var ?string
45     */
46    protected ?string $stream = null;
47
48    /**
49     * Encoding filter
50     * @var ?string
51     */
52    protected ?string $encoding = null;
53
54    /**
55     * Palette object flag
56     * @var bool
57     */
58    protected bool $isPalette = false;
59
60    /**
61     * XObject object flag
62     * @var bool
63     */
64    protected bool $isXObject = false;
65
66    /**
67     * Constructor
68     *
69     * Instantiate a PDF stream object.
70     *
71     * @param  int $index
72     */
73    public function __construct(int $index = 5)
74    {
75        $this->setIndex($index);
76        $this->setData("[{object_index}] 0 obj\n[{definition}]\n[{stream}]\nendobj\n\n");
77    }
78
79    /**
80     * Parse a stream object from a string
81     *
82     * @param  string $stream
83     * @return StreamObject
84     */
85    public static function parse(string $stream): StreamObject
86    {
87        $object = new self();
88        $object->setIndex((int)substr($stream, 0, strpos($stream, ' ')));
89        $stream = str_replace($object->getIndex() . ' 0 obj', '[{object_index}] 0 obj', $stream);
90
91        // Determine the objects definition and stream, if applicable.
92        $s = substr($stream, (strpos($stream, ' obj') + 4));
93        $s = substr($s, 0, strpos($s, 'endobj'));
94        if (str_contains($s, 'stream')) {
95            $def = substr($s, 0, strpos($s, 'stream'));
96            $str = substr($s, (strpos($s, 'stream') + 6));
97            $str = substr($str, 0, strpos($str, 'endstream'));
98
99            // __toString() always re-adds the EOL before 'endstream' itself, so
100            // an EOL captured here from the original string would otherwise be
101            // duplicated, making the declared and actual stream lengths disagree.
102            if (str_ends_with($str, "\r\n")) {
103                $str = substr($str, 0, -2);
104            } else if (str_ends_with($str, "\n") || str_ends_with($str, "\r")) {
105                $str = substr($str, 0, -1);
106            }
107
108            $object->setDefinition($def);
109            $object->appendStream($str);
110        } else {
111            $object->setDefinition($s);
112        }
113
114        $object->setData("[{object_index}] 0 obj\n[{definition}]\n[{stream}]\nendobj\n\n");
115        return $object;
116    }
117
118    /**
119     * Set the stream object definition
120     *
121     * @param  string $definition
122     * @return StreamObject
123     */
124    public function setDefinition(string $definition): StreamObject
125    {
126        $this->definition = (string)$definition;
127
128        if (str_contains($this->definition, '/ASCIIHexDecode')) {
129            $this->encoding = 'ASCIIHexDecode';
130        } else if (str_contains($this->definition, '/ASCII85Decode')) {
131            $this->encoding = 'ASCII85Decode';
132        } else if (str_contains($this->definition, '/LZWDecode')) {
133            $this->encoding = 'LZWDecode';
134        } else if (str_contains($this->definition, '/FlateDecode')) {
135            $this->encoding = 'FlateDecode';
136        } else if (str_contains($this->definition, '/RunLengthDecode')) {
137            $this->encoding = 'RunLengthDecode';
138        } else if (str_contains($this->definition, '/CCITTFaxDecode')) {
139            $this->encoding = 'CCITTFaxDecode';
140        } else if (str_contains($this->definition, '/JBIG2Decode')) {
141            $this->encoding = 'JBIG2Decode';
142        } else if (str_contains($this->definition, '/DCTDecode')) {
143            $this->encoding = 'DCTDecode';
144        } else if (str_contains($this->definition, '/JPXDecode')) {
145            $this->encoding = 'JPXDecode';
146        } else if (str_contains($this->definition, '/Crypt')) {
147            $this->encoding = 'Crypt';
148        }
149
150        if (stripos($this->definition, '/xobject') !== false) {
151            $this->isXObject = true;
152        }
153
154        return $this;
155    }
156
157    /**
158     * Set the stream object stream
159     *
160     * @param  string $stream
161     * @return StreamObject
162     */
163    public function setStream(string $stream): StreamObject
164    {
165        $this->stream = $stream;
166        return $this;
167    }
168
169    /**
170     * Append to the stream the PDF stream object
171     *
172     * @param  string $stream
173     * @return StreamObject
174     */
175    public function appendStream(string $stream): StreamObject
176    {
177        $this->stream .= $stream;
178        return $this;
179    }
180
181    /**
182     * Get the stream object definition
183     *
184     * @return ?string
185     */
186    public function getDefinition(): ?string
187    {
188        return $this->definition;
189    }
190
191    /**
192     * Get the PDF stream object stream
193     *
194     * @return ?string
195     */
196    public function getStream(): ?string
197    {
198        return $this->stream;
199    }
200
201    /**
202     * Method to encode the PDF stream object with FlateDecode (gzcompress)
203     *
204     * @return void
205     */
206    public function encode(): void
207    {
208        if (($this->stream != '') && (function_exists('gzcompress')) &&
209            (!str_contains((string)$this->definition, ' /Image')) && (!str_contains((string)$this->definition, '/FlateDecode'))) {
210            $this->stream   = "\n" . gzcompress($this->stream, 9) . "\n";
211            $this->encoding = 'FlateDecode';
212        }
213    }
214
215    /**
216     * Method to decode the PDF stream contents with FlateDecode (gzuncompress)
217     *
218     * @return bool|string
219     */
220    public function decode(): bool|string
221    {
222        $decoded = false;
223        if (($this->stream != '') && function_exists('gzuncompress')) {
224            $decoded = @gzuncompress(trim($this->stream));
225        }
226        return $decoded;
227    }
228
229    /**
230     * Determine whether or not the PDF stream object is encoded
231     *
232     * @return bool
233     */
234    public function isEncoded(): bool
235    {
236        return ($this->encoding !== null);
237    }
238
239    /**
240     * Get the encoding filter
241     *
242     * @return ?string
243     */
244    public function getEncoding(): ?string
245    {
246        return $this->encoding;
247    }
248
249    /**
250     * Set whether the PDF stream object is a palette object
251     *
252     * @param  bool $isPalette
253     * @return StreamObject
254     */
255    public function setPalette(bool $isPalette): StreamObject
256    {
257        $this->isPalette = $isPalette;
258        return $this;
259    }
260
261    /**
262     * Get whether the PDF stream object is a palette object
263     *
264     * @return bool
265     */
266    public function isPalette(): bool
267    {
268        return $this->isPalette;
269    }
270
271    /**
272     * Get whether the PDF stream object is an XObject
273     *
274     * @return bool
275     */
276    public function isXObject(): bool
277    {
278        return $this->isXObject;
279    }
280
281    /**
282     * Get the PDF stream object byte length
283     *
284     * @return int
285     */
286    public function getByteLength(): int
287    {
288        return $this->calculateByteLength((string)$this);
289    }
290
291    /**
292     * Calculate the byte length of a string
293     *
294     * @param  ?string $string
295     * @return int
296     */
297    protected function calculateByteLength(?string $string): int
298    {
299        return strlen((string)$string);
300    }
301
302    /**
303     * Method to print the PDF stream object.
304     *
305     * @return string
306     */
307    public function __toString(): string
308    {
309        // Set the stream, adding linefeed
310        $stream = ($this->stream !== null) ? "stream" . $this->stream . "\nendstream\n" : '';
311
312        // Set up the Length definition. The match/replace is scoped to the
313        // exact "/Length N" (or indirect "/Length N G R") span via
314        // preg_replace's own single-match substitution, rather than a
315        // blanket string search-and-replace on the extracted digits - a
316        // blanket replace would corrupt any other dict value that happens
317        // to contain the same digit substring (e.g. "/Length 38" colliding
318        // with "/Width 384"), and would leave a dangling, meaningless
319        // indirect reference behind for a source using indirect /Length.
320        if ((preg_match('/\/Length\s+\d+(?:\s+\d+\s+R)?/', (string) $this->definition)) &&
321            (!str_contains((string)$this->definition, '/Length1')) &&
322            (!str_contains((string)$this->definition, '/Image'))) {
323            $this->definition = preg_replace(
324                '/\/Length\s+\d+(?:\s+\d+\s+R)?/', '/Length [{byte_length}]', $this->definition, 1
325            );
326        } else if (!str_contains((string)$this->definition, '/Length')) {
327            $this->definition .= "<</Length [{byte_length}]>>\n";
328        }
329
330        // Calculate the byte length of the stream and swap out the placeholders.
331        $byteLength = (($this->encoding == 'FlateDecode') && (function_exists('gzcompress')) &&
332            (!str_contains((string)$this->definition, ' /Image')) && (!str_contains((string)$this->definition, '/FlateDecode'))) ?
333            $this->calculateByteLength($this->stream) . " /Filter /FlateDecode" : $this->calculateByteLength($this->stream);
334
335        $data = str_replace(
336            ['[{object_index}]', '[{stream}]', '[{definition}]', '[{byte_length}]'],
337            [(string)$this->index, $stream, (string)$this->definition, (string)$byteLength],
338            $this->data
339        );
340
341        // Clear Length definition if it is zero.
342        if (str_contains((string)$data, '<</Length 0>>')) {
343            $data = str_replace('<</Length 0>>', '', $data);
344        }
345
346        return $data;
347    }
348
349}