Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.54% covered (success)
80.54%
178 / 221
50.00% covered (warning)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Compiler
80.54% covered (success)
80.54%
178 / 221
50.00% covered (warning)
50.00%
5 / 10
118.55
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.92% covered (success)
97.92%
47 / 48
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 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 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 (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://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@nolainteractive.com>
25 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
26 * @license    http://www.popphp.org/license     New BSD License
27 * @version    5.0.0
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        $this->byteLength += $this->calculateByteLength($this->root);
159        $this->trailer    .= $this->formatByteLength($this->byteLength) . " 00000 n \n";
160
161        $this->output .= $this->root;
162
163        // Loop through the rest of the objects, calculate their size and length
164        // for the xref table and add their data to the output.
165        foreach ($this->objects as $object) {
166            if ($object->getIndex() != $this->root->getIndex()) {
167                if (($object instanceof PdfObject\StreamObject) && ($this->compression) && (!$object->isPalette()) &&
168                    (!$object->isEncoded() && !$object->isImported() && (stripos((string)$object->getDefinition(), '/length') === false))) {
169                    $object->encode();
170                }
171                $this->byteLength += $this->calculateByteLength($object);
172                $this->trailer    .= $this->formatByteLength($this->byteLength) . " 00000 n \n";
173                $this->output     .= $object;
174            }
175        }
176
177        // Finalize the trailer.
178        $this->trailer .= "trailer\n<</Size {$numObjs}/Root " . $this->root->getIndex() . " 0 R/Info " .
179            $this->info->getIndex() . " 0 R>>\nstartxref\n" . ($this->byteLength + 68) . "\n%%EOF";
180
181        // Append the trailer to the final output.
182        $this->output .= $this->trailer;
183    }
184
185    /**
186     * Prepare the font objects
187     *
188     * @throws Exception|Font\Exception
189     * @return void
190     */
191    public function prepareFonts(): void
192    {
193        foreach ($this->fonts as $font) {
194            if ($font instanceof \Pop\Pdf\Document\Font) {
195                $f = count($this->fontReferences) + 1;
196                $i = $this->lastIndex() + 1;
197
198                if ($font->isStandard()) {
199                    $this->fontReferences[$font->getName()] = '/MF' . $f . ' ' . $i . ' 0 R';
200                    $this->objects[$i] = PdfObject\StreamObject::parse(
201                        "{$i} 0 obj\n<<\n    /Type /Font\n    /Subtype /Type1\n    /Name /MF{$f}\n    /BaseFont /" .
202                        $font->getName() . "\n    /Encoding /WinAnsiEncoding\n>>\nendobj\n\n"
203                    );
204                } else {
205                    $font->parser()
206                        ->setCompression($this->compression)
207                        ->setFontIndex($f)
208                        ->setFontObjectIndex($i)
209                        ->setFontDescIndex($i + 1)
210                        ->setFontFileIndex($i + 2);
211
212                    $font->parser()->parse();
213
214                    $this->fontReferences[$font->parser()->getFontName()] = $font->parser()->getFontReference();
215                    foreach ($font->parser()->getObjects() as $fontObject) {
216                        $this->objects[$fontObject->getIndex()] = $fontObject;
217                    }
218                }
219            } else if (is_array($font)) {
220                $this->fontReferences[$font['name']] = $font['ref'];
221            }
222        }
223    }
224
225    /**
226     * Prepare the image objects
227     *
228     * @param  array $images
229     * @param  PdfObject\PageObject $pageObject
230     * @return void
231     */
232    protected function prepareImages(array $images, PdfObject\PageObject $pageObject): void
233    {
234        $imgs = [];
235
236        $contentObject = new PdfObject\StreamObject($this->lastIndex() + 1);
237        $this->objects[$contentObject->getIndex()] = $contentObject;
238        $pageObject->addContentIndex($contentObject->getIndex());
239
240        foreach ($images as $key => $image) {
241            $coordinates = $this->getCoordinates($image['x'], $image['y'], $pageObject);
242            if (!array_key_exists($key, $imgs)) {
243                $i = $this->lastIndex() + 1;
244                if ($image['image']->isStream()) {
245                    $imageParser = Image\Parser::createImageFromStream(
246                        $image['image']->getStream(), $coordinates['x'], $coordinates['y'],
247                        $image['image']->getResizeDimensions(), $image['image']->isPreserveResolution()
248                    );
249                } else {
250                    $imageParser = Image\Parser::createImageFromFile(
251                        $image['image']->getImage(), $coordinates['x'], $coordinates['y'],
252                        $image['image']->getResizeDimensions(), $image['image']->isPreserveResolution()
253                    );
254                }
255
256                $imageParser->setIndex($i);
257                $contentObject->appendStream($imageParser->getStream());
258                $pageObject->addXObjectReference($imageParser->getXObject());
259                foreach ($imageParser->getObjects() as $oi => $imageObject) {
260                    $this->objects[$oi] = $imageObject;
261                }
262                $imgs[$key] = $imageParser;
263            } else {
264                $imgs[$key]->setX($coordinates['x']);
265                $imgs[$key]->setY($coordinates['y']);
266                $contentObject->appendStream($imgs[$key]->getStream());
267            }
268        }
269    }
270
271    /**
272     * Prepare the path objects
273     *
274     * @param  array $paths
275     * @param  PdfObject\PageObject $pageObject
276     * @return void
277     */
278    protected function preparePaths(array $paths, PdfObject\PageObject $pageObject): void
279    {
280        $contentObject = new PdfObject\StreamObject($this->lastIndex() + 1);
281        $this->objects[$contentObject->getIndex()] = $contentObject;
282        $pageObject->addContentIndex($contentObject->getIndex());
283
284        foreach ($paths as $path) {
285            $stream  = null;
286            $streams = $path->getStreams();
287            foreach ($streams as $str) {
288                $s = $str['stream'];
289                if (isset($str['points'])) {
290                    foreach ($str['points'] as $points) {
291                        $keys = array_keys($points);
292                        $coordinates = $this->getCoordinates($points[$keys[0]], $points[$keys[1]], $pageObject);
293                        $s = str_replace(
294                            ['[{' . $keys[0] . '}]', '[{' . $keys[1] . '}]'], [$coordinates['x'], $coordinates['y']], $s
295                        );
296                    }
297                }
298                $stream .= $s;
299            }
300
301            $contentObject->appendStream($stream);
302        }
303    }
304
305    /**
306     * Prepare the text objects
307     *
308     * @param  array $text
309     * @param  PdfObject\PageObject $pageObject
310     * @throws Exception
311     * @return void
312     */
313    protected function prepareText(array $text, PdfObject\PageObject $pageObject): void
314    {
315        $contentObject = new PdfObject\StreamObject($this->lastIndex() + 1);
316        $this->objects[$contentObject->getIndex()] = $contentObject;
317        $pageObject->addContentIndex($contentObject->getIndex());
318
319        foreach ($text as $txt) {
320            if ($this->document->hasStyle($txt['font'])) {
321                $style = $this->document->getStyle($txt['font']);
322                if ($style->hasSize()) {
323                    $txt['text']->setSize($style->getSize());
324                }
325                if ($style->hasFont()) {
326                    $txt['font'] = $style->getFont();
327                }
328            }
329            if (!isset($this->fontReferences[$txt['font']])) {
330                throw new Exception('Error: The font \'' . $txt['font'] . '\' has not been added to the document.');
331            }
332            $coordinates = $this->getCoordinates($txt['x'], $txt['y'], $pageObject);
333
334            // Auto-wrap text by character length
335            if ($txt['text']->hasCharWrap()) {
336                $font    = $this->fontReferences[$txt['font']];
337                $stream  = $txt['text']->startStream($font, $coordinates['x'], $coordinates['y']);
338                $stream .= $txt['text']->getPartialStream($font);
339                $stream .= $txt['text']->endStream();
340                $contentObject->appendStream($stream);
341            // Left/right/center align text
342            } else if ($txt['text']->hasAlignment()) {
343                $fontObject = $this->fonts[$txt['font']];
344                $strings    = $txt['text']->getAlignment()->getStrings($txt['text'], $fontObject, $coordinates['y']);
345                foreach ($strings as $string) {
346                    $textString = new Text($string['string'], $txt['text']->getSize());
347                    $contentObject->appendStream(
348                        $textString->getStream($this->fontReferences[$txt['font']], $string['x'], $string['y'])
349                    );
350                }
351            // Left/right wrap text around box boundary
352            } else if ($txt['text']->hasWrap()) {
353                $fontObject = $this->fonts[$txt['font']];
354                $strings    = $txt['text']->getWrap()->getStrings($txt['text'], $fontObject, $coordinates['y']);
355                $stream     = $txt['text']->getColorStream();
356                if (!empty($stream)) {
357                    $contentObject->appendStream($stream);
358                }
359                foreach ($strings as $string) {
360                    $textString = new Text($string['string'], $txt['text']->getSize());
361                    $contentObject->appendStream(
362                        $textString->getStream($this->fontReferences[$txt['font']], $string['x'], $string['y'])
363                    );
364                }
365            // Else, just append the text stream
366            } else {
367                $contentObject->appendStream(
368                    $txt['text']->getStream($this->fontReferences[$txt['font']], $coordinates['x'], $coordinates['y'])
369                );
370            }
371        }
372    }
373
374    /**
375     * Prepare the text streams objects
376     *
377     * @param  array $textStreams
378     * @param  PdfObject\PageObject $pageObject
379     * @throws Exception
380     * @return void
381     */
382    protected function prepareTextStreams(array $textStreams, PdfObject\PageObject $pageObject): void
383    {
384        $contentObject = new PdfObject\StreamObject($this->lastIndex() + 1);
385        $this->objects[$contentObject->getIndex()] = $contentObject;
386        $pageObject->addContentIndex($contentObject->getIndex());
387
388        foreach ($textStreams as $txt) {
389            $contentObject->appendStream($txt->getStream($this->fonts, $this->fontReferences));
390        }
391    }
392
393    /**
394     * Prepare the annotation objects
395     *
396     * @param  array $annotations
397     * @param  PdfObject\PageObject $pageObject
398     * @return void
399     */
400    protected function prepareAnnotations(array $annotations, PdfObject\PageObject $pageObject): void
401    {
402        foreach ($annotations as $annotation) {
403            $i = $this->lastIndex() + 1;
404            $pageObject->addAnnotIndex($i);
405
406            $coordinates = $this->getCoordinates($annotation['x'], $annotation['y'], $pageObject);
407            if ($annotation['annotation'] instanceof \Pop\Pdf\Document\Page\Annotation\Url) {
408                $stream = $annotation['annotation']->getStream($i, $coordinates['x'], $coordinates['y']);
409            } else {
410                $targetCoordinates = $this->getCoordinates(
411                    $annotation['annotation']->getXTarget(), $annotation['annotation']->getYTarget(), $pageObject
412                );
413
414                $annotation['annotation']->setXTarget($targetCoordinates['x']);
415                $annotation['annotation']->setYTarget($targetCoordinates['y']);
416                $stream = $annotation['annotation']->getStream(
417                    $i, $coordinates['x'], $coordinates['y'], $pageObject->getIndex(), $this->parent->getKids()
418                );
419            }
420            $this->objects[$i] = PdfObject\StreamObject::parse($stream);
421        }
422    }
423
424    /**
425     * Prepare the field objects
426     *
427     * @param  array $fields
428     * @param  PdfObject\PageObject $pageObject
429     * @throws Exception
430     * @return void
431     */
432    protected function prepareFields(array $fields, PdfObject\PageObject $pageObject): void
433    {
434        foreach ($fields as $field) {
435            if ($this->document->getForm($field['form']) !== null) {
436                if (($field['field']->getFont() !== null) && (!isset($this->fontReferences[$field['field']->getFont()]))) {
437                    throw new Exception('Error: The font \'' . $field['field']->getFont() . '\' has not been added to the document.');
438                } else if (($field['field']->getFont() !== null) && (isset($this->fontReferences[$field['field']->getFont()]))) {
439                    $fontRef = $this->fontReferences[$field['field']->getFont()];
440                } else {
441                    $fontRef = null;
442                }
443                $i = $this->lastIndex() + 1;
444                $pageObject->addAnnotIndex($i);
445                $coordinates = $this->getCoordinates($field['x'], $field['y'], $pageObject);
446                $this->document->getForm($field['form'])->addFieldIndex($i);
447                $this->objects[$i] = PdfObject\StreamObject::parse(
448                    $field['field']->getStream($i, $pageObject->getIndex(), $fontRef, $coordinates['x'], $coordinates['y'])
449                );
450            }
451        }
452    }
453
454    /**
455     * Prepare the form objects
456     *
457     * @return void
458     */
459    protected function prepareForms(): void
460    {
461        $formRefs = '';
462        foreach ($this->document->getForms() as $form) {
463            $i = $this->lastIndex() + 1;
464            $this->objects[$i] = PdfObject\StreamObject::parse($form->getStream($i));
465            $formRefs .= $i . ' 0 R ';
466        }
467        $formRefs = substr($formRefs, 0, -1);
468        $this->root->setFormReferences($formRefs);
469    }
470
471}