Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.54% |
178 / 221 |
|
50.00% |
5 / 10 |
CRAP | |
0.00% |
0 / 1 |
Compiler | |
80.54% |
178 / 221 |
|
50.00% |
5 / 10 |
118.55 | |
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% |
22 / 22 |
|
100.00% |
1 / 1 |
6 | |||
prepareImages | |
73.08% |
19 / 26 |
|
0.00% |
0 / 1 |
5.49 | |||
preparePaths | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
prepareText | |
31.71% |
13 / 41 |
|
0.00% |
0 / 1 |
57.87 | |||
prepareTextStreams | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
prepareAnnotations | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
3 | |||
prepareFields | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
7.14 | |||
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-2024 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\Document; |
17 | use 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 | */ |
29 | class 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 | } |