Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.93% covered (success)
82.93%
170 / 205
50.00% covered (warning)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Compiler
82.93% covered (success)
82.93%
170 / 205
50.00% covered (warning)
50.00%
5 / 10
99.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.92% covered (success)
97.92%
47 / 48
0.00% covered (danger)
0.00%
0 / 1
21
 prepareFonts
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
6
 prepareImages
75.00% covered (success)
75.00%
18 / 24
0.00% covered (danger)
0.00%
0 / 1
5.39
 preparePaths
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 prepareText
34.38% covered (danger)
34.38%
11 / 32
0.00% covered (danger)
0.00%
0 / 1
31.89
 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%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 prepareFields
84.62% covered (success)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
7.18
 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-2023 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\Build\Image;
17use Pop\Pdf\Build\PdfObject;
18use Pop\Pdf\Document;
19use Pop\Pdf\Document\Page\Text;
20
21/**
22 * Pdf compiler class
23 *
24 * @category   Pop
25 * @package    Pop\Pdf
26 * @author     Nick Sagona, III <dev@nolainteractive.com>
27 * @copyright  Copyright (c) 2009-2023 NOLA Interactive, LLC. (http://www.nolainteractive.com)
28 * @license    http://www.popphp.org/license     New BSD License
29 * @version    4.2.0
30 */
31class Compiler extends AbstractCompiler
32{
33
34    /**
35     * Set the document object
36     *
37     * @param  Document\AbstractDocument $document
38     * @return Compiler
39     */
40    public function setDocument(Document\AbstractDocument $document)
41    {
42        $this->document = $document;
43
44        foreach ($this->document->getPages() as $key => $page) {
45            if (!in_array($page, $this->pages, true)) {
46                $this->pages[$key] = $page;
47            }
48        }
49
50        foreach ($this->document->getFonts() as $key => $font) {
51            if (!in_array($font, $this->fonts, true)) {
52                $this->fonts[$key] = $font;
53            }
54        }
55
56        $this->compression = $this->document->isCompressed();
57
58        if ($this->document->hasImportedObjects()) {
59            foreach ($this->document->getImportObjects() as $i => $object) {
60                if ($object instanceof PdfObject\RootObject) {
61                    $this->setRoot($object);
62                } else if ($object instanceof PdfObject\ParentObject) {
63                    $this->setParent($object);
64                } else if ($object instanceof PdfObject\InfoObject) {
65                    $this->setInfo($object);
66                } else {
67                    $this->objects[$i] = $object;
68                }
69            }
70        }
71
72        if (null === $this->root) {
73            $this->setRoot(new PdfObject\RootObject());
74        }
75        if (null === $this->parent) {
76            $this->setParent(new PdfObject\ParentObject());
77        }
78        if (null === $this->info) {
79            $this->setInfo(new PdfObject\InfoObject());
80        }
81
82        $this->root->setVersion($this->document->getVersion());
83        $this->info->setMetadata($this->document->getMetadata());
84
85        return $this;
86    }
87
88    /**
89     * Compile and finalize the PDF document
90     *
91     * @param  Document\AbstractDocument $document
92     * @return void
93     */
94    public function finalize(Document\AbstractDocument $document = null)
95    {
96        if (null !== $document) {
97            $this->setDocument($document);
98        }
99        $this->prepareFonts();
100
101        $pageObjects = [];
102
103        foreach ($this->pages as $page) {
104            if ($page->hasImportedPageObject()) {
105                $pageObject = $page->getImportedPageObject();
106                $pageObject->setCurrentContentIndex(null);
107                $this->objects[$pageObject->getIndex()] = $pageObject;
108            } else {
109                $page->setIndex($this->lastIndex() + 1);
110                $pageObject = new PdfObject\PageObject($page->getWidth(), $page->getHeight(), $page->getIndex());
111                $pageObject->setParentIndex($this->parent->getIndex());
112                $this->objects[$pageObject->getIndex()] = $pageObject;
113                $this->parent->addKid($pageObject->getIndex());
114            }
115
116            foreach ($this->fontReferences as $fontReference) {
117                $pageObject->addFontReference($fontReference);
118            }
119
120            // Prepare image objects
121            if ($page->hasImages()) {
122                $this->prepareImages($page->getImages(), $pageObject);
123            }
124            // Prepare path objects
125            if ($page->hasPaths()) {
126                $this->preparePaths($page->getPaths(), $pageObject);
127            }
128            // Prepare text objects
129            if ($page->hasText()) {
130                $this->prepareText($page->getText(), $pageObject);
131            }
132            // Prepare text objects
133            if ($page->hasTextStreams()) {
134                $this->prepareTextStreams($page->getTextStreams(), $pageObject);
135            }
136            // Prepare field objects
137            if ($page->hasFields()) {
138                $this->prepareFields($page->getFields(), $pageObject);
139            }
140
141            $pageObjects[$pageObject->getIndex()] = $pageObject;
142        }
143
144        // Prepare annotation objects, after the pages have been set
145        foreach ($this->pages as $page) {
146            if ($page->hasAnnotations()) {
147                $this->prepareAnnotations($page->getAnnotations(), $pageObjects[$page->getIndex()]);
148            }
149        }
150
151        // If the document has forms
152        if ($document->hasForms()) {
153            $this->prepareForms();
154        }
155
156        $numObjs       = count($this->objects) + 1;
157        $this->trailer = "xref\n0 {$numObjs}\n0000000000 65535 f \n";
158
159        $this->byteLength += $this->calculateByteLength($this->root);
160        $this->trailer    .= $this->formatByteLength($this->byteLength) . " 00000 n \n";
161
162        $this->output .= $this->root;
163
164        // Loop through the rest of the objects, calculate their size and length
165        // for the xref table and add their data to the output.
166        foreach ($this->objects as $object) {
167            if ($object->getIndex() != $this->root->getIndex()) {
168                if (($object instanceof PdfObject\StreamObject) && ($this->compression) && (!$object->isPalette()) &&
169                    (!$object->isEncoded() && !$object->isImported() && (stripos((string)$object->getDefinition(), '/length') === false))) {
170                    $object->encode();
171                }
172                $this->byteLength += $this->calculateByteLength($object);
173                $this->trailer    .= $this->formatByteLength($this->byteLength) . " 00000 n \n";
174                $this->output     .= $object;
175            }
176        }
177
178        // Finalize the trailer.
179        $this->trailer .= "trailer\n<</Size {$numObjs}/Root " . $this->root->getIndex() . " 0 R/Info " .
180            $this->info->getIndex() . " 0 R>>\nstartxref\n" . ($this->byteLength + 68) . "\n%%EOF";
181
182        // Append the trailer to the final output.
183        $this->output .= $this->trailer;
184    }
185
186    /**
187     * Prepare the font objects
188     *
189     * @return void
190     */
191    public function prepareFonts()
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)
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)
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)
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 (!isset($this->fontReferences[$txt['font']])) {
321                throw new Exception('Error: The font \'' . $txt['font'] . '\' has not been added to the document.');
322            }
323            $coordinates = $this->getCoordinates($txt['x'], $txt['y'], $pageObject);
324
325            // Auto-wrap text by character length
326            if ($txt['text']->hasCharWrap()) {
327                $font    = $this->fontReferences[$txt['font']];
328                $stream  = $txt['text']->startStream($font, $coordinates['x'], $coordinates['y']);
329                $stream .= $txt['text']->getPartialStream($font);
330                $stream .= $txt['text']->endStream();
331                $contentObject->appendStream($stream);
332            // Left/right/center align text
333            } else if ($txt['text']->hasAlignment()) {
334                $fontObject = $this->fonts[$txt['font']];
335                $strings    = $txt['text']->getAlignment()->getStrings($txt['text'], $fontObject, $coordinates['y']);
336                foreach ($strings as $string) {
337                    $textString = new Text($string['string'], $txt['text']->getSize());
338                    $contentObject->appendStream(
339                        $textString->getStream($this->fontReferences[$txt['font']], $string['x'], $string['y'])
340                    );
341                }
342            // Left/right wrap text around box boundary
343            } else if ($txt['text']->hasWrap()) {
344                $fontObject = $this->fonts[$txt['font']];
345                $strings    = $txt['text']->getWrap()->getStrings($txt['text'], $fontObject, $coordinates['y']);
346                $stream     = $txt['text']->getColorStream();
347                if (!empty($stream)) {
348                    $contentObject->appendStream($stream);
349                }
350                foreach ($strings as $string) {
351                    $textString = new Text($string['string'], $txt['text']->getSize());
352                    $contentObject->appendStream(
353                        $textString->getStream($this->fontReferences[$txt['font']], $string['x'], $string['y'])
354                    );
355                }
356            // Else, just append the text stream
357            } else {
358                $contentObject->appendStream(
359                    $txt['text']->getStream($this->fontReferences[$txt['font']], $coordinates['x'], $coordinates['y'])
360                );
361            }
362        }
363    }
364
365    /**
366     * Prepare the text streams objects
367     *
368     * @param  array $textStreams
369     * @param  PdfObject\PageObject $pageObject
370     * @throws Exception
371     * @return void
372     */
373    protected function prepareTextStreams(array $textStreams, PdfObject\PageObject $pageObject)
374    {
375        $contentObject = new PdfObject\StreamObject($this->lastIndex() + 1);
376        $this->objects[$contentObject->getIndex()] = $contentObject;
377        $pageObject->addContentIndex($contentObject->getIndex());
378
379        foreach ($textStreams as $txt) {
380            $contentObject->appendStream($txt->getStream($this->fonts, $this->fontReferences));
381        }
382    }
383
384    /**
385     * Prepare the annotation objects
386     *
387     * @param  array $annotations
388     * @param  PdfObject\PageObject $pageObject
389     * @return void
390     */
391    protected function prepareAnnotations(array $annotations, PdfObject\PageObject $pageObject)
392    {
393        foreach ($annotations as $annotation) {
394            $i = $this->lastIndex() + 1;
395            $pageObject->addAnnotIndex($i);
396
397            $coordinates = $this->getCoordinates($annotation['x'], $annotation['y'], $pageObject);
398            if ($annotation['annotation'] instanceof \Pop\Pdf\Document\Page\Annotation\Url) {
399                $stream = $annotation['annotation']->getStream($i, $coordinates['x'], $coordinates['y']);
400            } else {
401                $targetCoordinates = $this->getCoordinates(
402                    $annotation['annotation']->getXTarget(), $annotation['annotation']->getYTarget(), $pageObject
403                );
404
405                $annotation['annotation']->setXTarget($targetCoordinates['x']);
406                $annotation['annotation']->setYTarget($targetCoordinates['y']);
407                $stream = $annotation['annotation']->getStream(
408                    $i, $coordinates['x'], $coordinates['y'], $pageObject->getIndex(), $this->parent->getKids()
409                );
410            }
411            $this->objects[$i] = PdfObject\StreamObject::parse($stream);
412        }
413    }
414
415    /**
416     * Prepare the field objects
417     *
418     * @param  array $fields
419     * @param  PdfObject\PageObject $pageObject
420     * @throws Exception
421     * @return void
422     */
423    protected function prepareFields(array $fields, PdfObject\PageObject $pageObject)
424    {
425        foreach ($fields as $field) {
426            if (null !== $this->document->getForm($field['form'])) {
427                if ((null !== $field['field']->getFont()) && (!isset($this->fontReferences[$field['field']->getFont()]))) {
428                    throw new Exception('Error: The font \'' . $field['field']->getFont() . '\' has not been added to the document.');
429                } else if ((null !== $field['field']->getFont()) && (isset($this->fontReferences[$field['field']->getFont()]))) {
430                    $fontRef = $this->fontReferences[$field['field']->getFont()];
431                } else {
432                    $fontRef = null;
433                }
434                $i = $this->lastIndex() + 1;
435                $pageObject->addAnnotIndex($i);
436                $coordinates = $this->getCoordinates($field['x'], $field['y'], $pageObject);
437                $this->document->getForm($field['form'])->addFieldIndex($i);
438                $this->objects[$i] = PdfObject\StreamObject::parse(
439                    $field['field']->getStream($i, $pageObject->getIndex(), $fontRef, $coordinates['x'], $coordinates['y'])
440                );
441            }
442        }
443    }
444
445    /**
446     * Prepare the form objects
447     *
448     * @return void
449     */
450    protected function prepareForms()
451    {
452        $formRefs = '';
453        foreach ($this->document->getForms() as $form) {
454            $i = $this->lastIndex() + 1;
455            $this->objects[$i] = PdfObject\StreamObject::parse($form->getStream($i));
456            $formRefs .= $i . ' 0 R ';
457        }
458        $formRefs = substr($formRefs, 0, -1);
459        $this->root->setFormReferences($formRefs);
460    }
461
462}