Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
82.93% |
170 / 205 |
|
50.00% |
5 / 10 |
CRAP | |
0.00% |
0 / 1 |
Compiler | |
82.93% |
170 / 205 |
|
50.00% |
5 / 10 |
99.52 | |
0.00% |
0 / 1 |
setDocument | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
13 | |||
finalize | |
97.92% |
47 / 48 |
|
0.00% |
0 / 1 |
21 | |||
prepareFonts | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
6 | |||
prepareImages | |
75.00% |
18 / 24 |
|
0.00% |
0 / 1 |
5.39 | |||
preparePaths | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
5 | |||
prepareText | |
34.38% |
11 / 32 |
|
0.00% |
0 / 1 |
31.89 | |||
prepareTextStreams | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
prepareAnnotations | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
prepareFields | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
7.18 | |||
prepareForms | |
100.00% |
7 / 7 |
|
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 | */ |
14 | namespace Pop\Pdf\Build; |
15 | |
16 | use Pop\Pdf\Build\Image; |
17 | use Pop\Pdf\Build\PdfObject; |
18 | use Pop\Pdf\Document; |
19 | use 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 | */ |
31 | class 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 | } |