Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.85% covered (success)
78.85%
179 / 227
50.00% covered (warning)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Compiler
78.85% covered (success)
78.85%
179 / 227
50.00% covered (warning)
50.00%
5 / 10
135.52
0.00% covered (danger)
0.00%
0 / 1
 setDocument
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
13
 finalize
97.96% covered (success)
97.96%
48 / 49
0.00% covered (danger)
0.00%
0 / 1
21
 prepareFonts
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
6
 prepareImages
73.08% covered (success)
73.08%
19 / 26
0.00% covered (danger)
0.00%
0 / 1
5.49
 preparePaths
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 prepareText
31.71% covered (danger)
31.71%
13 / 41
0.00% covered (danger)
0.00%
0 / 1
57.87
 prepareTextStreams
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 prepareAnnotations
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 prepareFields
85.71% covered (success)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
7.14
 prepareForms
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Pdf\Build;
15
16use Pop\Pdf\Document;
17use Pop\Pdf\Document\Page\Text;
18
19/**
20 * Pdf compiler class
21 *
22 * @category   Pop
23 * @package    Pop\Pdf
24 * @author     Nick Sagona, III <dev@noladev.com>
25 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    5.2.7
28 */
29class Compiler extends AbstractCompiler
30{
31
32    /**
33     * Set the document object
34     *
35     * @param  Document\AbstractDocument $document
36     * @return Compiler
37     */
38    public function setDocument(Document\AbstractDocument $document): Compiler
39    {
40        $this->document = $document;
41
42        foreach ($this->document->getPages() as $key => $page) {
43            if (!in_array($page, $this->pages, true)) {
44                $this->pages[$key] = $page;
45            }
46        }
47
48        foreach ($this->document->getFonts() as $key => $font) {
49            if (!in_array($font, $this->fonts, true)) {
50                $this->fonts[$key] = $font;
51            }
52        }
53
54        $this->compression = $this->document->isCompressed();
55
56        if ($this->document->hasImportedObjects()) {
57            foreach ($this->document->getImportObjects() as $i => $object) {
58                if ($object instanceof PdfObject\RootObject) {
59                    $this->setRoot($object);
60                } else if ($object instanceof PdfObject\ParentObject) {
61                    $this->setParent($object);
62                } else if ($object instanceof PdfObject\InfoObject) {
63                    $this->setInfo($object);
64                } else {
65                    $this->objects[$i] = $object;
66                }
67            }
68        }
69
70        if ($this->root === null) {
71            $this->setRoot(new PdfObject\RootObject());
72        }
73        if ($this->parent === null) {
74            $this->setParent(new PdfObject\ParentObject());
75        }
76        if ($this->info === null) {
77            $this->setInfo(new PdfObject\InfoObject());
78        }
79
80        $this->root->setVersion($this->document->getVersion());
81        $this->info->setMetadata($this->document->getMetadata());
82
83        return $this;
84    }
85
86    /**
87     * Compile and finalize the PDF document
88     *
89     * @param  ?Document\AbstractDocument $document
90     * @throws Exception
91     * @return void
92     */
93    public function finalize(?Document\AbstractDocument $document = null): void
94    {
95        if ($document !== null) {
96            $this->setDocument($document);
97        }
98        $this->prepareFonts();
99
100        $pageObjects = [];
101
102        foreach ($this->pages as $page) {
103            if ($page->hasImportedPageObject()) {
104                $pageObject = $page->getImportedPageObject();
105                $pageObject->setCurrentContentIndex(null);
106                $this->objects[$pageObject->getIndex()] = $pageObject;
107            } else {
108                $page->setIndex($this->lastIndex() + 1);
109                $pageObject = new PdfObject\PageObject($page->getWidth(), $page->getHeight(), $page->getIndex());
110                $pageObject->setParentIndex($this->parent->getIndex());
111                $this->objects[$pageObject->getIndex()] = $pageObject;
112                $this->parent->addKid($pageObject->getIndex());
113            }
114
115            foreach ($this->fontReferences as $fontReference) {
116                $pageObject->addFontReference($fontReference);
117            }
118
119            // Prepare image objects
120            if ($page->hasImages()) {
121                $this->prepareImages($page->getImages(), $pageObject);
122            }
123            // Prepare path objects
124            if ($page->hasPaths()) {
125                $this->preparePaths($page->getPaths(), $pageObject);
126            }
127            // Prepare text objects
128            if ($page->hasText()) {
129                $this->prepareText($page->getText(), $pageObject);
130            }
131            // Prepare text objects
132            if ($page->hasTextStreams()) {
133                $this->prepareTextStreams($page->getTextStreams(), $pageObject);
134            }
135            // Prepare field objects
136            if ($page->hasFields()) {
137                $this->prepareFields($page->getFields(), $pageObject);
138            }
139
140            $pageObjects[$pageObject->getIndex()] = $pageObject;
141        }
142
143        // Prepare annotation objects, after the pages have been set
144        foreach ($this->pages as $page) {
145            if ($page->hasAnnotations()) {
146                $this->prepareAnnotations($page->getAnnotations(), $pageObjects[$page->getIndex()]);
147            }
148        }
149
150        // If the document has forms
151        if ($document->hasForms()) {
152            $this->prepareForms();
153        }
154
155        $numObjs       = count($this->objects) + 1;
156        $this->trailer = "xref\n0 {$numObjs}\n0000000000 65535 f \n";
157
158        // Intial Length is the length of the version string
159        $this->byteLength = 9;
160        $this->trailer    .= $this->formatByteLength($this->byteLength) . " 00000 n \n";
161
162        // New Length is the distance to the second object
163        $this->byteLength = $this->calculateByteLength($this->root);
164
165        $this->output .= $this->root;
166
167        // Loop through the rest of the objects, calculate their size and length
168        // for the xref table and add their data to the output.
169        foreach ($this->objects as $object) {
170            if ($object->getIndex() != $this->root->getIndex()) {
171                if (($object instanceof PdfObject\StreamObject) && ($this->compression) && (!$object->isPalette()) &&
172                    (!$object->isEncoded() && !$object->isImported() && (stripos((string)$object->getDefinition(), '/length') === false))) {
173                    $object->encode();
174                }
175                $this->trailer    .= $this->formatByteLength($this->byteLength) . " 00000 n \n";
176                $this->output     .= $object;
177                $this->byteLength += $this->calculateByteLength($object);
178            }
179        }
180
181        // Finalize the trailer.
182        $this->trailer .= "trailer\n<</Size {$numObjs}/Root " . $this->root->getIndex() . " 0 R/Info " .
183            $this->info->getIndex() . " 0 R>>\nstartxref\n" . ($this->byteLength) . "\n%%EOF";
184
185        // Append the trailer to the final output.
186        $this->output .= $this->trailer;
187    }
188
189    /**
190     * Prepare the font objects
191     *
192     * @throws Exception|Font\Exception
193     * @return void
194     */
195    public function prepareFonts(): void
196    {
197        foreach ($this->fonts as $font) {
198            if ($font instanceof \Pop\Pdf\Document\Font) {
199                $f = count($this->fontReferences) + 1;
200                $i = $this->lastIndex() + 1;
201
202                if ($font->isStandard()) {
203                    $this->fontReferences[$font->getName()] = '/MF' . $f . ' ' . $i . ' 0 R';
204                    $this->objects[$i] = PdfObject\StreamObject::parse(
205                        "{$i} 0 obj\n<<\n    /Type /Font\n    /Subtype /Type1\n    /Name /MF{$f}\n    /BaseFont /" .
206                        $font->getName() . "\n    /Encoding /WinAnsiEncoding\n>>\nendobj\n\n"
207                    );
208                } else {
209                    $font->parser()
210                        ->setCompression($this->compression)
211                        ->setFontIndex($f)
212                        ->setFontObjectIndex($i)
213                        ->setFontDescIndex($i + 1)
214                        ->setFontFileIndex($i + 2);
215
216                    $font->parser()->parse();
217
218                    $this->fontReferences[$font->parser()->getFontName()] = $font->parser()->getFontReference();
219                    foreach ($font->parser()->getObjects() as $fontObject) {
220                        $this->objects[$fontObject->getIndex()] = $fontObject;
221                    }
222                }
223            } else if (is_array($font)) {
224                $this->fontReferences[$font['name']] = $font['ref'];
225            }
226        }
227    }
228
229    /**
230     * Prepare the image objects
231     *
232     * @param  array $images
233     * @param  PdfObject\PageObject $pageObject
234     * @return void
235     */
236    protected function prepareImages(array $images, PdfObject\PageObject $pageObject): void
237    {
238        $imgs = [];
239
240        $contentObject = new PdfObject\StreamObject($this->lastIndex() + 1);
241        $this->objects[$contentObject->getIndex()] = $contentObject;
242        $pageObject->addContentIndex($contentObject->getIndex());
243
244        foreach ($images as $key => $image) {
245            $coordinates = $this->getCoordinates($image['x'], $image['y'], $pageObject);
246            if (!array_key_exists($key, $imgs)) {
247                $i = $this->lastIndex() + 1;
248                if ($image['image']->isStream()) {
249                    $imageParser = Image\Parser::createImageFromStream(
250                        $image['image']->getStream(), $coordinates['x'], $coordinates['y'],
251                        $image['image']->getResizeDimensions(), $image['image']->isPreserveResolution()
252                    );
253                } else {
254                    $imageParser = Image\Parser::createImageFromFile(
255                        $image['image']->getImage(), $coordinates['x'], $coordinates['y'],
256                        $image['image']->getResizeDimensions(), $image['image']->isPreserveResolution()
257                    );
258                }
259
260                $imageParser->setIndex($i);
261                $contentObject->appendStream($imageParser->getStream());
262                $pageObject->addXObjectReference($imageParser->getXObject());
263                foreach ($imageParser->getObjects() as $oi => $imageObject) {
264                    $this->objects[$oi] = $imageObject;
265                }
266                $imgs[$key] = $imageParser;
267            } else {
268                $imgs[$key]->setX($coordinates['x']);
269                $imgs[$key]->setY($coordinates['y']);
270                $contentObject->appendStream($imgs[$key]->getStream());
271            }
272        }
273    }
274
275    /**
276     * Prepare the path objects
277     *
278     * @param  array $paths
279     * @param  PdfObject\PageObject $pageObject
280     * @return void
281     */
282    protected function preparePaths(array $paths, PdfObject\PageObject $pageObject): void
283    {
284        $contentObject = new PdfObject\StreamObject($this->lastIndex() + 1);
285        $this->objects[$contentObject->getIndex()] = $contentObject;
286        $pageObject->addContentIndex($contentObject->getIndex());
287
288        foreach ($paths as $path) {
289            $stream  = null;
290            $streams = $path->getStreams();
291            foreach ($streams as $str) {
292                $s = $str['stream'];
293                if (isset($str['points'])) {
294                    foreach ($str['points'] as $points) {
295                        $keys = array_keys($points);
296                        $coordinates = $this->getCoordinates($points[$keys[0]], $points[$keys[1]], $pageObject);
297                        $s = str_replace(
298                            ['[{' . $keys[0] . '}]', '[{' . $keys[1] . '}]'], [$coordinates['x'], $coordinates['y']], $s
299                        );
300                    }
301                }
302                $stream .= $s;
303            }
304
305            $contentObject->appendStream($stream);
306        }
307    }
308
309    /**
310     * Prepare the text objects
311     *
312     * @param  array $text
313     * @param  PdfObject\PageObject $pageObject
314     * @throws Exception
315     * @return void
316     */
317    protected function prepareText(array $text, PdfObject\PageObject $pageObject): void
318    {
319        $contentObject = new PdfObject\StreamObject($this->lastIndex() + 1);
320        $this->objects[$contentObject->getIndex()] = $contentObject;
321        $pageObject->addContentIndex($contentObject->getIndex());
322
323        foreach ($text as $txt) {
324            if ($this->document->hasStyle($txt['font'])) {
325                $style = $this->document->getStyle($txt['font']);
326                if ($style->hasSize()) {
327                    $txt['text']->setSize($style->getSize());
328                }
329                if ($style->hasFont()) {
330                    $txt['font'] = $style->getFont();
331                }
332            }
333            if (!isset($this->fontReferences[$txt['font']])) {
334                throw new Exception('Error: The font \'' . $txt['font'] . '\' has not been added to the document.');
335            }
336            $coordinates = $this->getCoordinates($txt['x'], $txt['y'], $pageObject);
337
338            // Auto-wrap text by character length
339            if ($txt['text']->hasCharWrap()) {
340                $font    = $this->fontReferences[$txt['font']];
341                $stream  = $txt['text']->startStream($font, $coordinates['x'], $coordinates['y']);
342                $stream .= $txt['text']->getPartialStream($font);
343                $stream .= $txt['text']->endStream();
344                $contentObject->appendStream($stream);
345            // Left/right/center align text
346            } else if ($txt['text']->hasAlignment()) {
347                $fontObject = $this->fonts[$txt['font']];
348                $strings    = $txt['text']->getAlignment()->getStrings($txt['text'], $fontObject, $coordinates['y']);
349                foreach ($strings as $string) {
350                    $textString = new Text($string['string'], $txt['text']->getSize());
351                    $contentObject->appendStream(
352                        $textString->getStream($this->fontReferences[$txt['font']], $string['x'], $string['y'])
353                    );
354                }
355            // Left/right wrap text around box boundary
356            } else if ($txt['text']->hasWrap()) {
357                $fontObject = $this->fonts[$txt['font']];
358                $strings    = $txt['text']->getWrap()->getStrings($txt['text'], $fontObject, $coordinates['y']);
359                $stream     = $txt['text']->getColorStream();
360                if (!empty($stream)) {
361                    $contentObject->appendStream($stream);
362                }
363                foreach ($strings as $string) {
364                    $textString = new Text($string['string'], $txt['text']->getSize());
365                    $contentObject->appendStream(
366                        $textString->getStream($this->fontReferences[$txt['font']], $string['x'], $string['y'])
367                    );
368                }
369            // Else, just append the text stream
370            } else {
371                $contentObject->appendStream(
372                    $txt['text']->getStream($this->fontReferences[$txt['font']], $coordinates['x'], $coordinates['y'])
373                );
374            }
375        }
376    }
377
378    /**
379     * Prepare the text streams objects
380     *
381     * @param  array $textStreams
382     * @param  PdfObject\PageObject $pageObject
383     * @throws Exception
384     * @return void
385     */
386    protected function prepareTextStreams(array $textStreams, PdfObject\PageObject $pageObject): void
387    {
388        $contentObject = new PdfObject\StreamObject($this->lastIndex() + 1);
389        $this->objects[$contentObject->getIndex()] = $contentObject;
390        $pageObject->addContentIndex($contentObject->getIndex());
391
392        $curY = null;
393
394        foreach ($textStreams as $txt) {
395            if (($curY !== null) && ($txt->getStartY() > $curY)) {
396                $txt->setStartY($curY - (($txt->getTextStreams()[0]['y'] ?? 12) * 2));
397            }
398            $stream = $txt->getStream($this->fonts, $this->fontReferences);
399            $curY   = $txt->getCurrentY();
400            $contentObject->appendStream($stream);
401        }
402    }
403
404    /**
405     * Prepare the annotation objects
406     *
407     * @param  array $annotations
408     * @param  PdfObject\PageObject $pageObject
409     * @return void
410     */
411    protected function prepareAnnotations(array $annotations, PdfObject\PageObject $pageObject): void
412    {
413        foreach ($annotations as $annotation) {
414            $i = $this->lastIndex() + 1;
415            $pageObject->addAnnotIndex($i);
416
417            $coordinates = $this->getCoordinates($annotation['x'], $annotation['y'], $pageObject);
418            if ($annotation['annotation'] instanceof \Pop\Pdf\Document\Page\Annotation\Url) {
419                $stream = $annotation['annotation']->getStream($i, $coordinates['x'], $coordinates['y']);
420            } else {
421                $targetCoordinates = $this->getCoordinates(
422                    $annotation['annotation']->getXTarget(), $annotation['annotation']->getYTarget(), $pageObject
423                );
424
425                $annotation['annotation']->setXTarget($targetCoordinates['x']);
426                $annotation['annotation']->setYTarget($targetCoordinates['y']);
427                $stream = $annotation['annotation']->getStream(
428                    $i, $coordinates['x'], $coordinates['y'], $pageObject->getIndex(), $this->parent->getKids()
429                );
430            }
431            $this->objects[$i] = PdfObject\StreamObject::parse($stream);
432        }
433    }
434
435    /**
436     * Prepare the field objects
437     *
438     * @param  array $fields
439     * @param  PdfObject\PageObject $pageObject
440     * @throws Exception
441     * @return void
442     */
443    protected function prepareFields(array $fields, PdfObject\PageObject $pageObject): void
444    {
445        foreach ($fields as $field) {
446            if ($this->document->getForm($field['form']) !== null) {
447                if (($field['field']->getFont() !== null) && (!isset($this->fontReferences[$field['field']->getFont()]))) {
448                    throw new Exception('Error: The font \'' . $field['field']->getFont() . '\' has not been added to the document.');
449                } else if (($field['field']->getFont() !== null) && (isset($this->fontReferences[$field['field']->getFont()]))) {
450                    $fontRef = $this->fontReferences[$field['field']->getFont()];
451                } else {
452                    $fontRef = null;
453                }
454                $i = $this->lastIndex() + 1;
455                $pageObject->addAnnotIndex($i);
456                $coordinates = $this->getCoordinates($field['x'], $field['y'], $pageObject);
457                $this->document->getForm($field['form'])->addFieldIndex($i);
458                $this->objects[$i] = PdfObject\StreamObject::parse(
459                    $field['field']->getStream($i, $pageObject->getIndex(), $fontRef, $coordinates['x'], $coordinates['y'])
460                );
461            }
462        }
463    }
464
465    /**
466     * Prepare the form objects
467     *
468     * @return void
469     */
470    protected function prepareForms(): void
471    {
472        $formRefs = '';
473        foreach ($this->document->getForms() as $form) {
474            $i = $this->lastIndex() + 1;
475            $this->objects[$i] = PdfObject\StreamObject::parse($form->getStream($i));
476            $formRefs .= $i . ' 0 R ';
477        }
478        $formRefs = substr($formRefs, 0, -1);
479        $this->root->setFormReferences($formRefs);
480    }
481
482}