Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.39% covered (success)
99.39%
163 / 164
96.30% covered (success)
96.30%
26 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageObject
99.39% covered (success)
99.39%
163 / 164
96.30% covered (success)
96.30%
26 / 27
60
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 parse
98.72% covered (success)
98.72%
77 / 78
0.00% covered (danger)
0.00%
0 / 1
21
 setParentIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setWidth
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHeight
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setAnnots
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setContent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setXObjects
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setFonts
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setCurrentContentIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setPageExtra
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setOtherResources
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addAnnotIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addContentIndex
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addXObjectReference
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 addFontReference
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getParentIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentContentIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAnnots
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getXObjects
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFonts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAnnot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
33 / 33
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 page 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 PageObject extends AbstractObject
28{
29
30    /**
31     * PDF page object index
32     * @var ?int
33     */
34    protected ?int $index = 4;
35
36    /**
37     * PDF page object parent index
38     * @var ?int
39     */
40    protected ?int $parent = 2;
41
42    /**
43     * PDF page object width
44     * @var int
45     */
46    protected int $width = 612;
47
48    /**
49     * PDF page object height
50     * @var int
51     */
52    protected int $height = 792;
53
54    /**
55     * PDF page object current content object index
56     * @var ?int
57     */
58    protected ?int $currentContentIndex = null;
59
60    /**
61     * PDF page object annotation object indices
62     * @var array
63     */
64    protected array $annots = [];
65
66    /**
67     * PDF page object content object indices
68     * @var array
69     */
70    protected array $content = [];
71
72    /**
73     * PDF page object XObject references
74     * @var array
75     */
76    protected array $xObjects = [];
77
78    /**
79     * PDF page object font object references
80     * @var array
81     */
82    protected array $fonts = [];
83
84    /**
85     * Additional page-level dictionary entries beyond what this class
86     * templates (e.g. /Rotate, /CropBox, /Group), pre-rendered as literal
87     * PDF syntax
88     * @var string
89     */
90    protected string $pageExtra = '';
91
92    /**
93     * Additional /Resources sub-dictionary entries beyond /ProcSet,
94     * /XObject, /Font (e.g. /ColorSpace, /ExtGState, /Shading, /Pattern),
95     * pre-rendered as literal PDF syntax
96     * @var string
97     */
98    protected string $otherResources = '';
99
100    /**
101     * Constructor
102     *
103     * Instantiate a PDF page object, defaults to letter size.
104     *
105     * @param  mixed $width
106     * @param  mixed $height
107     * @param  int   $index
108     */
109    public function __construct(mixed $width = 612, mixed$height = 792, int $index = 4)
110    {
111        $this->setWidth($width);
112        $this->setHeight($height);
113        $this->setIndex($index);
114        $this->setData("[{page_index}] 0 obj\n<</Type/Page/Parent [{parent}] 0 R[{annotations}]/MediaBox[0 0 " .
115            "[{width}] [{height}]][{content_objects}][{page_extra}]/Resources" .
116            "<</ProcSet[/PDF/Text/ImageB/ImageC/ImageI][{xobjects}][{fonts}][{other_resources}]>>>>\nendobj\n");
117    }
118
119    /**
120     * Parse a page object from a string
121     *
122     * @param  string $stream
123     * @return PageObject
124     */
125    public static function parse(string $stream): PageObject
126    {
127        $page = new self();
128        $page->setIndex((int)substr($stream, 0, strpos($stream, ' ')));
129
130        // Determine the page parent object index.
131        $parent = substr($stream, (strpos($stream, '/Parent') + 7));
132        $parent = trim(substr($parent, 0, strpos($parent, '0 R')));
133        $page->setParentIndex((int)$parent);
134
135        // Determine the page width and height.
136        $dims = substr($stream, (strpos($stream, '/MediaBox') + 9));
137        $dims = substr($dims, 0, strpos($dims, ']'));
138        $dims = trim(str_replace('[', '', $dims));
139        $dims = explode(' ', $dims);
140        $page->setWidth($dims[2]);
141        $page->setHeight($dims[3]);
142
143        // Determine the page content objects.
144        if (str_contains($stream, '/Contents')) {
145            $contents = substr($stream, (strpos($stream, '/Contents') + 9));
146            $contents = $page->getDictionaryReferences($contents);
147            foreach ($contents as $content) {
148                $page->addContentIndex((int)$content);
149            }
150
151            // Set placeholder
152            $contents = substr($stream, (strpos($stream, '/Contents') + 9));
153            if (str_contains($contents, '[')) {
154                $contents = substr($contents, 0, (strpos($contents, ']') + 1));
155                $stream   = str_replace($contents, '[{content_objects}]', $stream);
156            } else {
157                $contents = (str_contains($contents, '/')) ?
158                    substr($contents, 0, strpos($contents, '/')) :
159                    substr($contents, 0, strpos($contents, '>'));
160                $stream   = str_replace($contents, '[{content_objects}]', $stream);
161            }
162        }
163
164        // If they exist, determine the page annotation objects.
165        if (str_contains($stream, '/Annots')) {
166            $annots = substr($stream, (strpos($stream, '/Annots') + 7));
167            $annots = $page->getDictionaryReferences($annots);
168            foreach ($annots as $annot) {
169                $page->addAnnotIndex((int)$annot);
170            }
171
172            // Set placeholder
173            $annots = substr($stream, (strpos($stream, '/Annots') + 7));
174            if (str_contains($annots, '[')) {
175                $annots = substr($annots, 0, (strpos($annots, ']') + 1));
176                $stream = str_replace($annots, '[{annotations}]', $stream);
177            } else {
178                $annots = (str_contains($annots, '/')) ?
179                    substr($annots, 0, strpos($annots, '/')) :
180                    substr($annots, 0, strpos($annots, '>'));
181                $stream = str_replace($annots, '[{annotations}]', $stream);
182            }
183        }
184
185        // If they exist, determine the page font references.
186        if (str_contains($stream, '/Font')) {
187            $fonts  = substr($stream, strpos($stream, 'Font'));
188            $fonts  = substr($fonts, 0, (strpos($fonts, '>>') + 2));
189            $stream = str_replace('/' . $fonts, '[{fonts}]', $stream);
190            $fonts  = str_replace('Font<<', '', $fonts);
191            $fonts  = str_replace('>>', '', $fonts);
192            $fonts  = explode('/', $fonts);
193            foreach ($fonts as $value) {
194                if ($value != '') {
195                    $page->addFontReference('/' . $value);
196                }
197            }
198        }
199
200        // If they exist, determine the page XObjects references.
201        if (str_contains($stream, '/XObject')) {
202            $xo     = substr($stream, strpos($stream, 'XObject'));
203            $xo     = substr($xo, 0, (strpos($xo, '>>') + 2));
204            $stream = str_replace('/' . $xo, '[{xobjects}]', $stream);
205            $xo     = str_replace('XObject<<', '', $xo);
206            $xo     = str_replace('>>', '', $xo);
207            $xo     = explode('/', $xo);
208            foreach ($xo as $value) {
209                if ($value != '') {
210                    $page->addXObjectReference('/' . $value);
211                }
212            }
213        }
214
215        // If they exist, determine the page graphic states.
216        if (str_contains($stream, '/ExtGState')) {
217            $gState = substr($stream, strpos($stream, 'ExtGState'));
218            $gState = '/' . substr($gState, 0, (strpos($gState, '>>') + 2));
219        } else {
220            $gState = '';
221        }
222
223        // If any groups exist
224        if (str_contains($stream, '/Group')) {
225            $group = substr($stream, strpos($stream, 'Group'));
226            $group = '/' . substr($group, 0, (strpos($group, '>>') + 2));
227        } else {
228            $group = '';
229        }
230
231        // If resources exists
232        if (str_contains($stream, '/Resources')) {
233            $resources = substr($stream, strpos($stream, 'Resources'));
234            if (str_contains($resources, ' R')) {
235                $resources = '/' . substr($resources, 0, (strpos($resources, ' R') + 2));
236            } else if (str_contains($resources, '>>')) {
237                $resources = '/' . substr($resources, 0, (strpos($resources, '>>') + 2));
238            } else {
239                $resources = "/Resources<</ProcSet[/PDF/Text/ImageB/ImageC/ImageI][{xobjects}][{fonts}]{$gState}>>";
240            }
241        } else {
242            $resources = "/Resources<</ProcSet[/PDF/Text/ImageB/ImageC/ImageI][{xobjects}][{fonts}]{$gState}>>";
243        }
244
245        if (substr_count($resources, '<<') > substr_count($resources, '>>')) {
246            $resources .= str_repeat('>>', (substr_count($resources, '<<') - substr_count($resources, '>>')));
247        }
248
249        $page->setData("[{page_index}] 0 obj\n<</Type/Page/Parent [{parent}] 0 R[{annotations}]/MediaBox" .
250            "[0 0 [{width}] [{height}]]{$group}[{content_objects}]{$resources}>>\nendobj\n");
251
252        return $page;
253    }
254
255    /**
256     * Set the page object parent index
257     *
258     * @param  int $parent
259     * @return PageObject
260     */
261    public function setParentIndex(int $parent): PageObject
262    {
263        $this->parent = $parent;
264        return $this;
265    }
266
267    /**
268     * Set the page object width
269     *
270     * @param  mixed $width
271     * @return PageObject
272     */
273    public function setWidth(mixed $width): PageObject
274    {
275        $this->width = (int)$width;
276        return $this;
277    }
278
279    /**
280     * Set the page object height
281     *
282     * @param  mixed $height
283     * @return PageObject
284     */
285    public function setHeight(mixed $height): PageObject
286    {
287        $this->height = (int)$height;
288        return $this;
289    }
290
291    /**
292     * Set the page object annotation indices
293     *
294     * @param  array $annots
295     * @return PageObject
296     */
297    public function setAnnots(array $annots): PageObject
298    {
299        $this->annots = $annots;
300        return $this;
301    }
302
303    /**
304     * Set the page object content object indices
305     *
306     * @param  array $content
307     * @return PageObject
308     */
309    public function setContent(array $content): PageObject
310    {
311        $this->content = $content;
312        return $this;
313    }
314
315    /**
316     * Set the page object XObject references
317     *
318     * @param  array $xObjects
319     * @return PageObject
320     */
321    public function setXObjects(array $xObjects): PageObject
322    {
323        $this->xObjects = [];
324        foreach ($xObjects as $xObject) {
325            $this->addXObjectReference($xObject);
326        }
327        return $this;
328    }
329
330    /**
331     * Set the page object font references
332     *
333     * @param  array $fonts
334     * @return PageObject
335     */
336    public function setFonts(array $fonts): PageObject
337    {
338        $this->fonts = [];
339        foreach ($fonts as $font) {
340            $this->addFontReference($font);
341        }
342        return $this;
343    }
344
345    /**
346     * Set the page object current content object index
347     *
348     * @param  mixed $i
349     * @return PageObject
350     */
351    public function setCurrentContentIndex(mixed $i = null): PageObject
352    {
353        $this->currentContentIndex = ($i !== null) ? (int)$i : null;
354        return $this;
355    }
356
357    /**
358     * Set additional page-level dictionary entries (pre-rendered PDF syntax)
359     *
360     * @param  string $pageExtra
361     * @return PageObject
362     */
363    public function setPageExtra(string $pageExtra): PageObject
364    {
365        $this->pageExtra = $pageExtra;
366        return $this;
367    }
368
369    /**
370     * Set additional /Resources sub-dictionary entries (pre-rendered PDF syntax)
371     *
372     * @param  string $otherResources
373     * @return PageObject
374     */
375    public function setOtherResources(string $otherResources): PageObject
376    {
377        $this->otherResources = $otherResources;
378        return $this;
379    }
380
381    /**
382     * Add annotation index
383     *
384     * @param  int $i
385     * @return PageObject
386     */
387    public function addAnnotIndex(int $i): PageObject
388    {
389        $this->annots[] = $i;
390        return $this;
391    }
392
393    /**
394     * Add content object index
395     *
396     * @param  int $i
397     * @return PageObject
398     */
399    public function addContentIndex(int $i): PageObject
400    {
401        $this->content[] = $i;
402        $this->setCurrentContentIndex($i);
403        return $this;
404    }
405
406    /**
407     * Add XObject reference
408     *
409     * @param  string $xObject
410     * @return PageObject
411     */
412    public function addXObjectReference(string $xObject): PageObject
413    {
414        $i = substr($xObject, (strpos($xObject, ' ') + 1));
415        $i = substr($i, 0, strpos($i, ' '));
416        $this->xObjects[(int)$i] = $xObject;
417        return $this;
418    }
419
420    /**
421     * Add font reference
422     *
423     * @param  string $font
424     * @return PageObject
425     */
426    public function addFontReference(string $font): PageObject
427    {
428        $i = substr($font, (strpos($font, ' ') + 1));
429        $i = substr($i, 0, strpos($i, ' '));
430        $this->fonts[(int)$i] = $font;
431        return $this;
432    }
433
434    /**
435     * Get the page object parent index
436     *
437     * @return ?int
438     */
439    public function getParentIndex(): ?int
440    {
441        return $this->parent;
442    }
443
444    /**
445     * Get the page object width
446     *
447     * @return mixed
448     */
449    public function getWidth(): mixed
450    {
451        return $this->width;
452    }
453
454    /**
455     * Get the page object height
456     *
457     * @return mixed
458     */
459    public function getHeight(): mixed
460    {
461        return $this->height;
462    }
463
464    /**
465     * Get the page object current content object index
466     *
467     * @return ?int
468     */
469    public function getCurrentContentIndex(): ?int
470    {
471        return $this->currentContentIndex;
472    }
473
474    /**
475     * Get the page object annotation indices
476     *
477     * @return array
478     */
479    public function getAnnots(): array
480    {
481        return $this->annots;
482    }
483
484    /**
485     * Get the page object content object indices
486     *
487     * @return array
488     */
489    public function getContent(): array
490    {
491        return $this->content;
492    }
493
494    /**
495     * Get the page object XObject references
496     *
497     * @return array
498     */
499    public function getXObjects(): array
500    {
501        return $this->xObjects;
502    }
503
504    /**
505     * Get the page object font references
506     *
507     * @return array
508     */
509    public function getFonts(): array
510    {
511        return $this->fonts;
512    }
513
514    /**
515     * Determine if the page object has an annotation index
516     *
517     * @param  int $i
518     * @return bool
519     */
520    public function hasAnnot(int $i): bool
521    {
522        return (isset($this->annots[$i]));
523    }
524
525    /**
526     * Determine if the page object has a content index
527     *
528     * @param  int $i
529     * @return bool
530     */
531    public function hasContent(int $i): bool
532    {
533        return (isset($this->content[$i]));
534    }
535
536    /**
537     * Method to print the page object.
538     *
539     * @return string
540     */
541    public function __toString(): string
542    {
543        $annots   = '';
544        $xObjects = '';
545        $fonts    = '';
546
547        // Format the annotations.
548        if (count($this->annots) > 0) {
549            $annots = '/Annots[';
550            $annots .= implode(" 0 R ", $this->annots);
551            $annots .= " 0 R]";
552        }
553
554        // Format the xobjects.
555        if (count($this->xObjects) > 0) {
556            $xObjects = '/XObject<<';
557            $xObjects .= implode('', $this->xObjects);
558            $xObjects .= '>>';
559        }
560
561        // Format the fonts.
562        if (count($this->fonts) > 0) {
563            $fonts = '/Font<<';
564            $fonts .= implode('', $this->fonts);
565            $fonts .= '>>';
566        }
567
568        // Swap out the placeholders.
569        $obj = str_replace(
570            ['[{page_index}]', '[{parent}]', '[{width}]', '[{height}]', '[{page_extra}]', '[{other_resources}]'],
571            [(string)$this->index, (string)$this->parent, (string)$this->width, (string)$this->height, $this->pageExtra, $this->otherResources],
572            $this->data
573        );
574
575        $obj = (($annots != '') && (!str_contains($obj, '[{annotations}]'))) ?
576            str_replace('/MediaBox', $annots . '/MediaBox', $obj) :
577            str_replace('[{annotations}]', $annots, $obj);
578
579        $obj = (($xObjects != '') && (!str_contains($obj, '[{xobjects}]'))) ?
580            str_replace('/ProcSet', $xObjects . '/ProcSet', $obj) :
581            str_replace('[{xobjects}]', $xObjects, $obj);
582
583        $obj = (($fonts != '') && (!str_contains($obj, '[{fonts}]'))) ?
584            str_replace('/ProcSet', $fonts . '/ProcSet', $obj) :
585            str_replace('[{fonts}]', $fonts, $obj);
586
587        if (count($this->content) > 0) {
588            $obj = str_replace('[{content_objects}]', '/Contents[' . implode(" 0 R ", $this->content) . " 0 R]", $obj);
589        } else {
590            $obj = str_replace('[{content_objects}]', '', $obj);
591        }
592
593        return $obj;
594    }
595
596}