PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.2
2.4.3 2.4.4 2.4.2 2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 All 60 releases
easy-invoice / vendor / dompdf / php-svg-lib / src / Svg / Surface / CPdf.php

CPdf.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.2, at vendor/dompdf/php-svg-lib/src/Svg/Surface/CPdf.php

6,424 lines 213.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A PHP class to provide the basic functionality to create a pdf document without
4 * any requirement for additional modules.
5 *
6 * @author Wayne Munro
7 * @license Public Domain http://creativecommons.org/licenses/publicdomain/
8 * @package Cpdf
9 */
10
11 namespace Svg\Surface;
12
13 class CPdf
14 {
15 const PDF_VERSION = '1.7';
16
17 const ACROFORM_SIG_SIGNATURESEXISTS = 0x0001;
18 const ACROFORM_SIG_APPENDONLY = 0x0002;
19
20 const ACROFORM_FIELD_BUTTON = 'Btn';
21 const ACROFORM_FIELD_TEXT = 'Tx';
22 const ACROFORM_FIELD_CHOICE = 'Ch';
23 const ACROFORM_FIELD_SIG = 'Sig';
24
25 const ACROFORM_FIELD_READONLY = 0x0001;
26 const ACROFORM_FIELD_REQUIRED = 0x0002;
27
28 const ACROFORM_FIELD_TEXT_MULTILINE = 0x1000;
29 const ACROFORM_FIELD_TEXT_PASSWORD = 0x2000;
30 const ACROFORM_FIELD_TEXT_RICHTEXT = 0x10000;
31
32 const ACROFORM_FIELD_CHOICE_COMBO = 0x20000;
33 const ACROFORM_FIELD_CHOICE_EDIT = 0x40000;
34 const ACROFORM_FIELD_CHOICE_SORT = 0x80000;
35 const ACROFORM_FIELD_CHOICE_MULTISELECT = 0x200000;
36
37 const XOBJECT_SUBTYPE_FORM = 'Form';
38
39 /**
40 * @var integer The current number of pdf objects in the document
41 */
42 public $numObj = 0;
43
44 /**
45 * @var array This array contains all of the pdf objects, ready for final assembly
46 */
47 public $objects = [];
48
49 /**
50 * @var integer The objectId (number within the objects array) of the document catalog
51 */
52 public $catalogId;
53
54 /**
55 * @var integer The objectId (number within the objects array) of indirect references (Javascript EmbeddedFiles)
56 */
57 protected $indirectReferenceId = 0;
58
59 /**
60 * @var integer The objectId (number within the objects array)
61 */
62 protected $embeddedFilesId = 0;
63
64 /**
65 * AcroForm objectId
66 *
67 * @var integer
68 */
69 public $acroFormId;
70
71 /**
72 * @var int
73 */
74 public $signatureMaxLen = 5000;
75
76 /**
77 * @var array Array carrying information about the fonts that the system currently knows about
78 * Used to ensure that a font is not loaded twice, among other things
79 */
80 public $fonts = [];
81
82 /**
83 * @var string The default font metrics file to use if no other font has been loaded.
84 * The path to the directory containing the font metrics should be included
85 */
86 public $defaultFont = './fonts/Helvetica.afm';
87
88 /**
89 * @string A record of the current font
90 */
91 public $currentFont = '';
92
93 /**
94 * @var string The current base font
95 */
96 public $currentBaseFont = '';
97
98 /**
99 * @var integer The number of the current font within the font array
100 */
101 public $currentFontNum = 0;
102
103 /**
104 * @var integer
105 */
106 public $currentNode;
107
108 /**
109 * @var integer Object number of the current page
110 */
111 public $currentPage;
112
113 /**
114 * @var integer Object number of the currently active contents block
115 */
116 public $currentContents;
117
118 /**
119 * @var integer Number of fonts within the system
120 */
121 public $numFonts = 0;
122
123 /**
124 * @var integer Number of graphic state resources used
125 */
126 private $numStates = 0;
127
128 /**
129 * @var array Number of graphic state resources used
130 */
131 private $gstates = [];
132
133 /**
134 * @var array Current color for fill operations, defaults to inactive value,
135 * all three components should be between 0 and 1 inclusive when active
136 */
137 public $currentColor = null;
138
139 /**
140 * @var array Current color for stroke operations (lines etc.)
141 */
142 public $currentStrokeColor = null;
143
144 /**
145 * @var string Fill rule (nonzero or evenodd)
146 */
147 public $fillRule = "nonzero";
148
149 /**
150 * @var string Current style that lines are drawn in
151 */
152 public $currentLineStyle = '';
153
154 /**
155 * @var array Current line transparency (partial graphics state)
156 */
157 public $currentLineTransparency = ["mode" => "Normal", "opacity" => 1.0];
158
159 /**
160 * array Current fill transparency (partial graphics state)
161 */
162 public $currentFillTransparency = ["mode" => "Normal", "opacity" => 1.0];
163
164 /**
165 * @var array An array which is used to save the state of the document, mainly the colors and styles
166 * it is used to temporarily change to another state, then change back to what it was before
167 */
168 public $stateStack = [];
169
170 /**
171 * @var integer Number of elements within the state stack
172 */
173 public $nStateStack = 0;
174
175 /**
176 * @var integer Number of page objects within the document
177 */
178 public $numPages = 0;
179
180 /**
181 * @var array Object Id storage stack
182 */
183 public $stack = [];
184
185 /**
186 * @var integer Number of elements within the object Id storage stack
187 */
188 public $nStack = 0;
189
190 /**
191 * an array which contains information about the objects which are not firmly attached to pages
192 * these have been added with the addObject function
193 */
194 public $looseObjects = [];
195
196 /**
197 * array contains information about how the loose objects are to be added to the document
198 */
199 public $addLooseObjects = [];
200
201 /**
202 * @var integer The objectId of the information object for the document
203 * this contains authorship, title etc.
204 */
205 public $infoObject = 0;
206
207 /**
208 * @var integer Number of images being tracked within the document
209 */
210 public $numImages = 0;
211
212 /**
213 * @var array An array containing options about the document
214 * it defaults to turning on the compression of the objects
215 */
216 public $options = ['compression' => true];
217
218 /**
219 * @var integer The objectId of the first page of the document
220 */
221 public $firstPageId;
222
223 /**
224 * @var integer The object Id of the procset object
225 */
226 public $procsetObjectId;
227
228 /**
229 * @var array Store the information about the relationship between font families
230 * this used so that the code knows which font is the bold version of another font, etc.
231 * the value of this array is initialised in the constructor function.
232 */
233 public $fontFamilies = [];
234
235 /**
236 * @var string Folder for php serialized formats of font metrics files.
237 * If empty string, use same folder as original metrics files.
238 * This can be passed in from class creator.
239 * If this folder does not exist or is not writable, Cpdf will be **much** slower.
240 * Because of potential trouble with php safe mode, folder cannot be created at runtime.
241 */
242 public $fontcache = '';
243
244 /**
245 * @var integer The version of the font metrics cache file.
246 * This value must be manually incremented whenever the internal font data structure is modified.
247 */
248 public $fontcacheVersion = 6;
249
250 /**
251 * @var string Temporary folder.
252 * If empty string, will attempt system tmp folder.
253 * This can be passed in from class creator.
254 */
255 public $tmp = '';
256
257 /**
258 * @var string Track if the current font is bolded or italicised
259 */
260 public $currentTextState = '';
261
262 /**
263 * @var string Messages are stored here during processing, these can be selected afterwards to give some useful debug information
264 */
265 public $messages = '';
266
267 /**
268 * @var string The encryption array for the document encryption is stored here
269 */
270 public $arc4 = '';
271
272 /**
273 * @var integer The object Id of the encryption information
274 */
275 public $arc4_objnum = 0;
276
277 /**
278 * @var string The file identifier, used to uniquely identify a pdf document
279 */
280 public $fileIdentifier = '';
281
282 /**
283 * @var boolean A flag to say if a document is to be encrypted or not
284 */
285 public $encrypted = false;
286
287 /**
288 * @var string The encryption key for the encryption of all the document content (structure is not encrypted)
289 */
290 public $encryptionKey = '';
291
292 /**
293 * @var array Array which forms a stack to keep track of nested callback functions
294 */
295 public $callback = [];
296
297 /**
298 * @var integer The number of callback functions in the callback array
299 */
300 public $nCallback = 0;
301
302 /**
303 * @var array Store label->id pairs for named destinations, these will be used to replace internal links
304 * done this way so that destinations can be defined after the location that links to them
305 */
306 public $destinations = [];
307
308 /**
309 * @var array Store the stack for the transaction commands, each item in here is a record of the values of all the
310 * publiciables within the class, so that the user can rollback at will (from each 'start' command)
311 * note that this includes the objects array, so these can be large.
312 */
313 public $checkpoint = '';
314
315 /**
316 * @var array Table of Image origin filenames and image labels which were already added with o_image().
317 * Allows to merge identical images
318 */
319 public $imagelist = [];
320
321 /**
322 * @var array Table of already added alpha and plain image files for transparent PNG images.
323 */
324 protected $imageAlphaList = [];
325
326 /**
327 * @var array List of temporary image files to be deleted after processing.
328 */
329 protected $imageCache = [];
330
331 /**
332 * @var boolean Whether the text passed in should be treated as Unicode or just local character set.
333 */
334 public $isUnicode = false;
335
336 /**
337 * @var string the JavaScript code of the document
338 */
339 public $javascript = '';
340
341 /**
342 * @var boolean whether the compression is possible
343 */
344 protected $compressionReady = false;
345
346 /**
347 * @var array Current page size
348 */
349 protected $currentPageSize = ["width" => 0, "height" => 0];
350
351 /**
352 * @var array All the chars that will be required in the font subsets
353 */
354 protected $stringSubsets = [];
355
356 /**
357 * @var string The target internal encoding
358 */
359 protected static $targetEncoding = 'Windows-1252';
360
361 /**
362 * @var array
363 */
364 protected $byteRange = array();
365
366 /**
367 * @var array The list of the core fonts
368 */
369 protected static $coreFonts = [
370 'courier',
371 'courier-bold',
372 'courier-oblique',
373 'courier-boldoblique',
374 'helvetica',
375 'helvetica-bold',
376 'helvetica-oblique',
377 'helvetica-boldoblique',
378 'times-roman',
379 'times-bold',
380 'times-italic',
381 'times-bolditalic',
382 'symbol',
383 'zapfdingbats'
384 ];
385
386 /**
387 * Class constructor
388 * This will start a new document
389 *
390 * @param array $pageSize Array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero.
391 * @param boolean $isUnicode Whether text will be treated as Unicode or not.
392 * @param string $fontcache The font cache folder
393 * @param string $tmp The temporary folder
394 */
395 function __construct($pageSize = [0, 0, 612, 792], $isUnicode = false, $fontcache = '', $tmp = '')
396 {
397 $this->isUnicode = $isUnicode;
398 $this->fontcache = rtrim($fontcache, DIRECTORY_SEPARATOR."/\\");
399 $this->tmp = ($tmp !== '' ? $tmp : sys_get_temp_dir());
400 $this->newDocument($pageSize);
401
402 $this->compressionReady = function_exists('gzcompress');
403
404 if (in_array('Windows-1252', mb_list_encodings())) {
405 self::$targetEncoding = 'Windows-1252';
406 }
407
408 // also initialize the font families that are known about already
409 $this->setFontFamily('init');
410 }
411
412 public function __destruct()
413 {
414 foreach ($this->imageCache as $file) {
415 if (file_exists($file)) {
416 unlink($file);
417 }
418 }
419 }
420
421 /**
422 * Document object methods (internal use only)
423 *
424 * There is about one object method for each type of object in the pdf document
425 * Each function has the same call list ($id,$action,$options).
426 * $id = the object ID of the object, or what it is to be if it is being created
427 * $action = a string specifying the action to be performed, though ALL must support:
428 * 'new' - create the object with the id $id
429 * 'out' - produce the output for the pdf object
430 * $options = optional, a string or array containing the various parameters for the object
431 *
432 * These, in conjunction with the output function are the ONLY way for output to be produced
433 * within the pdf 'file'.
434 */
435
436 /**
437 * Destination object, used to specify the location for the user to jump to, presently on opening
438 *
439 * @param $id
440 * @param $action
441 * @param string $options
442 * @return string|null
443 */
444 protected function o_destination($id, $action, $options = '')
445 {
446 switch ($action) {
447 case 'new':
448 $this->objects[$id] = ['t' => 'destination', 'info' => []];
449 $tmp = '';
450 switch ($options['type']) {
451 case 'XYZ':
452 /** @noinspection PhpMissingBreakStatementInspection */
453 case 'FitR':
454 $tmp = ' ' . $options['p3'] . $tmp;
455 case 'FitH':
456 case 'FitV':
457 case 'FitBH':
458 /** @noinspection PhpMissingBreakStatementInspection */
459 case 'FitBV':
460 $tmp = ' ' . $options['p1'] . ' ' . $options['p2'] . $tmp;
461 case 'Fit':
462 case 'FitB':
463 $tmp = $options['type'] . $tmp;
464 $this->objects[$id]['info']['string'] = $tmp;
465 $this->objects[$id]['info']['page'] = $options['page'];
466 }
467 break;
468
469 case 'out':
470 $o = &$this->objects[$id];
471
472 $tmp = $o['info'];
473 $res = "\n$id 0 obj\n" . '[' . $tmp['page'] . ' 0 R /' . $tmp['string'] . "]\nendobj";
474
475 return $res;
476 }
477
478 return null;
479 }
480
481 /**
482 * set the viewer preferences
483 *
484 * @param $id
485 * @param $action
486 * @param string|array $options
487 * @return string|null
488 */
489 protected function o_viewerPreferences($id, $action, $options = '')
490 {
491 switch ($action) {
492 case 'new':
493 $this->objects[$id] = ['t' => 'viewerPreferences', 'info' => []];
494 break;
495
496 case 'add':
497 $o = &$this->objects[$id];
498
499 foreach ($options as $k => $v) {
500 switch ($k) {
501 // Boolean keys
502 case 'HideToolbar':
503 case 'HideMenubar':
504 case 'HideWindowUI':
505 case 'FitWindow':
506 case 'CenterWindow':
507 case 'DisplayDocTitle':
508 case 'PickTrayByPDFSize':
509 $o['info'][$k] = (bool)$v;
510 break;
511
512 // Integer keys
513 case 'NumCopies':
514 $o['info'][$k] = (int)$v;
515 break;
516
517 // Name keys
518 case 'ViewArea':
519 case 'ViewClip':
520 case 'PrintClip':
521 case 'PrintArea':
522 $o['info'][$k] = (string)$v;
523 break;
524
525 // Named with limited valid values
526 case 'NonFullScreenPageMode':
527 if (!in_array($v, ['UseNone', 'UseOutlines', 'UseThumbs', 'UseOC'])) {
528 break;
529 }
530 $o['info'][$k] = $v;
531 break;
532
533 case 'Direction':
534 if (!in_array($v, ['L2R', 'R2L'])) {
535 break;
536 }
537 $o['info'][$k] = $v;
538 break;
539
540 case 'PrintScaling':
541 if (!in_array($v, ['None', 'AppDefault'])) {
542 break;
543 }
544 $o['info'][$k] = $v;
545 break;
546
547 case 'Duplex':
548 if (!in_array($v, ['None', 'Simplex', 'DuplexFlipShortEdge', 'DuplexFlipLongEdge'])) {
549 break;
550 }
551 $o['info'][$k] = $v;
552 break;
553
554 // Integer array
555 case 'PrintPageRange':
556 // Cast to integer array
557 foreach ($v as $vK => $vV) {
558 $v[$vK] = (int)$vV;
559 }
560 $o['info'][$k] = array_values($v);
561 break;
562 }
563 }
564 break;
565
566 case 'out':
567 $o = &$this->objects[$id];
568 $res = "\n$id 0 obj\n<< ";
569
570 foreach ($o['info'] as $k => $v) {
571 if (is_string($v)) {
572 $v = '/' . $v;
573 } elseif (is_int($v)) {
574 $v = (string) $v;
575 } elseif (is_bool($v)) {
576 $v = ($v ? 'true' : 'false');
577 } elseif (is_array($v)) {
578 $v = '[' . implode(' ', $v) . ']';
579 }
580 $res .= "\n/$k $v";
581 }
582 $res .= "\n>>\nendobj";
583
584 return $res;
585 }
586
587 return null;
588 }
589
590 /**
591 * define the document catalog, the overall controller for the document
592 *
593 * @param $id
594 * @param $action
595 * @param string|array $options
596 * @return string|null
597 */
598 protected function o_catalog($id, $action, $options = '')
599 {
600 if ($action !== 'new') {
601 $o = &$this->objects[$id];
602 }
603
604 switch ($action) {
605 case 'new':
606 $this->objects[$id] = ['t' => 'catalog', 'info' => []];
607 $this->catalogId = $id;
608 break;
609
610 case 'acroform':
611 case 'outlines':
612 case 'pages':
613 case 'openHere':
614 case 'names':
615 $o['info'][$action] = $options;
616 break;
617
618 case 'viewerPreferences':
619 if (!isset($o['info']['viewerPreferences'])) {
620 $this->numObj++;
621 $this->o_viewerPreferences($this->numObj, 'new');
622 $o['info']['viewerPreferences'] = $this->numObj;
623 }
624
625 $vp = $o['info']['viewerPreferences'];
626 $this->o_viewerPreferences($vp, 'add', $options);
627
628 break;
629
630 case 'out':
631 $res = "\n$id 0 obj\n<< /Type /Catalog";
632
633 foreach ($o['info'] as $k => $v) {
634 switch ($k) {
635 case 'outlines':
636 $res .= "\n/Outlines $v 0 R";
637 break;
638
639 case 'pages':
640 $res .= "\n/Pages $v 0 R";
641 break;
642
643 case 'viewerPreferences':
644 $res .= "\n/ViewerPreferences $v 0 R";
645 break;
646
647 case 'openHere':
648 $res .= "\n/OpenAction $v 0 R";
649 break;
650
651 case 'names':
652 $res .= "\n/Names $v 0 R";
653 break;
654
655 case 'acroform':
656 $res .= "\n/AcroForm $v 0 R";
657 break;
658 }
659 }
660
661 $res .= " >>\nendobj";
662
663 return $res;
664 }
665
666 return null;
667 }
668
669 /**
670 * object which is a parent to the pages in the document
671 *
672 * @param $id
673 * @param $action
674 * @param string $options
675 * @return string|null
676 */
677 protected function o_pages($id, $action, $options = '')
678 {
679 if ($action !== 'new') {
680 $o = &$this->objects[$id];
681 }
682
683 switch ($action) {
684 case 'new':
685 $this->objects[$id] = ['t' => 'pages', 'info' => []];
686 $this->o_catalog($this->catalogId, 'pages', $id);
687 break;
688
689 case 'page':
690 if (!is_array($options)) {
691 // then it will just be the id of the new page
692 $o['info']['pages'][] = $options;
693 } else {
694 // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative
695 // and pos is either 'before' or 'after', saying where this page will fit.
696 if (isset($options['id']) && isset($options['rid']) && isset($options['pos'])) {
697 $i = array_search($options['rid'], $o['info']['pages']);
698 if (isset($o['info']['pages'][$i]) && $o['info']['pages'][$i] == $options['rid']) {
699
700 // then there is a match
701 // make a space
702 switch ($options['pos']) {
703 case 'before':
704 $k = $i;
705 break;
706
707 case 'after':
708 $k = $i + 1;
709 break;
710
711 default:
712 $k = -1;
713 break;
714 }
715
716 if ($k >= 0) {
717 for ($j = count($o['info']['pages']) - 1; $j >= $k; $j--) {
718 $o['info']['pages'][$j + 1] = $o['info']['pages'][$j];
719 }
720
721 $o['info']['pages'][$k] = $options['id'];
722 }
723 }
724 }
725 }
726 break;
727
728 case 'procset':
729 $o['info']['procset'] = $options;
730 break;
731
732 case 'mediaBox':
733 $o['info']['mediaBox'] = $options;
734 // which should be an array of 4 numbers
735 $this->currentPageSize = ['width' => $options[2], 'height' => $options[3]];
736 break;
737
738 case 'font':
739 $o['info']['fonts'][] = ['objNum' => $options['objNum'], 'fontNum' => $options['fontNum']];
740 break;
741
742 case 'extGState':
743 $o['info']['extGStates'][] = ['objNum' => $options['objNum'], 'stateNum' => $options['stateNum']];
744 break;
745
746 case 'xObject':
747 $o['info']['xObjects'][] = ['objNum' => $options['objNum'], 'label' => $options['label']];
748 break;
749
750 case 'out':
751 if (count($o['info']['pages'])) {
752 $res = "\n$id 0 obj\n<< /Type /Pages\n/Kids [";
753 foreach ($o['info']['pages'] as $v) {
754 $res .= "$v 0 R\n";
755 }
756
757 $res .= "]\n/Count " . count($this->objects[$id]['info']['pages']);
758
759 if ((isset($o['info']['fonts']) && count($o['info']['fonts'])) ||
760 isset($o['info']['procset']) ||
761 (isset($o['info']['extGStates']) && count($o['info']['extGStates']))
762 ) {
763 $res .= "\n/Resources <<";
764
765 if (isset($o['info']['procset'])) {
766 $res .= "\n/ProcSet " . $o['info']['procset'] . " 0 R";
767 }
768
769 if (isset($o['info']['fonts']) && count($o['info']['fonts'])) {
770 $res .= "\n/Font << ";
771 foreach ($o['info']['fonts'] as $finfo) {
772 $res .= "\n/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R";
773 }
774 $res .= "\n>>";
775 }
776
777 if (isset($o['info']['xObjects']) && count($o['info']['xObjects'])) {
778 $res .= "\n/XObject << ";
779 foreach ($o['info']['xObjects'] as $finfo) {
780 $res .= "\n/" . $finfo['label'] . " " . $finfo['objNum'] . " 0 R";
781 }
782 $res .= "\n>>";
783 }
784
785 if (isset($o['info']['extGStates']) && count($o['info']['extGStates'])) {
786 $res .= "\n/ExtGState << ";
787 foreach ($o['info']['extGStates'] as $gstate) {
788 $res .= "\n/GS" . $gstate['stateNum'] . " " . $gstate['objNum'] . " 0 R";
789 }
790 $res .= "\n>>";
791 }
792
793 $res .= "\n>>";
794 if (isset($o['info']['mediaBox'])) {
795 $tmp = $o['info']['mediaBox'];
796 $res .= "\n/MediaBox [" . sprintf(
797 '%.3F %.3F %.3F %.3F',
798 $tmp[0],
799 $tmp[1],
800 $tmp[2],
801 $tmp[3]
802 ) . ']';
803 }
804 }
805
806 $res .= "\n >>\nendobj";
807 } else {
808 $res = "\n$id 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj";
809 }
810
811 return $res;
812 }
813
814 return null;
815 }
816
817 /**
818 * define the outlines in the doc, empty for now
819 *
820 * @param $id
821 * @param $action
822 * @param string $options
823 * @return string|null
824 */
825 protected function o_outlines($id, $action, $options = '')
826 {
827 if ($action !== 'new') {
828 $o = &$this->objects[$id];
829 }
830
831 switch ($action) {
832 case 'new':
833 $this->objects[$id] = ['t' => 'outlines', 'info' => ['outlines' => []]];
834 $this->o_catalog($this->catalogId, 'outlines', $id);
835 break;
836
837 case 'outline':
838 $o['info']['outlines'][] = $options;
839 break;
840
841 case 'out':
842 if (count($o['info']['outlines'])) {
843 $res = "\n$id 0 obj\n<< /Type /Outlines /Kids [";
844 foreach ($o['info']['outlines'] as $v) {
845 $res .= "$v 0 R ";
846 }
847
848 $res .= "] /Count " . count($o['info']['outlines']) . " >>\nendobj";
849 } else {
850 $res = "\n$id 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj";
851 }
852
853 return $res;
854 }
855
856 return null;
857 }
858
859 /**
860 * an object to hold the font description
861 *
862 * @param $id
863 * @param $action
864 * @param string|array $options
865 * @return string|null
866 * @throws FontNotFoundException
867 */
868 protected function o_font($id, $action, $options = '')
869 {
870 if ($action !== 'new') {
871 $o = &$this->objects[$id];
872 }
873
874 switch ($action) {
875 case 'new':
876 $this->objects[$id] = [
877 't' => 'font',
878 'info' => [
879 'name' => $options['name'],
880 'fontFileName' => $options['fontFileName'],
881 'SubType' => 'Type1',
882 'isSubsetting' => $options['isSubsetting']
883 ]
884 ];
885 $fontNum = $this->numFonts;
886 $this->objects[$id]['info']['fontNum'] = $fontNum;
887
888 // deal with the encoding and the differences
889 if (isset($options['differences'])) {
890 // then we'll need an encoding dictionary
891 $this->numObj++;
892 $this->o_fontEncoding($this->numObj, 'new', $options);
893 $this->objects[$id]['info']['encodingDictionary'] = $this->numObj;
894 } else {
895 if (isset($options['encoding'])) {
896 // we can specify encoding here
897 switch ($options['encoding']) {
898 case 'WinAnsiEncoding':
899 case 'MacRomanEncoding':
900 case 'MacExpertEncoding':
901 $this->objects[$id]['info']['encoding'] = $options['encoding'];
902 break;
903
904 case 'none':
905 break;
906
907 default:
908 $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding';
909 break;
910 }
911 } else {
912 $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding';
913 }
914 }
915
916 if ($this->fonts[$options['fontFileName']]['isUnicode']) {
917 // For Unicode fonts, we need to incorporate font data into
918 // sub-sections that are linked from the primary font section.
919 // Look at o_fontGIDtoCID and o_fontDescendentCID functions
920 // for more information.
921 //
922 // All of this code is adapted from the excellent changes made to
923 // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/)
924
925 $toUnicodeId = ++$this->numObj;
926 $this->o_toUnicode($toUnicodeId, 'new');
927 $this->objects[$id]['info']['toUnicode'] = $toUnicodeId;
928
929 $cidFontId = ++$this->numObj;
930 $this->o_fontDescendentCID($cidFontId, 'new', $options);
931 $this->objects[$id]['info']['cidFont'] = $cidFontId;
932 }
933
934 // also tell the pages node about the new font
935 $this->o_pages($this->currentNode, 'font', ['fontNum' => $fontNum, 'objNum' => $id]);
936 break;
937
938 case 'add':
939 $font_options = $this->processFont($id, $o['info']);
940
941 if ($font_options !== false) {
942 foreach ($font_options as $k => $v) {
943 switch ($k) {
944 case 'BaseFont':
945 $o['info']['name'] = $v;
946 break;
947 case 'FirstChar':
948 case 'LastChar':
949 case 'Widths':
950 case 'FontDescriptor':
951 case 'SubType':
952 $this->addMessage('o_font ' . $k . " : " . $v);
953 $o['info'][$k] = $v;
954 break;
955 }
956 }
957
958 // pass values down to descendent font
959 if (isset($o['info']['cidFont'])) {
960 $this->o_fontDescendentCID($o['info']['cidFont'], 'add', $font_options);
961 }
962 }
963 break;
964
965 case 'out':
966 if ($this->fonts[$this->objects[$id]['info']['fontFileName']]['isUnicode']) {
967 // For Unicode fonts, we need to incorporate font data into
968 // sub-sections that are linked from the primary font section.
969 // Look at o_fontGIDtoCID and o_fontDescendentCID functions
970 // for more information.
971 //
972 // All of this code is adapted from the excellent changes made to
973 // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/)
974
975 $res = "\n$id 0 obj\n<</Type /Font\n/Subtype /Type0\n";
976 $res .= "/BaseFont /" . $o['info']['name'] . "\n";
977
978 // The horizontal identity mapping for 2-byte CIDs; may be used
979 // with CIDFonts using any Registry, Ordering, and Supplement values.
980 $res .= "/Encoding /Identity-H\n";
981 $res .= "/DescendantFonts [" . $o['info']['cidFont'] . " 0 R]\n";
982 $res .= "/ToUnicode " . $o['info']['toUnicode'] . " 0 R\n";
983 $res .= ">>\n";
984 $res .= "endobj";
985 } else {
986 $res = "\n$id 0 obj\n<< /Type /Font\n/Subtype /" . $o['info']['SubType'] . "\n";
987 $res .= "/Name /F" . $o['info']['fontNum'] . "\n";
988 $res .= "/BaseFont /" . $o['info']['name'] . "\n";
989
990 if (isset($o['info']['encodingDictionary'])) {
991 // then place a reference to the dictionary
992 $res .= "/Encoding " . $o['info']['encodingDictionary'] . " 0 R\n";
993 } else {
994 if (isset($o['info']['encoding'])) {
995 // use the specified encoding
996 $res .= "/Encoding /" . $o['info']['encoding'] . "\n";
997 }
998 }
999
1000 if (isset($o['info']['FirstChar'])) {
1001 $res .= "/FirstChar " . $o['info']['FirstChar'] . "\n";
1002 }
1003
1004 if (isset($o['info']['LastChar'])) {
1005 $res .= "/LastChar " . $o['info']['LastChar'] . "\n";
1006 }
1007
1008 if (isset($o['info']['Widths'])) {
1009 $res .= "/Widths " . $o['info']['Widths'] . " 0 R\n";
1010 }
1011
1012 if (isset($o['info']['FontDescriptor'])) {
1013 $res .= "/FontDescriptor " . $o['info']['FontDescriptor'] . " 0 R\n";
1014 }
1015
1016 $res .= ">>\n";
1017 $res .= "endobj";
1018 }
1019
1020 return $res;
1021 }
1022
1023 return null;
1024 }
1025
1026 protected function getFontSubsettingTag(array $font): string
1027 {
1028 // convert font num to hexavigesimal numeral system letters A - Z only
1029 $base_26 = strtoupper(base_convert($font['fontNum'], 10, 26));
1030 for ($i = 0; $i < strlen($base_26); $i++) {
1031 $char = $base_26[$i];
1032 if ($char <= "9") {
1033 $base_26[$i] = chr(65 + intval($char));
1034 } else {
1035 $base_26[$i] = chr(ord($char) + 10);
1036 }
1037 }
1038
1039 return 'SUB' . str_pad($base_26, 3, 'A', STR_PAD_LEFT);
1040 }
1041
1042 /**
1043 * @param int $fontObjId
1044 * @param array $object_info
1045 * @return array|false
1046 * @throws FontNotFoundException
1047 */
1048 private function processFont(int $fontObjId, array $object_info)
1049 {
1050 $fontFileName = $object_info['fontFileName'];
1051 if (!isset($this->fonts[$fontFileName])) {
1052 return false;
1053 }
1054
1055 $font = &$this->fonts[$fontFileName];
1056
1057 $fileSuffix = $font['fileSuffix'];
1058 $fileSuffixLower = strtolower($font['fileSuffix']);
1059 $fbfile = "$fontFileName.$fileSuffix";
1060 $isTtfFont = $fileSuffixLower === 'ttf';
1061 $isPfbFont = $fileSuffixLower === 'pfb';
1062
1063 $this->addMessage('selectFont: checking for - ' . $fbfile);
1064
1065 if (!$fileSuffix) {
1066 $this->addMessage(
1067 'selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts'
1068 );
1069
1070 return false;
1071 } else {
1072 $adobeFontName = isset($font['PostScriptName']) ? $font['PostScriptName'] : $font['FontName'];
1073 // $fontObj = $this->numObj;
1074 $this->addMessage("selectFont: adding font file - $fbfile - $adobeFontName");
1075
1076 // find the array of font widths, and put that into an object.
1077 $firstChar = -1;
1078 $lastChar = 0;
1079 $widths = [];
1080 $cid_widths = [];
1081
1082 foreach ($font['C'] as $num => $d) {
1083 if (intval($num) > 0 || $num == '0') {
1084 if (!$font['isUnicode']) {
1085 // With Unicode, widths array isn't used
1086 if ($lastChar > 0 && $num > $lastChar + 1) {
1087 for ($i = $lastChar + 1; $i < $num; $i++) {
1088 $widths[] = 0;
1089 }
1090 }
1091 }
1092
1093 $widths[] = $d;
1094
1095 if ($font['isUnicode']) {
1096 $cid_widths[$num] = $d;
1097 }
1098
1099 if ($firstChar == -1) {
1100 $firstChar = $num;
1101 }
1102
1103 $lastChar = $num;
1104 }
1105 }
1106
1107 // also need to adjust the widths for the differences array
1108 if (isset($object['differences'])) {
1109 foreach ($object['differences'] as $charNum => $charName) {
1110 if ($charNum > $lastChar) {
1111 if (!$object['isUnicode']) {
1112 // With Unicode, widths array isn't used
1113 for ($i = $lastChar + 1; $i <= $charNum; $i++) {
1114 $widths[] = 0;
1115 }
1116 }
1117
1118 $lastChar = $charNum;
1119 }
1120
1121 if (isset($font['C'][$charName])) {
1122 $widths[$charNum - $firstChar] = $font['C'][$charName];
1123 if ($font['isUnicode']) {
1124 $cid_widths[$charName] = $font['C'][$charName];
1125 }
1126 }
1127 }
1128 }
1129
1130 if ($font['isUnicode']) {
1131 $font['CIDWidths'] = $cid_widths;
1132 }
1133
1134 $this->addMessage('selectFont: FirstChar = ' . $firstChar);
1135 $this->addMessage('selectFont: LastChar = ' . $lastChar);
1136
1137 $widthid = -1;
1138
1139 if (!$font['isUnicode']) {
1140 // With Unicode, widths array isn't used
1141
1142 $this->numObj++;
1143 $this->o_contents($this->numObj, 'new', 'raw');
1144 $this->objects[$this->numObj]['c'] .= '[' . implode(' ', $widths) . ']';
1145 $widthid = $this->numObj;
1146 }
1147
1148 $missing_width = 500;
1149 $stemV = 70;
1150
1151 if (isset($font['MissingWidth'])) {
1152 $missing_width = $font['MissingWidth'];
1153 }
1154 if (isset($font['StdVW'])) {
1155 $stemV = $font['StdVW'];
1156 } else {
1157 if (isset($font['Weight']) && preg_match('!(bold|black)!i', $font['Weight'])) {
1158 $stemV = 120;
1159 }
1160 }
1161
1162 // load the pfb file, and put that into an object too.
1163 // note that pdf supports only binary format type 1 font files, though there is a
1164 // simple utility to convert them from pfa to pfb.
1165 $data = file_get_contents($fbfile);
1166
1167 // create the font descriptor
1168 $this->numObj++;
1169 $fontDescriptorId = $this->numObj;
1170
1171 $this->numObj++;
1172 $pfbid = $this->numObj;
1173
1174 // determine flags (more than a little flakey, hopefully will not matter much)
1175 $flags = 0;
1176
1177 if ($font['ItalicAngle'] != 0) {
1178 $flags += pow(2, 6);
1179 }
1180
1181 if ($font['IsFixedPitch'] === 'true') {
1182 $flags += 1;
1183 }
1184
1185 $flags += pow(2, 5); // assume non-sybolic
1186 $list = [
1187 'Ascent' => 'Ascender',
1188 'CapHeight' => 'Ascender', //FIXME: php-font-lib is not grabbing this value, so we'll fake it and use the Ascender value // 'CapHeight'
1189 'MissingWidth' => 'MissingWidth',
1190 'Descent' => 'Descender',
1191 'FontBBox' => 'FontBBox',
1192 'ItalicAngle' => 'ItalicAngle'
1193 ];
1194 $fdopt = [
1195 'Flags' => $flags,
1196 'FontName' => $adobeFontName,
1197 'StemV' => $stemV
1198 ];
1199
1200 foreach ($list as $k => $v) {
1201 if (isset($font[$v])) {
1202 $fdopt[$k] = $font[$v];
1203 }
1204 }
1205
1206 if ($isPfbFont) {
1207 $fdopt['FontFile'] = $pfbid;
1208 } elseif ($isTtfFont) {
1209 $fdopt['FontFile2'] = $pfbid;
1210 }
1211
1212 $this->o_fontDescriptor($fontDescriptorId, 'new', $fdopt);
1213
1214 // embed the font program
1215 $this->o_contents($this->numObj, 'new');
1216 $this->objects[$pfbid]['c'] .= $data;
1217
1218 // determine the cruicial lengths within this file
1219 if ($isPfbFont) {
1220 $l1 = strpos($data, 'eexec') + 6;
1221 $l2 = strpos($data, '00000000') - $l1;
1222 $l3 = mb_strlen($data, '8bit') - $l2 - $l1;
1223 $this->o_contents(
1224 $this->numObj,
1225 'add',
1226 ['Length1' => $l1, 'Length2' => $l2, 'Length3' => $l3]
1227 );
1228 } elseif ($isTtfFont) {
1229 $l1 = mb_strlen($data, '8bit');
1230 $this->o_contents($this->numObj, 'add', ['Length1' => $l1]);
1231 }
1232
1233 // tell the font object about all this new stuff
1234 $options = [
1235 'BaseFont' => $adobeFontName,
1236 'MissingWidth' => $missing_width,
1237 'Widths' => $widthid,
1238 'FirstChar' => $firstChar,
1239 'LastChar' => $lastChar,
1240 'FontDescriptor' => $fontDescriptorId
1241 ];
1242
1243 if ($isTtfFont) {
1244 $options['SubType'] = 'TrueType';
1245 }
1246
1247 $this->addMessage("adding extra info to font.($fontObjId)");
1248
1249 foreach ($options as $fk => $fv) {
1250 $this->addMessage("$fk : $fv");
1251 }
1252 }
1253
1254 return $options;
1255 }
1256
1257 /**
1258 * A toUnicode section, needed for unicode fonts
1259 *
1260 * @param $id
1261 * @param $action
1262 * @return null|string
1263 */
1264 protected function o_toUnicode($id, $action)
1265 {
1266 switch ($action) {
1267 case 'new':
1268 $this->objects[$id] = [
1269 't' => 'toUnicode'
1270 ];
1271 break;
1272 case 'add':
1273 break;
1274 case 'out':
1275 $ordering = 'UCS';
1276 $registry = 'Adobe';
1277
1278 if ($this->encrypted) {
1279 $this->encryptInit($id);
1280 $ordering = $this->ARC4($ordering);
1281 $registry = $this->filterText($this->ARC4($registry), false, false);
1282 }
1283
1284 $stream = <<<EOT
1285 /CIDInit /ProcSet findresource begin
1286 12 dict begin
1287 begincmap
1288 /CIDSystemInfo
1289 <</Registry ($registry)
1290 /Ordering ($ordering)
1291 /Supplement 0
1292 >> def
1293 /CMapName /Adobe-Identity-UCS def
1294 /CMapType 2 def
1295 1 begincodespacerange
1296 <0000> <FFFF>
1297 endcodespacerange
1298 1 beginbfrange
1299 <0000> <FFFF> <0000>
1300 endbfrange
1301 endcmap
1302 CMapName currentdict /CMap defineresource pop
1303 end
1304 end
1305 EOT;
1306
1307 $res = "\n$id 0 obj\n";
1308 $res .= "<</Length " . mb_strlen($stream, '8bit') . " >>\n";
1309 $res .= "stream\n" . $stream . "\nendstream" . "\nendobj";
1310
1311 return $res;
1312 }
1313
1314 return null;
1315 }
1316
1317 /**
1318 * a font descriptor, needed for including additional fonts
1319 *
1320 * @param $id
1321 * @param $action
1322 * @param string $options
1323 * @return null|string
1324 */
1325 protected function o_fontDescriptor($id, $action, $options = '')
1326 {
1327 if ($action !== 'new') {
1328 $o = &$this->objects[$id];
1329 }
1330
1331 switch ($action) {
1332 case 'new':
1333 $this->objects[$id] = ['t' => 'fontDescriptor', 'info' => $options];
1334 break;
1335
1336 case 'out':
1337 $res = "\n$id 0 obj\n<< /Type /FontDescriptor\n";
1338 foreach ($o['info'] as $label => $value) {
1339 switch ($label) {
1340 case 'Ascent':
1341 case 'CapHeight':
1342 case 'Descent':
1343 case 'Flags':
1344 case 'ItalicAngle':
1345 case 'StemV':
1346 case 'AvgWidth':
1347 case 'Leading':
1348 case 'MaxWidth':
1349 case 'MissingWidth':
1350 case 'StemH':
1351 case 'XHeight':
1352 case 'CharSet':
1353 if (mb_strlen($value, '8bit')) {
1354 $res .= "/$label $value\n";
1355 }
1356
1357 break;
1358 case 'FontFile':
1359 case 'FontFile2':
1360 case 'FontFile3':
1361 $res .= "/$label $value 0 R\n";
1362 break;
1363
1364 case 'FontBBox':
1365 $res .= "/$label [$value[0] $value[1] $value[2] $value[3]]\n";
1366 break;
1367
1368 case 'FontName':
1369 $res .= "/$label /$value\n";
1370 break;
1371 }
1372 }
1373
1374 $res .= ">>\nendobj";
1375
1376 return $res;
1377 }
1378
1379 return null;
1380 }
1381
1382 /**
1383 * the font encoding
1384 *
1385 * @param $id
1386 * @param $action
1387 * @param string $options
1388 * @return null|string
1389 */
1390 protected function o_fontEncoding($id, $action, $options = '')
1391 {
1392 if ($action !== 'new') {
1393 $o = &$this->objects[$id];
1394 }
1395
1396 switch ($action) {
1397 case 'new':
1398 // the options array should contain 'differences' and maybe 'encoding'
1399 $this->objects[$id] = ['t' => 'fontEncoding', 'info' => $options];
1400 break;
1401
1402 case 'out':
1403 $res = "\n$id 0 obj\n<< /Type /Encoding\n";
1404 if (!isset($o['info']['encoding'])) {
1405 $o['info']['encoding'] = 'WinAnsiEncoding';
1406 }
1407
1408 if ($o['info']['encoding'] !== 'none') {
1409 $res .= "/BaseEncoding /" . $o['info']['encoding'] . "\n";
1410 }
1411
1412 $res .= "/Differences \n[";
1413
1414 $onum = -100;
1415
1416 foreach ($o['info']['differences'] as $num => $label) {
1417 if ($num != $onum + 1) {
1418 // we cannot make use of consecutive numbering
1419 $res .= "\n$num /$label";
1420 } else {
1421 $res .= " /$label";
1422 }
1423
1424 $onum = $num;
1425 }
1426
1427 $res .= "\n]\n>>\nendobj";
1428
1429 return $res;
1430 }
1431
1432 return null;
1433 }
1434
1435 /**
1436 * a descendent cid font, needed for unicode fonts
1437 *
1438 * @param $id
1439 * @param $action
1440 * @param string|array $options
1441 * @return null|string
1442 */
1443 protected function o_fontDescendentCID($id, $action, $options = '')
1444 {
1445 if ($action !== 'new') {
1446 $o = &$this->objects[$id];
1447 }
1448
1449 switch ($action) {
1450 case 'new':
1451 $this->objects[$id] = ['t' => 'fontDescendentCID', 'info' => $options];
1452
1453 // we need a CID system info section
1454 $cidSystemInfoId = ++$this->numObj;
1455 $this->o_cidSystemInfo($cidSystemInfoId, 'new');
1456 $this->objects[$id]['info']['cidSystemInfo'] = $cidSystemInfoId;
1457
1458 // and a CID to GID map
1459 $cidToGidMapId = ++$this->numObj;
1460 $this->o_fontGIDtoCIDMap($cidToGidMapId, 'new', $options);
1461 $this->objects[$id]['info']['cidToGidMap'] = $cidToGidMapId;
1462 break;
1463
1464 case 'add':
1465 foreach ($options as $k => $v) {
1466 switch ($k) {
1467 case 'BaseFont':
1468 $o['info']['name'] = $v;
1469 break;
1470
1471 case 'FirstChar':
1472 case 'LastChar':
1473 case 'MissingWidth':
1474 case 'FontDescriptor':
1475 case 'SubType':
1476 $this->addMessage("o_fontDescendentCID $k : $v");
1477 $o['info'][$k] = $v;
1478 break;
1479 }
1480 }
1481
1482 // pass values down to cid to gid map
1483 $this->o_fontGIDtoCIDMap($o['info']['cidToGidMap'], 'add', $options);
1484 break;
1485
1486 case 'out':
1487 $res = "\n$id 0 obj\n";
1488 $res .= "<</Type /Font\n";
1489 $res .= "/Subtype /CIDFontType2\n";
1490 $res .= "/BaseFont /" . $o['info']['name'] . "\n";
1491 $res .= "/CIDSystemInfo " . $o['info']['cidSystemInfo'] . " 0 R\n";
1492 // if (isset($o['info']['FirstChar'])) {
1493 // $res.= "/FirstChar ".$o['info']['FirstChar']."\n";
1494 // }
1495
1496 // if (isset($o['info']['LastChar'])) {
1497 // $res.= "/LastChar ".$o['info']['LastChar']."\n";
1498 // }
1499 if (isset($o['info']['FontDescriptor'])) {
1500 $res .= "/FontDescriptor " . $o['info']['FontDescriptor'] . " 0 R\n";
1501 }
1502
1503 if (isset($o['info']['MissingWidth'])) {
1504 $res .= "/DW " . $o['info']['MissingWidth'] . "\n";
1505 }
1506
1507 if (isset($o['info']['fontFileName']) && isset($this->fonts[$o['info']['fontFileName']]['CIDWidths'])) {
1508 $cid_widths = &$this->fonts[$o['info']['fontFileName']]['CIDWidths'];
1509 $w = '';
1510 foreach ($cid_widths as $cid => $width) {
1511 $w .= "$cid [$width] ";
1512 }
1513 $res .= "/W [$w]\n";
1514 }
1515
1516 $res .= "/CIDToGIDMap " . $o['info']['cidToGidMap'] . " 0 R\n";
1517 $res .= ">>\n";
1518 $res .= "endobj";
1519
1520 return $res;
1521 }
1522
1523 return null;
1524 }
1525
1526 /**
1527 * CID system info section, needed for unicode fonts
1528 *
1529 * @param $id
1530 * @param $action
1531 * @return null|string
1532 */
1533 protected function o_cidSystemInfo($id, $action)
1534 {
1535 switch ($action) {
1536 case 'new':
1537 $this->objects[$id] = [
1538 't' => 'cidSystemInfo'
1539 ];
1540 break;
1541 case 'add':
1542 break;
1543 case 'out':
1544 $ordering = 'UCS';
1545 $registry = 'Adobe';
1546
1547 if ($this->encrypted) {
1548 $this->encryptInit($id);
1549 $ordering = $this->ARC4($ordering);
1550 $registry = $this->ARC4($registry);
1551 }
1552
1553
1554 $res = "\n$id 0 obj\n";
1555
1556 $res .= '<</Registry (' . $registry . ")\n"; // A string identifying an issuer of character collections
1557 $res .= '/Ordering (' . $ordering . ")\n"; // A string that uniquely names a character collection issued by a specific registry
1558 $res .= "/Supplement 0\n"; // The supplement number of the character collection.
1559 $res .= ">>";
1560
1561 $res .= "\nendobj";
1562
1563 return $res;
1564 }
1565
1566 return null;
1567 }
1568
1569 /**
1570 * a font glyph to character map, needed for unicode fonts
1571 *
1572 * @param $id
1573 * @param $action
1574 * @param string $options
1575 * @return null|string
1576 */
1577 protected function o_fontGIDtoCIDMap($id, $action, $options = '')
1578 {
1579 if ($action !== 'new') {
1580 $o = &$this->objects[$id];
1581 }
1582
1583 switch ($action) {
1584 case 'new':
1585 $this->objects[$id] = ['t' => 'fontGIDtoCIDMap', 'info' => $options];
1586 break;
1587
1588 case 'out':
1589 $res = "\n$id 0 obj\n";
1590 $fontFileName = $o['info']['fontFileName'];
1591 $tmp = $this->fonts[$fontFileName]['CIDtoGID'] = base64_decode($this->fonts[$fontFileName]['CIDtoGID']);
1592
1593 $compressed = isset($this->fonts[$fontFileName]['CIDtoGID_Compressed']) &&
1594 $this->fonts[$fontFileName]['CIDtoGID_Compressed'];
1595
1596 if (!$compressed && isset($o['raw'])) {
1597 $res .= $tmp;
1598 } else {
1599 $res .= "<<";
1600
1601 if (!$compressed && $this->compressionReady && $this->options['compression']) {
1602 // then implement ZLIB based compression on this content stream
1603 $compressed = true;
1604 $tmp = gzcompress($tmp, 6);
1605 }
1606 if ($compressed) {
1607 $res .= "\n/Filter /FlateDecode";
1608 }
1609
1610 if ($this->encrypted) {
1611 $this->encryptInit($id);
1612 $tmp = $this->ARC4($tmp);
1613 }
1614
1615 $res .= "\n/Length " . mb_strlen($tmp, '8bit') . ">>\nstream\n$tmp\nendstream";
1616 }
1617
1618 $res .= "\nendobj";
1619
1620 return $res;
1621 }
1622
1623 return null;
1624 }
1625
1626 /**
1627 * the document procset, solves some problems with printing to old PS printers
1628 *
1629 * @param $id
1630 * @param $action
1631 * @param string $options
1632 * @return null|string
1633 */
1634 protected function o_procset($id, $action, $options = '')
1635 {
1636 if ($action !== 'new') {
1637 $o = &$this->objects[$id];
1638 }
1639
1640 switch ($action) {
1641 case 'new':
1642 $this->objects[$id] = ['t' => 'procset', 'info' => ['PDF' => 1, 'Text' => 1]];
1643 $this->o_pages($this->currentNode, 'procset', $id);
1644 $this->procsetObjectId = $id;
1645 break;
1646
1647 case 'add':
1648 // this is to add new items to the procset list, despite the fact that this is considered
1649 // obsolete, the items are required for printing to some postscript printers
1650 switch ($options) {
1651 case 'ImageB':
1652 case 'ImageC':
1653 case 'ImageI':
1654 $o['info'][$options] = 1;
1655 break;
1656 }
1657 break;
1658
1659 case 'out':
1660 $res = "\n$id 0 obj\n[";
1661 foreach ($o['info'] as $label => $val) {
1662 $res .= "/$label ";
1663 }
1664 $res .= "]\nendobj";
1665
1666 return $res;
1667 }
1668
1669 return null;
1670 }
1671
1672 /**
1673 * define the document information
1674 *
1675 * @param $id
1676 * @param $action
1677 * @param string $options
1678 * @return null|string
1679 */
1680 protected function o_info($id, $action, $options = '')
1681 {
1682 switch ($action) {
1683 case 'new':
1684 $this->infoObject = $id;
1685 $date = 'D:' . @date('Ymd');
1686 $this->objects[$id] = [
1687 't' => 'info',
1688 'info' => [
1689 'Producer' => 'CPDF (dompdf)',
1690 'CreationDate' => $date
1691 ]
1692 ];
1693 break;
1694 case 'Title':
1695 case 'Author':
1696 case 'Subject':
1697 case 'Keywords':
1698 case 'Creator':
1699 case 'Producer':
1700 case 'CreationDate':
1701 case 'ModDate':
1702 case 'Trapped':
1703 $this->objects[$id]['info'][$action] = $options;
1704 break;
1705
1706 case 'out':
1707 $encrypted = $this->encrypted;
1708 if ($encrypted) {
1709 $this->encryptInit($id);
1710 }
1711
1712 $res = "\n$id 0 obj\n<<\n";
1713 $o = &$this->objects[$id];
1714 foreach ($o['info'] as $k => $v) {
1715 $res .= "/$k (";
1716
1717 // dates must be outputted as-is, without Unicode transformations
1718 if ($k !== 'CreationDate' && $k !== 'ModDate') {
1719 $v = $this->utf8toUtf16BE($v);
1720 }
1721
1722 if ($encrypted) {
1723 $v = $this->ARC4($v);
1724 }
1725
1726 $res .= $this->filterText($v, false, false);
1727 $res .= ")\n";
1728 }
1729
1730 $res .= ">>\nendobj";
1731
1732 return $res;
1733 }
1734
1735 return null;
1736 }
1737
1738 /**
1739 * an action object, used to link to URLS initially
1740 *
1741 * @param $id
1742 * @param $action
1743 * @param string $options
1744 * @return null|string
1745 */
1746 protected function o_action($id, $action, $options = '')
1747 {
1748 if ($action !== 'new') {
1749 $o = &$this->objects[$id];
1750 }
1751
1752 switch ($action) {
1753 case 'new':
1754 if (is_array($options)) {
1755 $this->objects[$id] = ['t' => 'action', 'info' => $options, 'type' => $options['type']];
1756 } else {
1757 // then assume a URI action
1758 $this->objects[$id] = ['t' => 'action', 'info' => $options, 'type' => 'URI'];
1759 }
1760 break;
1761
1762 case 'out':
1763 if ($this->encrypted) {
1764 $this->encryptInit($id);
1765 }
1766
1767 $res = "\n$id 0 obj\n<< /Type /Action";
1768 switch ($o['type']) {
1769 case 'ilink':
1770 if (!isset($this->destinations[(string)$o['info']['label']])) {
1771 break;
1772 }
1773
1774 // there will be an 'label' setting, this is the name of the destination
1775 $res .= "\n/S /GoTo\n/D " . $this->destinations[(string)$o['info']['label']] . " 0 R";
1776 break;
1777
1778 case 'URI':
1779 $res .= "\n/S /URI\n/URI (";
1780 if ($this->encrypted) {
1781 $res .= $this->filterText($this->ARC4($o['info']), false, false);
1782 } else {
1783 $res .= $this->filterText($o['info'], false, false);
1784 }
1785
1786 $res .= ")";
1787 break;
1788 }
1789
1790 $res .= "\n>>\nendobj";
1791
1792 return $res;
1793 }
1794
1795 return null;
1796 }
1797
1798 /**
1799 * an annotation object, this will add an annotation to the current page.
1800 * initially will support just link annotations
1801 *
1802 * @param $id
1803 * @param $action
1804 * @param string $options
1805 * @return null|string
1806 */
1807 protected function o_annotation($id, $action, $options = '')
1808 {
1809 if ($action !== 'new') {
1810 $o = &$this->objects[$id];
1811 }
1812
1813 switch ($action) {
1814 case 'new':
1815 // add the annotation to the current page
1816 $pageId = $this->currentPage;
1817 $this->o_page($pageId, 'annot', $id);
1818
1819 // and add the action object which is going to be required
1820 switch ($options['type']) {
1821 case 'link':
1822 $this->objects[$id] = ['t' => 'annotation', 'info' => $options];
1823 $this->numObj++;
1824 $this->o_action($this->numObj, 'new', $options['url']);
1825 $this->objects[$id]['info']['actionId'] = $this->numObj;
1826 break;
1827
1828 case 'ilink':
1829 // this is to a named internal link
1830 $label = $options['label'];
1831 $this->objects[$id] = ['t' => 'annotation', 'info' => $options];
1832 $this->numObj++;
1833 $this->o_action($this->numObj, 'new', ['type' => 'ilink', 'label' => $label]);
1834 $this->objects[$id]['info']['actionId'] = $this->numObj;
1835 break;
1836 }
1837 break;
1838
1839 case 'out':
1840 $res = "\n$id 0 obj\n<< /Type /Annot";
1841 switch ($o['info']['type']) {
1842 case 'link':
1843 case 'ilink':
1844 $res .= "\n/Subtype /Link";
1845 break;
1846 }
1847 $res .= "\n/A " . $o['info']['actionId'] . " 0 R";
1848 $res .= "\n/Border [0 0 0]";
1849 $res .= "\n/H /I";
1850 $res .= "\n/Rect [ ";
1851
1852 foreach ($o['info']['rect'] as $v) {
1853 $res .= sprintf("%.4F ", $v);
1854 }
1855
1856 $res .= "]";
1857 $res .= "\n>>\nendobj";
1858
1859 return $res;
1860 }
1861
1862 return null;
1863 }
1864
1865 /**
1866 * a page object, it also creates a contents object to hold its contents
1867 *
1868 * @param $id
1869 * @param $action
1870 * @param string $options
1871 * @return null|string
1872 */
1873 protected function o_page($id, $action, $options = '')
1874 {
1875 if ($action !== 'new') {
1876 $o = &$this->objects[$id];
1877 }
1878
1879 switch ($action) {
1880 case 'new':
1881 $this->numPages++;
1882 $this->objects[$id] = [
1883 't' => 'page',
1884 'info' => [
1885 'parent' => $this->currentNode,
1886 'pageNum' => $this->numPages,
1887 'mediaBox' => $this->objects[$this->currentNode]['info']['mediaBox']
1888 ]
1889 ];
1890
1891 if (is_array($options)) {
1892 // then this must be a page insertion, array should contain 'rid','pos'=[before|after]
1893 $options['id'] = $id;
1894 $this->o_pages($this->currentNode, 'page', $options);
1895 } else {
1896 $this->o_pages($this->currentNode, 'page', $id);
1897 }
1898
1899 $this->currentPage = $id;
1900 //make a contents object to go with this page
1901 $this->numObj++;
1902 $this->o_contents($this->numObj, 'new', $id);
1903 $this->currentContents = $this->numObj;
1904 $this->objects[$id]['info']['contents'] = [];
1905 $this->objects[$id]['info']['contents'][] = $this->numObj;
1906
1907 $match = ($this->numPages % 2 ? 'odd' : 'even');
1908 foreach ($this->addLooseObjects as $oId => $target) {
1909 if ($target === 'all' || $match === $target) {
1910 $this->objects[$id]['info']['contents'][] = $oId;
1911 }
1912 }
1913 break;
1914
1915 case 'content':
1916 $o['info']['contents'][] = $options;
1917 break;
1918
1919 case 'annot':
1920 // add an annotation to this page
1921 if (!isset($o['info']['annot'])) {
1922 $o['info']['annot'] = [];
1923 }
1924
1925 // $options should contain the id of the annotation dictionary
1926 $o['info']['annot'][] = $options;
1927 break;
1928
1929 case 'out':
1930 $res = "\n$id 0 obj\n<< /Type /Page";
1931 if (isset($o['info']['mediaBox'])) {
1932 $tmp = $o['info']['mediaBox'];
1933 $res .= "\n/MediaBox [" . sprintf(
1934 '%.3F %.3F %.3F %.3F',
1935 $tmp[0],
1936 $tmp[1],
1937 $tmp[2],
1938 $tmp[3]
1939 ) . ']';
1940 }
1941 $res .= "\n/Parent " . $o['info']['parent'] . " 0 R";
1942
1943 if (isset($o['info']['annot'])) {
1944 $res .= "\n/Annots [";
1945 foreach ($o['info']['annot'] as $aId) {
1946 $res .= " $aId 0 R";
1947 }
1948 $res .= " ]";
1949 }
1950
1951 $count = count($o['info']['contents']);
1952 if ($count == 1) {
1953 $res .= "\n/Contents " . $o['info']['contents'][0] . " 0 R";
1954 } else {
1955 if ($count > 1) {
1956 $res .= "\n/Contents [\n";
1957
1958 // reverse the page contents so added objects are below normal content
1959 //foreach (array_reverse($o['info']['contents']) as $cId) {
1960 // Back to normal now that I've got transparency working --Benj
1961 foreach ($o['info']['contents'] as $cId) {
1962 $res .= "$cId 0 R\n";
1963 }
1964 $res .= "]";
1965 }
1966 }
1967
1968 $res .= "\n>>\nendobj";
1969
1970 return $res;
1971 }
1972
1973 return null;
1974 }
1975
1976 /**
1977 * the contents objects hold all of the content which appears on pages
1978 *
1979 * @param $id
1980 * @param $action
1981 * @param string|array $options
1982 * @return null|string
1983 */
1984 protected function o_contents($id, $action, $options = '')
1985 {
1986 if ($action !== 'new') {
1987 $o = &$this->objects[$id];
1988 }
1989
1990 switch ($action) {
1991 case 'new':
1992 $this->objects[$id] = ['t' => 'contents', 'c' => '', 'info' => []];
1993 if (mb_strlen($options, '8bit') && intval($options)) {
1994 // then this contents is the primary for a page
1995 $this->objects[$id]['onPage'] = $options;
1996 } else {
1997 if ($options === 'raw') {
1998 // then this page contains some other type of system object
1999 $this->objects[$id]['raw'] = 1;
2000 }
2001 }
2002 break;
2003
2004 case 'add':
2005 // add more options to the declaration
2006 foreach ($options as $k => $v) {
2007 $o['info'][$k] = $v;
2008 }
2009
2010 case 'out':
2011 $tmp = $o['c'];
2012 $res = "\n$id 0 obj\n";
2013
2014 if (isset($this->objects[$id]['raw'])) {
2015 $res .= $tmp;
2016 } else {
2017 $res .= "<<";
2018 if ($this->compressionReady && $this->options['compression']) {
2019 // then implement ZLIB based compression on this content stream
2020 $res .= " /Filter /FlateDecode";
2021 $tmp = gzcompress($tmp, 6);
2022 }
2023
2024 if ($this->encrypted) {
2025 $this->encryptInit($id);
2026 $tmp = $this->ARC4($tmp);
2027 }
2028
2029 foreach ($o['info'] as $k => $v) {
2030 $res .= "\n/$k $v";
2031 }
2032
2033 $res .= "\n/Length " . mb_strlen($tmp, '8bit') . " >>\nstream\n$tmp\nendstream";
2034 }
2035
2036 $res .= "\nendobj";
2037
2038 return $res;
2039 }
2040
2041 return null;
2042 }
2043
2044 /**
2045 * @param $id
2046 * @param $action
2047 * @return string|null
2048 */
2049 protected function o_embedjs($id, $action)
2050 {
2051 switch ($action) {
2052 case 'new':
2053 $this->objects[$id] = [
2054 't' => 'embedjs',
2055 'info' => [
2056 'Names' => '[(EmbeddedJS) ' . ($id + 1) . ' 0 R]'
2057 ]
2058 ];
2059 break;
2060
2061 case 'out':
2062 $o = &$this->objects[$id];
2063 $res = "\n$id 0 obj\n<< ";
2064 foreach ($o['info'] as $k => $v) {
2065 $res .= "\n/$k $v";
2066 }
2067 $res .= "\n>>\nendobj";
2068
2069 return $res;
2070 }
2071
2072 return null;
2073 }
2074
2075 /**
2076 * @param $id
2077 * @param $action
2078 * @param string $code
2079 * @return null|string
2080 */
2081 protected function o_javascript($id, $action, $code = '')
2082 {
2083 switch ($action) {
2084 case 'new':
2085 $this->objects[$id] = [
2086 't' => 'javascript',
2087 'info' => [
2088 'S' => '/JavaScript',
2089 'JS' => '(' . $this->filterText($code, true, false) . ')',
2090 ]
2091 ];
2092 break;
2093
2094 case 'out':
2095 $o = &$this->objects[$id];
2096 $res = "\n$id 0 obj\n<< ";
2097
2098 foreach ($o['info'] as $k => $v) {
2099 $res .= "\n/$k $v";
2100 }
2101 $res .= "\n>>\nendobj";
2102
2103 return $res;
2104 }
2105
2106 return null;
2107 }
2108
2109 /**
2110 * an image object, will be an XObject in the document, includes description and data
2111 *
2112 * @param $id
2113 * @param $action
2114 * @param string $options
2115 * @return null|string
2116 */
2117 protected function o_image($id, $action, $options = '')
2118 {
2119 switch ($action) {
2120 case 'new':
2121 // make the new object
2122 $this->objects[$id] = ['t' => 'image', 'data' => &$options['data'], 'info' => []];
2123
2124 $info =& $this->objects[$id]['info'];
2125
2126 $info['Type'] = '/XObject';
2127 $info['Subtype'] = '/Image';
2128 $info['Width'] = $options['iw'];
2129 $info['Height'] = $options['ih'];
2130
2131 if (isset($options['masked']) && $options['masked']) {
2132 $info['SMask'] = ($this->numObj - 1) . ' 0 R';
2133 }
2134
2135 if (!isset($options['type']) || $options['type'] === 'jpg') {
2136 if (!isset($options['channels'])) {
2137 $options['channels'] = 3;
2138 }
2139
2140 switch ($options['channels']) {
2141 case 1:
2142 $info['ColorSpace'] = '/DeviceGray';
2143 break;
2144 case 4:
2145 $info['ColorSpace'] = '/DeviceCMYK';
2146 break;
2147 default:
2148 $info['ColorSpace'] = '/DeviceRGB';
2149 break;
2150 }
2151
2152 if ($info['ColorSpace'] === '/DeviceCMYK') {
2153 $info['Decode'] = '[1 0 1 0 1 0 1 0]';
2154 }
2155
2156 $info['Filter'] = '/DCTDecode';
2157 $info['BitsPerComponent'] = 8;
2158 } else {
2159 if ($options['type'] === 'png') {
2160 $info['Filter'] = '/FlateDecode';
2161 $info['DecodeParms'] = '<< /Predictor 15 /Colors ' . $options['ncolor'] . ' /Columns ' . $options['iw'] . ' /BitsPerComponent ' . $options['bitsPerComponent'] . '>>';
2162
2163 if ($options['isMask']) {
2164 $info['ColorSpace'] = '/DeviceGray';
2165 } else {
2166 if (mb_strlen($options['pdata'], '8bit')) {
2167 $tmp = ' [ /Indexed /DeviceRGB ' . (mb_strlen($options['pdata'], '8bit') / 3 - 1) . ' ';
2168 $this->numObj++;
2169 $this->o_contents($this->numObj, 'new');
2170 $this->objects[$this->numObj]['c'] = $options['pdata'];
2171 $tmp .= $this->numObj . ' 0 R';
2172 $tmp .= ' ]';
2173 $info['ColorSpace'] = $tmp;
2174
2175 if (isset($options['transparency'])) {
2176 $transparency = $options['transparency'];
2177 switch ($transparency['type']) {
2178 case 'indexed':
2179 $tmp = ' [ ' . $transparency['data'] . ' ' . $transparency['data'] . '] ';
2180 $info['Mask'] = $tmp;
2181 break;
2182
2183 case 'color-key':
2184 $tmp = ' [ ' .
2185 $transparency['r'] . ' ' . $transparency['r'] .
2186 $transparency['g'] . ' ' . $transparency['g'] .
2187 $transparency['b'] . ' ' . $transparency['b'] .
2188 ' ] ';
2189 $info['Mask'] = $tmp;
2190 break;
2191 }
2192 }
2193 } else {
2194 if (isset($options['transparency'])) {
2195 $transparency = $options['transparency'];
2196
2197 switch ($transparency['type']) {
2198 case 'indexed':
2199 $tmp = ' [ ' . $transparency['data'] . ' ' . $transparency['data'] . '] ';
2200 $info['Mask'] = $tmp;
2201 break;
2202
2203 case 'color-key':
2204 $tmp = ' [ ' .
2205 $transparency['r'] . ' ' . $transparency['r'] . ' ' .
2206 $transparency['g'] . ' ' . $transparency['g'] . ' ' .
2207 $transparency['b'] . ' ' . $transparency['b'] .
2208 ' ] ';
2209 $info['Mask'] = $tmp;
2210 break;
2211 }
2212 }
2213 $info['ColorSpace'] = '/' . $options['color'];
2214 }
2215 }
2216
2217 $info['BitsPerComponent'] = $options['bitsPerComponent'];
2218 }
2219 }
2220
2221 // assign it a place in the named resource dictionary as an external object, according to
2222 // the label passed in with it.
2223 $this->o_pages($this->currentNode, 'xObject', ['label' => $options['label'], 'objNum' => $id]);
2224
2225 // also make sure that we have the right procset object for it.
2226 $this->o_procset($this->procsetObjectId, 'add', 'ImageC');
2227 break;
2228
2229 case 'out':
2230 $o = &$this->objects[$id];
2231 $tmp = &$o['data'];
2232 $res = "\n$id 0 obj\n<<";
2233
2234 foreach ($o['info'] as $k => $v) {
2235 $res .= "\n/$k $v";
2236 }
2237
2238 if ($this->encrypted) {
2239 $this->encryptInit($id);
2240 $tmp = $this->ARC4($tmp);
2241 }
2242
2243 $res .= "\n/Length " . mb_strlen($tmp, '8bit') . ">>\nstream\n$tmp\nendstream\nendobj";
2244
2245 return $res;
2246 }
2247
2248 return null;
2249 }
2250
2251 /**
2252 * graphics state object
2253 *
2254 * @param $id
2255 * @param $action
2256 * @param string $options
2257 * @return null|string
2258 */
2259 protected function o_extGState($id, $action, $options = "")
2260 {
2261 static $valid_params = [
2262 "LW",
2263 "LC",
2264 "LC",
2265 "LJ",
2266 "ML",
2267 "D",
2268 "RI",
2269 "OP",
2270 "op",
2271 "OPM",
2272 "Font",
2273 "BG",
2274 "BG2",
2275 "UCR",
2276 "TR",
2277 "TR2",
2278 "HT",
2279 "FL",
2280 "SM",
2281 "SA",
2282 "BM",
2283 "SMask",
2284 "CA",
2285 "ca",
2286 "AIS",
2287 "TK"
2288 ];
2289
2290 switch ($action) {
2291 case "new":
2292 $this->objects[$id] = ['t' => 'extGState', 'info' => $options];
2293
2294 // Tell the pages about the new resource
2295 $this->numStates++;
2296 $this->o_pages($this->currentNode, 'extGState', ["objNum" => $id, "stateNum" => $this->numStates]);
2297 break;
2298
2299 case "out":
2300 $o = &$this->objects[$id];
2301 $res = "\n$id 0 obj\n<< /Type /ExtGState\n";
2302
2303 foreach ($o["info"] as $k => $v) {
2304 if (!in_array($k, $valid_params)) {
2305 continue;
2306 }
2307 $res .= "/$k $v\n";
2308 }
2309
2310 $res .= ">>\nendobj";
2311
2312 return $res;
2313 }
2314
2315 return null;
2316 }
2317
2318 /**
2319 * @param integer $id
2320 * @param string $action
2321 * @param mixed $options
2322 * @return string
2323 */
2324 protected function o_xobject($id, $action, $options = '')
2325 {
2326 switch ($action) {
2327 case 'new':
2328 $this->objects[$id] = ['t' => 'xobject', 'info' => $options, 'c' => ''];
2329 break;
2330
2331 case 'procset':
2332 $this->objects[$id]['procset'] = $options;
2333 break;
2334
2335 case 'font':
2336 $this->objects[$id]['fonts'][$options['fontNum']] = [
2337 'objNum' => $options['objNum'],
2338 'fontNum' => $options['fontNum']
2339 ];
2340 break;
2341
2342 case 'xObject':
2343 $this->objects[$id]['xObjects'][] = ['objNum' => $options['objNum'], 'label' => $options['label']];
2344 break;
2345
2346 case 'out':
2347 $o = &$this->objects[$id];
2348 $res = "\n$id 0 obj\n<< /Type /XObject\n";
2349
2350 foreach ($o["info"] as $k => $v) {
2351 switch ($k) {
2352 case 'Subtype':
2353 $res .= "/Subtype /$v\n";
2354 break;
2355 case 'bbox':
2356 $res .= "/BBox [";
2357 foreach ($v as $value) {
2358 $res .= sprintf("%.4F ", $value);
2359 }
2360 $res .= "]\n";
2361 break;
2362 default:
2363 $res .= "/$k $v\n";
2364 break;
2365 }
2366 }
2367 $res .= "/Matrix[1.0 0.0 0.0 1.0 0.0 0.0]\n";
2368
2369 $res .= "/Resources <<";
2370 if (isset($o['procset'])) {
2371 $res .= "\n/ProcSet " . $o['procset'] . " 0 R";
2372 } else {
2373 $res .= "\n/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]";
2374 }
2375 if (isset($o['fonts']) && count($o['fonts'])) {
2376 $res .= "\n/Font << ";
2377 foreach ($o['fonts'] as $finfo) {
2378 $res .= "\n/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R";
2379 }
2380 $res .= "\n>>";
2381 }
2382 if (isset($o['xObjects']) && count($o['xObjects'])) {
2383 $res .= "\n/XObject << ";
2384 foreach ($o['xObjects'] as $finfo) {
2385 $res .= "\n/" . $finfo['label'] . " " . $finfo['objNum'] . " 0 R";
2386 }
2387 $res .= "\n>>";
2388 }
2389 $res .= "\n>>\n";
2390
2391 $tmp = $o["c"];
2392 if ($this->compressionReady && $this->options['compression']) {
2393 // then implement ZLIB based compression on this content stream
2394 $res .= " /Filter /FlateDecode\n";
2395 $tmp = gzcompress($tmp, 6);
2396 }
2397
2398 if ($this->encrypted) {
2399 $this->encryptInit($id);
2400 $tmp = $this->ARC4($tmp);
2401 }
2402
2403 $res .= "/Length " . mb_strlen($tmp, '8bit') . " >>\n";
2404 $res .= "stream\n" . $tmp . "\nendstream" . "\nendobj";
2405
2406 return $res;
2407 }
2408
2409 return null;
2410 }
2411
2412 /**
2413 * @param $id
2414 * @param $action
2415 * @param string $options
2416 * @return null|string
2417 */
2418 protected function o_acroform($id, $action, $options = '')
2419 {
2420 switch ($action) {
2421 case "new":
2422 $this->o_catalog($this->catalogId, 'acroform', $id);
2423 $this->objects[$id] = array('t' => 'acroform', 'info' => $options);
2424 break;
2425
2426 case 'addfield':
2427 $this->objects[$id]['info']['Fields'][] = $options;
2428 break;
2429
2430 case 'font':
2431 $this->objects[$id]['fonts'][$options['fontNum']] = [
2432 'objNum' => $options['objNum'],
2433 'fontNum' => $options['fontNum']
2434 ];
2435 break;
2436
2437 case "out":
2438 $o = &$this->objects[$id];
2439 $res = "\n$id 0 obj\n<<";
2440
2441 foreach ($o["info"] as $k => $v) {
2442 switch ($k) {
2443 case 'Fields':
2444 $res .= " /Fields [";
2445 foreach ($v as $i) {
2446 $res .= "$i 0 R ";
2447 }
2448 $res .= "]\n";
2449 break;
2450 default:
2451 $res .= "/$k $v\n";
2452 }
2453 }
2454
2455 $res .= "/DR <<\n";
2456 if (isset($o['fonts']) && count($o['fonts'])) {
2457 $res .= "/Font << \n";
2458 foreach ($o['fonts'] as $finfo) {
2459 $res .= "/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R\n";
2460 }
2461 $res .= ">>\n";
2462 }
2463 $res .= ">>\n";
2464
2465 $res .= ">>\nendobj";
2466
2467 return $res;
2468 }
2469
2470 return null;
2471 }
2472
2473 /**
2474 * @param $id
2475 * @param $action
2476 * @param mixed $options
2477 * @return null|string
2478 */
2479 protected function o_field($id, $action, $options = '')
2480 {
2481 switch ($action) {
2482 case "new":
2483 $this->o_page($options['pageid'], 'annot', $id);
2484 $this->o_acroform($this->acroFormId, 'addfield', $id);
2485 $this->objects[$id] = ['t' => 'field', 'info' => $options];
2486 break;
2487
2488 case 'set':
2489 $this->objects[$id]['info'] = array_merge($this->objects[$id]['info'], $options);
2490 break;
2491
2492 case "out":
2493 $o = &$this->objects[$id];
2494 $res = "\n$id 0 obj\n<< /Type /Annot /Subtype /Widget \n";
2495
2496 $encrypted = $this->encrypted;
2497 if ($encrypted) {
2498 $this->encryptInit($id);
2499 }
2500
2501 foreach ($o["info"] as $k => $v) {
2502 switch ($k) {
2503 case 'pageid':
2504 $res .= "/P $v 0 R\n";
2505 break;
2506 case 'value':
2507 if ($encrypted) {
2508 $v = $this->filterText($this->ARC4($v), false, false);
2509 }
2510 $res .= "/V ($v)\n";
2511 break;
2512 case 'refvalue':
2513 $res .= "/V $v 0 R\n";
2514 break;
2515 case 'da':
2516 if ($encrypted) {
2517 $v = $this->filterText($this->ARC4($v), false, false);
2518 }
2519 $res .= "/DA ($v)\n";
2520 break;
2521 case 'options':
2522 $res .= "/Opt [\n";
2523 foreach ($v as $opt) {
2524 if ($encrypted) {
2525 $opt = $this->filterText($this->ARC4($opt), false, false);
2526 }
2527 $res .= "($opt)\n";
2528 }
2529 $res .= "]\n";
2530 break;
2531 case 'rect':
2532 $res .= "/Rect [";
2533 foreach ($v as $value) {
2534 $res .= sprintf("%.4F ", $value);
2535 }
2536 $res .= "]\n";
2537 break;
2538 case 'appearance':
2539 $res .= "/AP << ";
2540 foreach ($v as $a => $ref) {
2541 $res .= "/$a $ref 0 R ";
2542 }
2543 $res .= ">>\n";
2544 break;
2545 case 'T':
2546 if ($encrypted) {
2547 $v = $this->filterText($this->ARC4($v), false, false);
2548 }
2549 $res .= "/T ($v)\n";
2550 break;
2551 default:
2552 $res .= "/$k $v\n";
2553 }
2554
2555 }
2556
2557 $res .= ">>\nendobj";
2558
2559 return $res;
2560 }
2561
2562 return null;
2563 }
2564
2565 /**
2566 *
2567 * @param $id
2568 * @param $action
2569 * @param string $options
2570 * @return null|string
2571 */
2572 protected function o_sig($id, $action, $options = '')
2573 {
2574 $sign_maxlen = $this->signatureMaxLen;
2575
2576 switch ($action) {
2577 case "new":
2578 $this->objects[$id] = array('t' => 'sig', 'info' => $options);
2579 $this->byteRange[$id] = ['t' => 'sig'];
2580 break;
2581
2582 case 'byterange':
2583 $o = &$this->objects[$id];
2584 $content =& $options['content'];
2585 $content_len = strlen($content);
2586 $pos = strpos($content, sprintf("/ByteRange [ %'.010d", $id));
2587 $len = strlen('/ByteRange [ ********** ********** ********** ********** ]');
2588 $rangeStartPos = $pos + $len + 1 + 10; // before '<'
2589 $content = substr_replace($content, str_pad(sprintf('/ByteRange [ 0 %u %u %u ]', $rangeStartPos, $rangeStartPos + $sign_maxlen + 2, $content_len - 2 - $sign_maxlen - $rangeStartPos), $len, ' ', STR_PAD_RIGHT), $pos, $len);
2590
2591 $fuid = uniqid();
2592 $tmpInput = $this->tmp . "/pkcs7.tmp." . $fuid . '.in';
2593 $tmpOutput = $this->tmp . "/pkcs7.tmp." . $fuid . '.out';
2594
2595 if (file_put_contents($tmpInput, substr($content, 0, $rangeStartPos)) === false) {
2596 throw new \Exception("Unable to write temporary file for signing.");
2597 }
2598 if (file_put_contents($tmpInput, substr($content, $rangeStartPos + 2 + $sign_maxlen),
2599 FILE_APPEND) === false) {
2600 throw new \Exception("Unable to write temporary file for signing.");
2601 }
2602
2603 if (openssl_pkcs7_sign($tmpInput, $tmpOutput,
2604 $o['info']['SignCert'],
2605 array($o['info']['PrivKey'], $o['info']['Password']),
2606 array(), PKCS7_BINARY | PKCS7_DETACHED) === false) {
2607 throw new \Exception("Failed to prepare signature.");
2608 }
2609
2610 $signature = file_get_contents($tmpOutput);
2611
2612 unlink($tmpInput);
2613 unlink($tmpOutput);
2614
2615 $sign = substr($signature, (strpos($signature, "%%EOF\n\n------") + 13));
2616 list($head, $signature) = explode("\n\n", $sign);
2617
2618 $signature = base64_decode(trim($signature));
2619
2620 $signature = current(unpack('H*', $signature));
2621 $signature = str_pad($signature, $sign_maxlen, '0');
2622 $siglen = strlen($signature);
2623 if (strlen($signature) > $sign_maxlen) {
2624 throw new \Exception("Signature length ($siglen) exceeds the $sign_maxlen limit.");
2625 }
2626
2627 $content = substr_replace($content, $signature, $rangeStartPos + 1, $sign_maxlen);
2628 break;
2629
2630 case "out":
2631 $res = "\n$id 0 obj\n<<\n";
2632
2633 $encrypted = $this->encrypted;
2634 if ($encrypted) {
2635 $this->encryptInit($id);
2636 }
2637
2638 $res .= "/ByteRange " .sprintf("[ %'.010d ********** ********** ********** ]\n", $id);
2639 $res .= "/Contents <" . str_pad('', $sign_maxlen, '0') . ">\n";
2640 $res .= "/Filter/Adobe.PPKLite\n"; //PPKMS \n";
2641 $res .= "/Type/Sig/SubFilter/adbe.pkcs7.detached \n";
2642
2643 $date = "D:" . substr_replace(date('YmdHisO'), '\'', -2, 0) . '\'';
2644 if ($encrypted) {
2645 $date = $this->ARC4($date);
2646 }
2647
2648 $res .= "/M ($date)\n";
2649 $res .= "/Prop_Build << /App << /Name /DomPDF >> /Filter << /Name /Adobe.PPKLite >> >>\n";
2650
2651 $o = &$this->objects[$id];
2652 foreach ($o['info'] as $k => $v) {
2653 switch ($k) {
2654 case 'Name':
2655 case 'Location':
2656 case 'Reason':
2657 case 'ContactInfo':
2658 if ($v !== null && $v !== '') {
2659 $res .= "/$k (" .
2660 ($encrypted ? $this->filterText($this->ARC4($v), false, false) : $v) . ") \n";
2661 }
2662 break;
2663 }
2664 }
2665 $res .= ">>\nendobj";
2666
2667 return $res;
2668 }
2669
2670 return null;
2671 }
2672
2673 /**
2674 * encryption object.
2675 *
2676 * @param $id
2677 * @param $action
2678 * @param string $options
2679 * @return string|null
2680 */
2681 protected function o_encryption($id, $action, $options = '')
2682 {
2683 switch ($action) {
2684 case 'new':
2685 // make the new object
2686 $this->objects[$id] = ['t' => 'encryption', 'info' => $options];
2687 $this->arc4_objnum = $id;
2688 break;
2689
2690 case 'keys':
2691 // figure out the additional parameters required
2692 $pad = chr(0x28) . chr(0xBF) . chr(0x4E) . chr(0x5E) . chr(0x4E) . chr(0x75) . chr(0x8A) . chr(0x41)
2693 . chr(0x64) . chr(0x00) . chr(0x4E) . chr(0x56) . chr(0xFF) . chr(0xFA) . chr(0x01) . chr(0x08)
2694 . chr(0x2E) . chr(0x2E) . chr(0x00) . chr(0xB6) . chr(0xD0) . chr(0x68) . chr(0x3E) . chr(0x80)
2695 . chr(0x2F) . chr(0x0C) . chr(0xA9) . chr(0xFE) . chr(0x64) . chr(0x53) . chr(0x69) . chr(0x7A);
2696
2697 $info = $this->objects[$id]['info'];
2698
2699 $len = mb_strlen($info['owner'], '8bit');
2700
2701 if ($len > 32) {
2702 $owner = substr($info['owner'], 0, 32);
2703 } else {
2704 if ($len < 32) {
2705 $owner = $info['owner'] . substr($pad, 0, 32 - $len);
2706 } else {
2707 $owner = $info['owner'];
2708 }
2709 }
2710
2711 $len = mb_strlen($info['user'], '8bit');
2712 if ($len > 32) {
2713 $user = substr($info['user'], 0, 32);
2714 } else {
2715 if ($len < 32) {
2716 $user = $info['user'] . substr($pad, 0, 32 - $len);
2717 } else {
2718 $user = $info['user'];
2719 }
2720 }
2721
2722 $tmp = $this->md5_16($owner);
2723 $okey = substr($tmp, 0, 5);
2724 $this->ARC4_init($okey);
2725 $ovalue = $this->ARC4($user);
2726 $this->objects[$id]['info']['O'] = $ovalue;
2727
2728 // now make the u value, phew.
2729 $tmp = $this->md5_16(
2730 $user . $ovalue . chr($info['p']) . chr(255) . chr(255) . chr(255) . hex2bin($this->fileIdentifier)
2731 );
2732
2733 $ukey = substr($tmp, 0, 5);
2734 $this->ARC4_init($ukey);
2735 $this->encryptionKey = $ukey;
2736 $this->encrypted = true;
2737 $uvalue = $this->ARC4($pad);
2738 $this->objects[$id]['info']['U'] = $uvalue;
2739 // initialize the arc4 array
2740 break;
2741
2742 case 'out':
2743 $o = &$this->objects[$id];
2744
2745 $res = "\n$id 0 obj\n<<";
2746 $res .= "\n/Filter /Standard";
2747 $res .= "\n/V 1";
2748 $res .= "\n/R 2";
2749 $res .= "\n/O (" . $this->filterText($o['info']['O'], false, false) . ')';
2750 $res .= "\n/U (" . $this->filterText($o['info']['U'], false, false) . ')';
2751 // and the p-value needs to be converted to account for the twos-complement approach
2752 $o['info']['p'] = (($o['info']['p'] ^ 255) + 1) * -1;
2753 $res .= "\n/P " . ($o['info']['p']);
2754 $res .= "\n>>\nendobj";
2755
2756 return $res;
2757 }
2758
2759 return null;
2760 }
2761
2762 protected function o_indirect_references($id, $action, $options = null)
2763 {
2764 switch ($action) {
2765 case 'new':
2766 case 'add':
2767 if ($id === 0) {
2768 $id = ++$this->numObj;
2769 $this->o_catalog($this->catalogId, 'names', $id);
2770 $this->objects[$id] = ['t' => 'indirect_references', 'info' => $options];
2771 $this->indirectReferenceId = $id;
2772 } else {
2773 $this->objects[$id]['info'] = array_merge($this->objects[$id]['info'], $options);
2774 }
2775 break;
2776 case 'out':
2777 $res = "\n$id 0 obj << ";
2778
2779 foreach ($this->objects[$id]['info'] as $referenceObjName => $referenceObjId) {
2780 $res .= "/$referenceObjName $referenceObjId 0 R ";
2781 }
2782
2783 $res .= ">> endobj";
2784 return $res;
2785 }
2786
2787 return null;
2788 }
2789
2790 protected function o_names($id, $action, $options = null)
2791 {
2792 switch ($action) {
2793 case 'new':
2794 case 'add':
2795 if ($id === 0) {
2796 $id = ++$this->numObj;
2797 $this->objects[$id] = ['t' => 'names', 'info' => [$options]];
2798 $this->o_indirect_references($this->indirectReferenceId, 'add', ['EmbeddedFiles' => $id]);
2799 $this->embeddedFilesId = $id;
2800 } else {
2801 $this->objects[$id]['info'][] = $options;
2802 }
2803 break;
2804 case 'out':
2805 $info = &$this->objects[$id]['info'];
2806 $res = '';
2807 if (count($info) > 0) {
2808 $res = "\n$id 0 obj << /Names [ ";
2809
2810 if ($this->encrypted) {
2811 $this->encryptInit($id);
2812 }
2813
2814 foreach ($info as $entry) {
2815 if ($this->encrypted) {
2816 $filename = $this->ARC4($entry['filename']);
2817 } else {
2818 $filename = $entry['filename'];
2819 }
2820
2821 $res .= "($filename) " . $entry['dict_reference'] . " 0 R ";
2822 }
2823
2824 $res .= "] >> endobj";
2825 }
2826 return $res;
2827 }
2828
2829 return null;
2830 }
2831
2832 protected function o_embedded_file_dictionary($id, $action, $options = null)
2833 {
2834 switch ($action) {
2835 case 'new':
2836 $embeddedFileId = ++$this->numObj;
2837 $options['embedded_reference'] = $embeddedFileId;
2838 $this->objects[$id] = ['t' => 'embedded_file_dictionary', 'info' => $options];
2839 $this->o_embedded_file($embeddedFileId, 'new', $options);
2840 $options['dict_reference'] = $id;
2841 $this->o_names($this->embeddedFilesId, 'add', $options);
2842 break;
2843 case 'out':
2844 $info = &$this->objects[$id]['info'];
2845 $filename = $this->utf8toUtf16BE($info['filename']);
2846 $description = $this->utf8toUtf16BE($info['description']);
2847
2848 if ($this->encrypted) {
2849 $this->encryptInit($id);
2850 $filename = $this->ARC4($filename);
2851 $description = $this->ARC4($description);
2852 }
2853
2854 $filename = $this->filterText($filename, false, false);
2855 $description = $this->filterText($description, false, false);
2856
2857 $res = "\n$id 0 obj <</Type /Filespec /EF";
2858 $res .= " <</F " . $info['embedded_reference'] . " 0 R >>";
2859 $res .= " /F ($filename) /UF ($filename) /Desc ($description)";
2860 $res .= " >> endobj";
2861 return $res;
2862 }
2863
2864 return null;
2865 }
2866
2867 protected function o_embedded_file($id, $action, $options = null): ?string
2868 {
2869 switch ($action) {
2870 case 'new':
2871 $this->objects[$id] = ['t' => 'embedded_file', 'info' => $options];
2872 break;
2873 case 'out':
2874 $info = &$this->objects[$id]['info'];
2875
2876 if ($this->compressionReady) {
2877 $filepath = $info['filepath'];
2878 $checksum = md5_file($filepath);
2879 $f = fopen($filepath, "rb");
2880
2881 $file_content_compressed = '';
2882 $deflateContext = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => 6]);
2883 while (($block = fread($f, 8192))) {
2884 $file_content_compressed .= deflate_add($deflateContext, $block, ZLIB_NO_FLUSH);
2885 }
2886 $file_content_compressed .= deflate_add($deflateContext, '', ZLIB_FINISH);
2887 $file_size_uncompressed = ftell($f);
2888 fclose($f);
2889 } else {
2890 $file_content = file_get_contents($info['filepath']);
2891 $file_size_uncompressed = mb_strlen($file_content, '8bit');
2892 $checksum = md5($file_content);
2893 }
2894
2895 if ($this->encrypted) {
2896 $this->encryptInit($id);
2897 $checksum = $this->ARC4($checksum);
2898 $file_content_compressed = $this->ARC4($file_content_compressed);
2899 }
2900 $file_size_compressed = mb_strlen($file_content_compressed, '8bit');
2901
2902 $res = "\n$id 0 obj <</Params <</Size $file_size_uncompressed /CheckSum ($checksum) >>" .
2903 " /Type/EmbeddedFile /Filter/FlateDecode" .
2904 " /Length $file_size_compressed >> stream\n$file_content_compressed\nendstream\nendobj";
2905
2906 return $res;
2907 }
2908
2909 return null;
2910 }
2911
2912 /**
2913 * ARC4 functions
2914 * A series of function to implement ARC4 encoding in PHP
2915 */
2916
2917 /**
2918 * calculate the 16 byte version of the 128 bit md5 digest of the string
2919 *
2920 * @param $string
2921 * @return string
2922 */
2923 function md5_16($string)
2924 {
2925 $tmp = md5($string);
2926 $out = '';
2927 for ($i = 0; $i <= 30; $i = $i + 2) {
2928 $out .= chr(hexdec(substr($tmp, $i, 2)));
2929 }
2930
2931 return $out;
2932 }
2933
2934 /**
2935 * initialize the encryption for processing a particular object
2936 *
2937 * @param $id
2938 */
2939 function encryptInit($id)
2940 {
2941 $tmp = $this->encryptionKey;
2942 $hex = dechex($id);
2943 if (mb_strlen($hex, '8bit') < 6) {
2944 $hex = substr('000000', 0, 6 - mb_strlen($hex, '8bit')) . $hex;
2945 }
2946 $tmp .= chr(hexdec(substr($hex, 4, 2)))
2947 . chr(hexdec(substr($hex, 2, 2)))
2948 . chr(hexdec(substr($hex, 0, 2)))
2949 . chr(0)
2950 . chr(0)
2951 ;
2952 $key = $this->md5_16($tmp);
2953 $this->ARC4_init(substr($key, 0, 10));
2954 }
2955
2956 /**
2957 * initialize the ARC4 encryption
2958 *
2959 * @param string $key
2960 */
2961 function ARC4_init($key = '')
2962 {
2963 $this->arc4 = '';
2964
2965 // setup the control array
2966 if (mb_strlen($key, '8bit') == 0) {
2967 return;
2968 }
2969
2970 $k = '';
2971 while (mb_strlen($k, '8bit') < 256) {
2972 $k .= $key;
2973 }
2974
2975 $k = substr($k, 0, 256);
2976 for ($i = 0; $i < 256; $i++) {
2977 $this->arc4 .= chr($i);
2978 }
2979
2980 $j = 0;
2981
2982 for ($i = 0; $i < 256; $i++) {
2983 $t = $this->arc4[$i];
2984 $j = ($j + ord($t) + ord($k[$i])) % 256;
2985 $this->arc4[$i] = $this->arc4[$j];
2986 $this->arc4[$j] = $t;
2987 }
2988 }
2989
2990 /**
2991 * ARC4 encrypt a text string
2992 *
2993 * @param $text
2994 * @return string
2995 */
2996 function ARC4($text)
2997 {
2998 $len = mb_strlen($text, '8bit');
2999 $a = 0;
3000 $b = 0;
3001 $c = $this->arc4;
3002 $out = '';
3003 for ($i = 0; $i < $len; $i++) {
3004 $a = ($a + 1) % 256;
3005 $t = $c[$a];
3006 $b = ($b + ord($t)) % 256;
3007 $c[$a] = $c[$b];
3008 $c[$b] = $t;
3009 $k = ord($c[(ord($c[$a]) + ord($c[$b])) % 256]);
3010 $out .= chr(ord($text[$i]) ^ $k);
3011 }
3012
3013 return $out;
3014 }
3015
3016 /**
3017 * functions which can be called to adjust or add to the document
3018 */
3019
3020 /**
3021 * add a link in the document to an external URL
3022 *
3023 * @param $url
3024 * @param $x0
3025 * @param $y0
3026 * @param $x1
3027 * @param $y1
3028 */
3029 function addLink($url, $x0, $y0, $x1, $y1)
3030 {
3031 $this->numObj++;
3032 $info = ['type' => 'link', 'url' => $url, 'rect' => [$x0, $y0, $x1, $y1]];
3033 $this->o_annotation($this->numObj, 'new', $info);
3034 }
3035
3036 /**
3037 * add a link in the document to an internal destination (ie. within the document)
3038 *
3039 * @param $label
3040 * @param $x0
3041 * @param $y0
3042 * @param $x1
3043 * @param $y1
3044 */
3045 function addInternalLink($label, $x0, $y0, $x1, $y1)
3046 {
3047 $this->numObj++;
3048 $info = ['type' => 'ilink', 'label' => $label, 'rect' => [$x0, $y0, $x1, $y1]];
3049 $this->o_annotation($this->numObj, 'new', $info);
3050 }
3051
3052 /**
3053 * set the encryption of the document
3054 * can be used to turn it on and/or set the passwords which it will have.
3055 * also the functions that the user will have are set here, such as print, modify, add
3056 *
3057 * @param string $userPass
3058 * @param string $ownerPass
3059 * @param array $pc
3060 */
3061 function setEncryption($userPass = '', $ownerPass = '', $pc = [])
3062 {
3063 $p = bindec("11000000");
3064
3065 $options = ['print' => 4, 'modify' => 8, 'copy' => 16, 'add' => 32];
3066
3067 foreach ($pc as $k => $v) {
3068 if ($v && isset($options[$k])) {
3069 $p += $options[$k];
3070 } else {
3071 if (isset($options[$v])) {
3072 $p += $options[$v];
3073 }
3074 }
3075 }
3076
3077 // implement encryption on the document
3078 if ($this->arc4_objnum == 0) {
3079 // then the block does not exist already, add it.
3080 $this->numObj++;
3081 if (mb_strlen($ownerPass) == 0) {
3082 $ownerPass = $userPass;
3083 }
3084
3085 $this->o_encryption($this->numObj, 'new', ['user' => $userPass, 'owner' => $ownerPass, 'p' => $p]);
3086 }
3087 }
3088
3089 /**
3090 * should be used for internal checks, not implemented as yet
3091 */
3092 function checkAllHere()
3093 {
3094 }
3095
3096 /**
3097 * return the pdf stream as a string returned from the function
3098 *
3099 * @param bool $debug
3100 * @return string
3101 */
3102 function output($debug = false)
3103 {
3104 if ($debug) {
3105 // turn compression off
3106 $this->options['compression'] = false;
3107 }
3108
3109 if ($this->javascript) {
3110 $this->numObj++;
3111
3112 $js_id = $this->numObj;
3113 $this->o_embedjs($js_id, 'new');
3114 $this->o_javascript(++$this->numObj, 'new', $this->javascript);
3115
3116 $id = $this->catalogId;
3117
3118 $this->o_indirect_references($this->indirectReferenceId, 'add', ['JavaScript' => $js_id]);
3119 }
3120
3121 if ($this->fileIdentifier === '') {
3122 $tmp = implode('', $this->objects[$this->infoObject]['info']);
3123 $this->fileIdentifier = md5('DOMPDF' . __FILE__ . $tmp . microtime() . mt_rand());
3124 }
3125
3126 if ($this->arc4_objnum) {
3127 $this->o_encryption($this->arc4_objnum, 'keys');
3128 $this->ARC4_init($this->encryptionKey);
3129 }
3130
3131 $this->checkAllHere();
3132
3133 $xref = [];
3134 $content = '%PDF-' . self::PDF_VERSION;
3135 $pos = mb_strlen($content, '8bit');
3136
3137 // pre-process o_font objects before output of all objects
3138 foreach ($this->objects as $k => $v) {
3139 if ($v['t'] === 'font') {
3140 $this->o_font($k, 'add');
3141 }
3142 }
3143
3144 foreach ($this->objects as $k => $v) {
3145 $tmp = 'o_' . $v['t'];
3146 $cont = $this->$tmp($k, 'out');
3147 $content .= $cont;
3148 $xref[] = $pos + 1; //+1 to account for \n at the start of each object
3149 $pos += mb_strlen($cont, '8bit');
3150 }
3151
3152 $content .= "\nxref\n0 " . (count($xref) + 1) . "\n0000000000 65535 f \n";
3153
3154 foreach ($xref as $p) {
3155 $content .= str_pad($p, 10, "0", STR_PAD_LEFT) . " 00000 n \n";
3156 }
3157
3158 $content .= "trailer\n<<\n" .
3159 '/Size ' . (count($xref) + 1) . "\n" .
3160 '/Root 1 0 R' . "\n" .
3161 '/Info ' . $this->infoObject . " 0 R\n"
3162 ;
3163
3164 // if encryption has been applied to this document then add the marker for this dictionary
3165 if ($this->arc4_objnum > 0) {
3166 $content .= '/Encrypt ' . $this->arc4_objnum . " 0 R\n";
3167 }
3168
3169 $content .= '/ID[<' . $this->fileIdentifier . '><' . $this->fileIdentifier . ">]\n";
3170
3171 // account for \n added at start of xref table
3172 $pos++;
3173
3174 $content .= ">>\nstartxref\n$pos\n%%EOF\n";
3175
3176 if (count($this->byteRange) > 0) {
3177 foreach ($this->byteRange as $k => $v) {
3178 $tmp = 'o_' . $v['t'];
3179 $this->$tmp($k, 'byterange', ['content' => &$content]);
3180 }
3181 }
3182
3183 return $content;
3184 }
3185
3186 /**
3187 * initialize a new document
3188 * if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum
3189 * this function is called automatically by the constructor function
3190 *
3191 * @param array $pageSize
3192 */
3193 private function newDocument($pageSize = [0, 0, 612, 792])
3194 {
3195 $this->numObj = 0;
3196 $this->objects = [];
3197
3198 $this->numObj++;
3199 $this->o_catalog($this->numObj, 'new');
3200
3201 $this->numObj++;
3202 $this->o_outlines($this->numObj, 'new');
3203
3204 $this->numObj++;
3205 $this->o_pages($this->numObj, 'new');
3206
3207 $this->o_pages($this->numObj, 'mediaBox', $pageSize);
3208 $this->currentNode = 3;
3209
3210 $this->numObj++;
3211 $this->o_procset($this->numObj, 'new');
3212
3213 $this->numObj++;
3214 $this->o_info($this->numObj, 'new');
3215
3216 $this->numObj++;
3217 $this->o_page($this->numObj, 'new');
3218
3219 // need to store the first page id as there is no way to get it to the user during
3220 // startup
3221 $this->firstPageId = $this->currentContents;
3222 }
3223
3224 /**
3225 * open the font file and return a php structure containing it.
3226 * first check if this one has been done before and saved in a form more suited to php
3227 * note that if a php serialized version does not exist it will try and make one, but will
3228 * require write access to the directory to do it... it is MUCH faster to have these serialized
3229 * files.
3230 *
3231 * @param $font
3232 */
3233 private function openFont($font)
3234 {
3235 // assume that $font contains the path and file but not the extension
3236 $name = basename($font);
3237 $dir = dirname($font);
3238
3239 $fontcache = $this->fontcache;
3240 if ($fontcache == '') {
3241 $fontcache = $dir;
3242 }
3243
3244 //$name filename without folder and extension of font metrics
3245 //$dir folder of font metrics
3246 //$fontcache folder of runtime created php serialized version of font metrics.
3247 // If this is not given, the same folder as the font metrics will be used.
3248 // Storing and reusing serialized versions improves speed much
3249
3250 $this->addMessage("openFont: $font - $name");
3251
3252 if (!$this->isUnicode || in_array(mb_strtolower(basename($name)), self::$coreFonts)) {
3253 $metrics_name = "$name.afm";
3254 } else {
3255 $metrics_name = "$name.ufm";
3256 }
3257
3258 $cache_name = "$metrics_name.json";
3259 $this->addMessage("metrics: $metrics_name, cache: $cache_name");
3260
3261 if (file_exists($fontcache . '/' . $cache_name)) {
3262 $this->addMessage("openFont: json metrics file exists $fontcache/$cache_name");
3263 $cached_font_info = json_decode(file_get_contents($fontcache . '/' . $cache_name), true);
3264 if (!isset($cached_font_info['_version_']) || $cached_font_info['_version_'] != $this->fontcacheVersion) {
3265 $this->addMessage('openFont: font cache is out of date, regenerating');
3266 } else {
3267 $this->fonts[$font] = $cached_font_info;
3268 }
3269 }
3270
3271 if (!isset($this->fonts[$font]) && file_exists("$dir/$metrics_name")) {
3272 // then rebuild the php_<font>.afm file from the <font>.afm file
3273 $this->addMessage("openFont: build php file from $dir/$metrics_name");
3274 $data = [];
3275
3276 // 20 => 'space'
3277 $data['codeToName'] = [];
3278
3279 // Since we're not going to enable Unicode for the core fonts we need to use a font-based
3280 // setting for Unicode support rather than a global setting.
3281 $data['isUnicode'] = (strtolower(substr($metrics_name, -3)) !== 'afm');
3282
3283 $cidtogid = '';
3284 if ($data['isUnicode']) {
3285 $cidtogid = str_pad('', 256 * 256 * 2, "\x00");
3286 }
3287
3288 $file = file("$dir/$metrics_name");
3289
3290 foreach ($file as $rowA) {
3291 $row = trim($rowA);
3292 $pos = strpos($row, ' ');
3293
3294 if ($pos) {
3295 // then there must be some keyword
3296 $key = substr($row, 0, $pos);
3297 switch ($key) {
3298 case 'FontName':
3299 case 'FullName':
3300 case 'FamilyName':
3301 case 'PostScriptName':
3302 case 'Weight':
3303 case 'ItalicAngle':
3304 case 'IsFixedPitch':
3305 case 'CharacterSet':
3306 case 'UnderlinePosition':
3307 case 'UnderlineThickness':
3308 case 'Version':
3309 case 'EncodingScheme':
3310 case 'CapHeight':
3311 case 'XHeight':
3312 case 'Ascender':
3313 case 'Descender':
3314 case 'StdHW':
3315 case 'StdVW':
3316 case 'StartCharMetrics':
3317 case 'FontHeightOffset': // OAR - Added so we can offset the height calculation of a Windows font. Otherwise it's too big.
3318 $data[$key] = trim(substr($row, $pos));
3319 break;
3320
3321 case 'FontBBox':
3322 $data[$key] = explode(' ', trim(substr($row, $pos)));
3323 break;
3324
3325 //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ;
3326 case 'C': // Found in AFM files
3327 $bits = explode(';', trim($row));
3328 $dtmp = ['C' => null, 'N' => null, 'WX' => null, 'B' => []];
3329
3330 foreach ($bits as $bit) {
3331 $bits2 = explode(' ', trim($bit));
3332 if (mb_strlen($bits2[0], '8bit') == 0) {
3333 continue;
3334 }
3335
3336 if (count($bits2) > 2) {
3337 $dtmp[$bits2[0]] = [];
3338 for ($i = 1; $i < count($bits2); $i++) {
3339 $dtmp[$bits2[0]][] = $bits2[$i];
3340 }
3341 } else {
3342 if (count($bits2) == 2) {
3343 $dtmp[$bits2[0]] = $bits2[1];
3344 }
3345 }
3346 }
3347
3348 $c = (int)$dtmp['C'];
3349 $n = $dtmp['N'];
3350 $width = floatval($dtmp['WX']);
3351
3352 if ($c >= 0) {
3353 if (!ctype_xdigit($n) || $c != hexdec($n)) {
3354 $data['codeToName'][$c] = $n;
3355 }
3356 $data['C'][$c] = $width;
3357 } elseif (isset($n)) {
3358 $data['C'][$n] = $width;
3359 }
3360
3361 if (!isset($data['MissingWidth']) && $c === -1 && $n === '.notdef') {
3362 $data['MissingWidth'] = $width;
3363 }
3364
3365 break;
3366
3367 // U 827 ; WX 0 ; N squaresubnosp ; G 675 ;
3368 case 'U': // Found in UFM files
3369 if (!$data['isUnicode']) {
3370 break;
3371 }
3372
3373 $bits = explode(';', trim($row));
3374 $dtmp = ['G' => null, 'N' => null, 'U' => null, 'WX' => null];
3375
3376 foreach ($bits as $bit) {
3377 $bits2 = explode(' ', trim($bit));
3378 if (mb_strlen($bits2[0], '8bit') === 0) {
3379 continue;
3380 }
3381
3382 if (count($bits2) > 2) {
3383 $dtmp[$bits2[0]] = [];
3384 for ($i = 1; $i < count($bits2); $i++) {
3385 $dtmp[$bits2[0]][] = $bits2[$i];
3386 }
3387 } else {
3388 if (count($bits2) == 2) {
3389 $dtmp[$bits2[0]] = $bits2[1];
3390 }
3391 }
3392 }
3393
3394 $c = (int)$dtmp['U'];
3395 $n = $dtmp['N'];
3396 $glyph = $dtmp['G'];
3397 $width = floatval($dtmp['WX']);
3398
3399 if ($c >= 0) {
3400 // Set values in CID to GID map
3401 if ($c >= 0 && $c < 0xFFFF && $glyph) {
3402 $cidtogid[$c * 2] = chr($glyph >> 8);
3403 $cidtogid[$c * 2 + 1] = chr($glyph & 0xFF);
3404 }
3405
3406 if (!ctype_xdigit($n) || $c != hexdec($n)) {
3407 $data['codeToName'][$c] = $n;
3408 }
3409 $data['C'][$c] = $width;
3410 } elseif (isset($n)) {
3411 $data['C'][$n] = $width;
3412 }
3413
3414 if (!isset($data['MissingWidth']) && $c === -1 && $n === '.notdef') {
3415 $data['MissingWidth'] = $width;
3416 }
3417
3418 break;
3419
3420 case 'KPX':
3421 break; // don't include them as they are not used yet
3422 //KPX Adieresis yacute -40
3423 /*$bits = explode(' ', trim($row));
3424 $data['KPX'][$bits[1]][$bits[2]] = $bits[3];
3425 break;*/
3426 }
3427 }
3428 }
3429
3430 if ($this->compressionReady && $this->options['compression']) {
3431 // then implement ZLIB based compression on CIDtoGID string
3432 $data['CIDtoGID_Compressed'] = true;
3433 $cidtogid = gzcompress($cidtogid, 6);
3434 }
3435 $data['CIDtoGID'] = base64_encode($cidtogid);
3436 $data['_version_'] = $this->fontcacheVersion;
3437 $this->fonts[$font] = $data;
3438
3439 //Because of potential trouble with php safe mode, expect that the folder already exists.
3440 //If not existing, this will hit performance because of missing cached results.
3441 if (is_dir($fontcache) && is_writable($fontcache)) {
3442 file_put_contents("$fontcache/$cache_name", json_encode($data, JSON_PRETTY_PRINT));
3443 }
3444 $data = null;
3445 }
3446
3447 if (!isset($this->fonts[$font])) {
3448 $this->addMessage("openFont: no font file found for $font. Do you need to run load_font.php?");
3449 }
3450 }
3451
3452 /**
3453 * if the font is not loaded then load it and make the required object
3454 * else just make it the current font
3455 * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding'
3456 * note that encoding='none' will need to be used for symbolic fonts
3457 * and 'differences' => an array of mappings between numbers 0->255 and character names.
3458 *
3459 * @param string $fontName
3460 * @param string $encoding
3461 * @param bool $set
3462 * @param bool $isSubsetting
3463 * @return int
3464 * @throws FontNotFoundException
3465 */
3466 function selectFont($fontName, $encoding = '', $set = true, $isSubsetting = true)
3467 {
3468 if ($fontName === null || $fontName === '') {
3469 return $this->currentFontNum;
3470 }
3471
3472 $ext = substr($fontName, -4);
3473 if ($ext === '.afm' || $ext === '.ufm') {
3474 $fontName = substr($fontName, 0, mb_strlen($fontName) - 4);
3475 }
3476
3477 if (!isset($this->fonts[$fontName])) {
3478 $this->addMessage("selectFont: selecting - $fontName - $encoding, $set");
3479
3480 // load the file
3481 $this->openFont($fontName);
3482
3483 if (isset($this->fonts[$fontName])) {
3484 $this->numObj++;
3485 $this->numFonts++;
3486
3487 $font = &$this->fonts[$fontName];
3488
3489 $name = basename($fontName);
3490 $options = ['name' => $name, 'fontFileName' => $fontName, 'isSubsetting' => $isSubsetting];
3491
3492 if (is_array($encoding)) {
3493 // then encoding and differences might be set
3494 if (isset($encoding['encoding'])) {
3495 $options['encoding'] = $encoding['encoding'];
3496 }
3497
3498 if (isset($encoding['differences'])) {
3499 $options['differences'] = $encoding['differences'];
3500 }
3501 } else {
3502 if (mb_strlen($encoding, '8bit')) {
3503 // then perhaps only the encoding has been set
3504 $options['encoding'] = $encoding;
3505 }
3506 }
3507
3508 $this->o_font($this->numObj, 'new', $options);
3509
3510 if (file_exists("$fontName.ttf")) {
3511 $fileSuffix = 'ttf';
3512 } elseif (file_exists("$fontName.TTF")) {
3513 $fileSuffix = 'TTF';
3514 } elseif (file_exists("$fontName.pfb")) {
3515 $fileSuffix = 'pfb';
3516 } elseif (file_exists("$fontName.PFB")) {
3517 $fileSuffix = 'PFB';
3518 } else {
3519 $fileSuffix = '';
3520 }
3521
3522 $font['fileSuffix'] = $fileSuffix;
3523
3524 $font['fontNum'] = $this->numFonts;
3525 $font['isSubsetting'] = $isSubsetting && $font['isUnicode'] && strtolower($fileSuffix) === 'ttf';
3526
3527 // also set the differences here, note that this means that these will take effect only the
3528 //first time that a font is selected, else they are ignored
3529 if (isset($options['differences'])) {
3530 $font['differences'] = $options['differences'];
3531 }
3532 }
3533 }
3534
3535 if ($set && isset($this->fonts[$fontName])) {
3536 // so if for some reason the font was not set in the last one then it will not be selected
3537 $this->currentBaseFont = $fontName;
3538
3539 // the next lines mean that if a new font is selected, then the current text state will be
3540 // applied to it as well.
3541 $this->currentFont = $this->currentBaseFont;
3542 $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum'];
3543 }
3544
3545 return $this->currentFontNum;
3546 }
3547
3548 /**
3549 * sets up the current font, based on the font families, and the current text state
3550 * note that this system is quite flexible, a bold-italic font can be completely different to a
3551 * italic-bold font, and even bold-bold will have to be defined within the family to have meaning
3552 * This function is to be called whenever the currentTextState is changed, it will update
3553 * the currentFont setting to whatever the appropriate family one is.
3554 * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont
3555 * This function will change the currentFont to whatever it should be, but will not change the
3556 * currentBaseFont.
3557 */
3558 private function setCurrentFont()
3559 {
3560 // if (strlen($this->currentBaseFont) == 0){
3561 // // then assume an initial font
3562 // $this->selectFont($this->defaultFont);
3563 // }
3564 // $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1);
3565 // if (strlen($this->currentTextState)
3566 // && isset($this->fontFamilies[$cf])
3567 // && isset($this->fontFamilies[$cf][$this->currentTextState])){
3568 // // then we are in some state or another
3569 // // and this font has a family, and the current setting exists within it
3570 // // select the font, then return it
3571 // $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState];
3572 // $this->selectFont($nf,'',0);
3573 // $this->currentFont = $nf;
3574 // $this->currentFontNum = $this->fonts[$nf]['fontNum'];
3575 // } else {
3576 // // the this font must not have the right family member for the current state
3577 // // simply assume the base font
3578 $this->currentFont = $this->currentBaseFont;
3579 $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum'];
3580 // }
3581 }
3582
3583 /**
3584 * function for the user to find out what the ID is of the first page that was created during
3585 * startup - useful if they wish to add something to it later.
3586 *
3587 * @return int
3588 */
3589 function getFirstPageId()
3590 {
3591 return $this->firstPageId;
3592 }
3593
3594 /**
3595 * add content to the currently active object
3596 *
3597 * @param $content
3598 */
3599 private function addContent($content)
3600 {
3601 $this->objects[$this->currentContents]['c'] .= $content;
3602 }
3603
3604 /**
3605 * sets the color for fill operations
3606 *
3607 * @param array $color
3608 * @param bool $force
3609 */
3610 function setColor($color, $force = false)
3611 {
3612 $new_color = [$color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null];
3613
3614 if (!$force && $this->currentColor == $new_color) {
3615 return;
3616 }
3617
3618 if (isset($new_color[3])) {
3619 $this->currentColor = $new_color;
3620 $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F k", $this->currentColor));
3621 } else {
3622 if (isset($new_color[2])) {
3623 $this->currentColor = $new_color;
3624 $this->addContent(vsprintf("\n%.3F %.3F %.3F rg", $this->currentColor));
3625 }
3626 }
3627 }
3628
3629 /**
3630 * sets the color for fill operations
3631 *
3632 * @param string $fillRule
3633 */
3634 function setFillRule($fillRule)
3635 {
3636 if (!in_array($fillRule, ["nonzero", "evenodd"])) {
3637 return;
3638 }
3639
3640 $this->fillRule = $fillRule;
3641 }
3642
3643 /**
3644 * sets the color for stroke operations
3645 *
3646 * @param array $color
3647 * @param bool $force
3648 */
3649 function setStrokeColor($color, $force = false)
3650 {
3651 $new_color = [$color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null];
3652
3653 if (!$force && $this->currentStrokeColor == $new_color) {
3654 return;
3655 }
3656
3657 if (isset($new_color[3])) {
3658 $this->currentStrokeColor = $new_color;
3659 $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F K", $this->currentStrokeColor));
3660 } else {
3661 if (isset($new_color[2])) {
3662 $this->currentStrokeColor = $new_color;
3663 $this->addContent(vsprintf("\n%.3F %.3F %.3F RG", $this->currentStrokeColor));
3664 }
3665 }
3666 }
3667
3668 /**
3669 * Set the graphics state for compositions
3670 *
3671 * @param $parameters
3672 */
3673 function setGraphicsState($parameters)
3674 {
3675 // Create a new graphics state object if necessary
3676 if (($gstate = array_search($parameters, $this->gstates)) === false) {
3677 $this->numObj++;
3678 $this->o_extGState($this->numObj, 'new', $parameters);
3679 $gstate = $this->numStates;
3680 $this->gstates[$gstate] = $parameters;
3681 }
3682 $this->addContent("\n/GS$gstate gs");
3683 }
3684
3685 /**
3686 * Set current blend mode & opacity for lines.
3687 *
3688 * Valid blend modes are:
3689 *
3690 * Normal, Multiply, Screen, Overlay, Darken, Lighten,
3691 * ColorDogde, ColorBurn, HardLight, SoftLight, Difference,
3692 * Exclusion
3693 *
3694 * @param string $mode the blend mode to use
3695 * @param float $opacity 0.0 fully transparent, 1.0 fully opaque
3696 */
3697 function setLineTransparency($mode, $opacity)
3698 {
3699 static $blend_modes = [
3700 "Normal",
3701 "Multiply",
3702 "Screen",
3703 "Overlay",
3704 "Darken",
3705 "Lighten",
3706 "ColorDogde",
3707 "ColorBurn",
3708 "HardLight",
3709 "SoftLight",
3710 "Difference",
3711 "Exclusion"
3712 ];
3713
3714 if (!in_array($mode, $blend_modes)) {
3715 $mode = "Normal";
3716 }
3717
3718 if (is_null($this->currentLineTransparency)) {
3719 $this->currentLineTransparency = [];
3720 }
3721
3722 if ($mode === (key_exists('mode', $this->currentLineTransparency) ?
3723 $this->currentLineTransparency['mode'] : '') &&
3724 $opacity === (key_exists('opacity', $this->currentLineTransparency) ?
3725 $this->currentLineTransparency["opacity"] : '')) {
3726 return;
3727 }
3728
3729 $this->currentLineTransparency["mode"] = $mode;
3730 $this->currentLineTransparency["opacity"] = $opacity;
3731
3732 $options = [
3733 "BM" => "/$mode",
3734 "CA" => (float)$opacity
3735 ];
3736
3737 $this->setGraphicsState($options);
3738 }
3739
3740 /**
3741 * Set current blend mode & opacity for filled objects.
3742 *
3743 * Valid blend modes are:
3744 *
3745 * Normal, Multiply, Screen, Overlay, Darken, Lighten,
3746 * ColorDogde, ColorBurn, HardLight, SoftLight, Difference,
3747 * Exclusion
3748 *
3749 * @param string $mode the blend mode to use
3750 * @param float $opacity 0.0 fully transparent, 1.0 fully opaque
3751 */
3752 function setFillTransparency($mode, $opacity)
3753 {
3754 static $blend_modes = [
3755 "Normal",
3756 "Multiply",
3757 "Screen",
3758 "Overlay",
3759 "Darken",
3760 "Lighten",
3761 "ColorDogde",
3762 "ColorBurn",
3763 "HardLight",
3764 "SoftLight",
3765 "Difference",
3766 "Exclusion"
3767 ];
3768
3769 if (!in_array($mode, $blend_modes)) {
3770 $mode = "Normal";
3771 }
3772
3773 if (is_null($this->currentFillTransparency)) {
3774 $this->currentFillTransparency = [];
3775 }
3776
3777 if ($mode === (key_exists('mode', $this->currentFillTransparency) ?
3778 $this->currentFillTransparency['mode'] : '') &&
3779 $opacity === (key_exists('opacity', $this->currentFillTransparency) ?
3780 $this->currentFillTransparency["opacity"] : '')) {
3781 return;
3782 }
3783
3784 $this->currentFillTransparency["mode"] = $mode;
3785 $this->currentFillTransparency["opacity"] = $opacity;
3786
3787 $options = [
3788 "BM" => "/$mode",
3789 "ca" => (float)$opacity,
3790 ];
3791
3792 $this->setGraphicsState($options);
3793 }
3794
3795 /**
3796 * draw a line from one set of coordinates to another
3797 *
3798 * @param float $x1
3799 * @param float $y1
3800 * @param float $x2
3801 * @param float $y2
3802 * @param bool $stroke
3803 */
3804 function line($x1, $y1, $x2, $y2, $stroke = true)
3805 {
3806 $this->addContent(sprintf("\n%.3F %.3F m %.3F %.3F l", $x1, $y1, $x2, $y2));
3807
3808 if ($stroke) {
3809 $this->addContent(' S');
3810 }
3811 }
3812
3813 /**
3814 * draw a bezier curve based on 4 control points
3815 *
3816 * @param float $x0
3817 * @param float $y0
3818 * @param float $x1
3819 * @param float $y1
3820 * @param float $x2
3821 * @param float $y2
3822 * @param float $x3
3823 * @param float $y3
3824 */
3825 function curve($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
3826 {
3827 // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points
3828 // as the control points for the curve.
3829 $this->addContent(
3830 sprintf("\n%.3F %.3F m %.3F %.3F %.3F %.3F %.3F %.3F c S", $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
3831 );
3832 }
3833
3834 /**
3835 * draw a part of an ellipse
3836 *
3837 * @param float $x0
3838 * @param float $y0
3839 * @param float $astart
3840 * @param float $afinish
3841 * @param float $r1
3842 * @param float $r2
3843 * @param float $angle
3844 * @param int $nSeg
3845 */
3846 function partEllipse($x0, $y0, $astart, $afinish, $r1, $r2 = 0, $angle = 0, $nSeg = 8)
3847 {
3848 $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, false);
3849 }
3850
3851 /**
3852 * draw a filled ellipse
3853 *
3854 * @param float $x0
3855 * @param float $y0
3856 * @param float $r1
3857 * @param float $r2
3858 * @param float $angle
3859 * @param int $nSeg
3860 * @param float $astart
3861 * @param float $afinish
3862 */
3863 function filledEllipse($x0, $y0, $r1, $r2 = 0, $angle = 0, $nSeg = 8, $astart = 0, $afinish = 360)
3864 {
3865 $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, true, true);
3866 }
3867
3868 /**
3869 * @param float $x
3870 * @param float $y
3871 */
3872 function lineTo($x, $y)
3873 {
3874 $this->addContent(sprintf("\n%.3F %.3F l", $x, $y));
3875 }
3876
3877 /**
3878 * @param float $x
3879 * @param float $y
3880 */
3881 function moveTo($x, $y)
3882 {
3883 $this->addContent(sprintf("\n%.3F %.3F m", $x, $y));
3884 }
3885
3886 /**
3887 * draw a bezier curve based on 4 control points
3888 *
3889 * @param float $x1
3890 * @param float $y1
3891 * @param float $x2
3892 * @param float $y2
3893 * @param float $x3
3894 * @param float $y3
3895 */
3896 function curveTo($x1, $y1, $x2, $y2, $x3, $y3)
3897 {
3898 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F %.3F %.3F c", $x1, $y1, $x2, $y2, $x3, $y3));
3899 }
3900
3901 /**
3902 * draw a bezier curve based on 4 control points
3903 *
3904 * @param float $cpx
3905 * @param float $cpy
3906 * @param float $x
3907 * @param float $y
3908 */
3909 function quadTo($cpx, $cpy, $x, $y)
3910 {
3911 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F v", $cpx, $cpy, $x, $y));
3912 }
3913
3914 function closePath()
3915 {
3916 $this->addContent(' h');
3917 }
3918
3919 function endPath()
3920 {
3921 $this->addContent(' n');
3922 }
3923
3924 /**
3925 * draw an ellipse
3926 * note that the part and filled ellipse are just special cases of this function
3927 *
3928 * draws an ellipse in the current line style
3929 * centered at $x0,$y0, radii $r1,$r2
3930 * if $r2 is not set, then a circle is drawn
3931 * from $astart to $afinish, measured in degrees, running anti-clockwise from the right hand side of the ellipse.
3932 * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a
3933 * pretty crappy shape at 2, as we are approximating with bezier curves.
3934 *
3935 * @param float $x0
3936 * @param float $y0
3937 * @param float $r1
3938 * @param float $r2
3939 * @param float $angle
3940 * @param int $nSeg
3941 * @param float $astart
3942 * @param float $afinish
3943 * @param bool $close
3944 * @param bool $fill
3945 * @param bool $stroke
3946 * @param bool $incomplete
3947 */
3948 function ellipse(
3949 $x0,
3950 $y0,
3951 $r1,
3952 $r2 = 0,
3953 $angle = 0,
3954 $nSeg = 8,
3955 $astart = 0,
3956 $afinish = 360,
3957 $close = true,
3958 $fill = false,
3959 $stroke = true,
3960 $incomplete = false
3961 ) {
3962 if ($r1 == 0) {
3963 return;
3964 }
3965
3966 if ($r2 == 0) {
3967 $r2 = $r1;
3968 }
3969
3970 if ($nSeg < 2) {
3971 $nSeg = 2;
3972 }
3973
3974 $astart = deg2rad((float)$astart);
3975 $afinish = deg2rad((float)$afinish);
3976 $totalAngle = $afinish - $astart;
3977
3978 $dt = $totalAngle / $nSeg;
3979 $dtm = $dt / 3;
3980
3981 if ($angle != 0) {
3982 $a = -1 * deg2rad((float)$angle);
3983
3984 $this->addContent(
3985 sprintf("\n q %.3F %.3F %.3F %.3F %.3F %.3F cm", cos($a), -sin($a), sin($a), cos($a), $x0, $y0)
3986 );
3987
3988 $x0 = 0;
3989 $y0 = 0;
3990 }
3991
3992 $t1 = $astart;
3993 $a0 = $x0 + $r1 * cos($t1);
3994 $b0 = $y0 + $r2 * sin($t1);
3995 $c0 = -$r1 * sin($t1);
3996 $d0 = $r2 * cos($t1);
3997
3998 if (!$incomplete) {
3999 $this->addContent(sprintf("\n%.3F %.3F m ", $a0, $b0));
4000 }
4001
4002 for ($i = 1; $i <= $nSeg; $i++) {
4003 // draw this bit of the total curve
4004 $t1 = $i * $dt + $astart;
4005 $a1 = $x0 + $r1 * cos($t1);
4006 $b1 = $y0 + $r2 * sin($t1);
4007 $c1 = -$r1 * sin($t1);
4008 $d1 = $r2 * cos($t1);
4009
4010 $this->addContent(
4011 sprintf(
4012 "\n%.3F %.3F %.3F %.3F %.3F %.3F c",
4013 ($a0 + $c0 * $dtm),
4014 ($b0 + $d0 * $dtm),
4015 ($a1 - $c1 * $dtm),
4016 ($b1 - $d1 * $dtm),
4017 $a1,
4018 $b1
4019 )
4020 );
4021
4022 $a0 = $a1;
4023 $b0 = $b1;
4024 $c0 = $c1;
4025 $d0 = $d1;
4026 }
4027
4028 if (!$incomplete) {
4029 if ($fill) {
4030 $this->addContent(' f');
4031 }
4032
4033 if ($stroke) {
4034 if ($close) {
4035 $this->addContent(' s'); // small 's' signifies closing the path as well
4036 } else {
4037 $this->addContent(' S');
4038 }
4039 }
4040 }
4041
4042 if ($angle != 0) {
4043 $this->addContent(' Q');
4044 }
4045 }
4046
4047 /**
4048 * this sets the line drawing style.
4049 * width, is the thickness of the line in user units
4050 * cap is the type of cap to put on the line, values can be 'butt','round','square'
4051 * where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the
4052 * end of the line.
4053 * join can be 'miter', 'round', 'bevel'
4054 * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the
4055 * on and off dashes.
4056 * (2) represents 2 on, 2 off, 2 on , 2 off ...
4057 * (2,1) is 2 on, 1 off, 2 on, 1 off.. etc
4058 * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts.
4059 *
4060 * @param float $width
4061 * @param string $cap
4062 * @param string $join
4063 * @param array $dash
4064 * @param int $phase
4065 */
4066 function setLineStyle($width = 1, $cap = '', $join = '', $dash = '', $phase = 0)
4067 {
4068 // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day
4069 $string = '';
4070
4071 if ($width > 0) {
4072 $string .= "$width w";
4073 }
4074
4075 $ca = ['butt' => 0, 'round' => 1, 'square' => 2];
4076
4077 if (isset($ca[$cap])) {
4078 $string .= " $ca[$cap] J";
4079 }
4080
4081 $ja = ['miter' => 0, 'round' => 1, 'bevel' => 2];
4082
4083 if (isset($ja[$join])) {
4084 $string .= " $ja[$join] j";
4085 }
4086
4087 if (is_array($dash)) {
4088 $string .= ' [ ' . implode(' ', $dash) . " ] $phase d";
4089 }
4090
4091 $this->currentLineStyle = $string;
4092 $this->addContent("\n$string");
4093 }
4094
4095 /**
4096 * draw a polygon, the syntax for this is similar to the GD polygon command
4097 *
4098 * @param float[] $p
4099 * @param bool $fill
4100 */
4101 public function polygon(array $p, bool $fill = false): void
4102 {
4103 $this->addContent(sprintf("\n%.3F %.3F m ", $p[0], $p[1]));
4104
4105 $n = count($p);
4106 for ($i = 2; $i < $n; $i = $i + 2) {
4107 $this->addContent(sprintf("%.3F %.3F l ", $p[$i], $p[$i + 1]));
4108 }
4109
4110 if ($fill) {
4111 $this->addContent(' f');
4112 } else {
4113 $this->addContent(' S');
4114 }
4115 }
4116
4117 /**
4118 * a filled rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not
4119 * the coordinates of the upper-right corner
4120 *
4121 * @param float $x1
4122 * @param float $y1
4123 * @param float $width
4124 * @param float $height
4125 */
4126 function filledRectangle($x1, $y1, $width, $height)
4127 {
4128 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re f", $x1, $y1, $width, $height));
4129 }
4130
4131 /**
4132 * draw a rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not
4133 * the coordinates of the upper-right corner
4134 *
4135 * @param float $x1
4136 * @param float $y1
4137 * @param float $width
4138 * @param float $height
4139 */
4140 function rectangle($x1, $y1, $width, $height)
4141 {
4142 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re S", $x1, $y1, $width, $height));
4143 }
4144
4145 /**
4146 * draw a rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not
4147 * the coordinates of the upper-right corner
4148 *
4149 * @param float $x1
4150 * @param float $y1
4151 * @param float $width
4152 * @param float $height
4153 */
4154 function rect($x1, $y1, $width, $height)
4155 {
4156 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re", $x1, $y1, $width, $height));
4157 }
4158
4159 function stroke(bool $close = false)
4160 {
4161 $this->addContent("\n" . ($close ? "s" : "S"));
4162 }
4163
4164 function fill()
4165 {
4166 $this->addContent("\nf" . ($this->fillRule === "evenodd" ? "*" : ""));
4167 }
4168
4169 function fillStroke(bool $close = false)
4170 {
4171 $this->addContent("\n" . ($close ? "b" : "B") . ($this->fillRule === "evenodd" ? "*" : ""));
4172 }
4173
4174 /**
4175 * @param string $subtype
4176 * @param integer $x
4177 * @param integer $y
4178 * @param integer $w
4179 * @param integer $h
4180 * @return int
4181 */
4182 function addXObject($subtype, $x, $y, $w, $h)
4183 {
4184 $id = ++$this->numObj;
4185 $this->o_xobject($id, 'new', ['Subtype' => $subtype, 'bbox' => [$x, $y, $w, $h]]);
4186 return $id;
4187 }
4188
4189 /**
4190 * @param integer $numXObject
4191 * @param string $type
4192 * @param array $options
4193 */
4194 function setXObjectResource($numXObject, $type, $options)
4195 {
4196 if (in_array($type, ['procset', 'font', 'xObject'])) {
4197 $this->o_xobject($numXObject, $type, $options);
4198 }
4199 }
4200
4201 /**
4202 * add signature
4203 *
4204 * $fieldSigId = $cpdf->addFormField(Cpdf::ACROFORM_FIELD_SIG, 'Signature1', 0, 0, 0, 0, 0);
4205 *
4206 * $signatureId = $cpdf->addSignature([
4207 * 'signcert' => file_get_contents('dompdf.crt'),
4208 * 'privkey' => file_get_contents('dompdf.key'),
4209 * 'password' => 'password',
4210 * 'name' => 'DomPDF DEMO',
4211 * 'location' => 'Home',
4212 * 'reason' => 'First Form',
4213 * 'contactinfo' => 'info'
4214 * ]);
4215 * $cpdf->setFormFieldValue($fieldSigId, "$signatureId 0 R");
4216 *
4217 * @param string $signcert
4218 * @param string $privkey
4219 * @param string $password
4220 * @param string|null $name
4221 * @param string|null $location
4222 * @param string|null $reason
4223 * @param string|null $contactinfo
4224 * @return int
4225 */
4226 function addSignature($signcert, $privkey, $password = '', $name = null, $location = null, $reason = null, $contactinfo = null) {
4227 $sigId = ++$this->numObj;
4228 $this->o_sig($sigId, 'new', [
4229 'SignCert' => $signcert,
4230 'PrivKey' => $privkey,
4231 'Password' => $password,
4232 'Name' => $name,
4233 'Location' => $location,
4234 'Reason' => $reason,
4235 'ContactInfo' => $contactinfo
4236 ]);
4237
4238 return $sigId;
4239 }
4240
4241 /**
4242 * add field to form
4243 *
4244 * @param string $type ACROFORM_FIELD_*
4245 * @param string $name
4246 * @param $x0
4247 * @param $y0
4248 * @param $x1
4249 * @param $y1
4250 * @param integer $ff Field Flag ACROFORM_FIELD_*_*
4251 * @param float $size
4252 * @param array $color
4253 * @return int
4254 */
4255 public function addFormField($type, $name, $x0, $y0, $x1, $y1, $ff = 0, $size = 10.0, $color = [0, 0, 0])
4256 {
4257 if (!$this->numFonts) {
4258 $this->selectFont($this->defaultFont);
4259 }
4260
4261 $color = implode(' ', $color) . ' rg';
4262
4263 $currentFontNum = $this->currentFontNum;
4264 $font = array_filter(
4265 $this->objects[$this->currentNode]['info']['fonts'],
4266 function ($item) use ($currentFontNum) { return $item['fontNum'] == $currentFontNum; }
4267 );
4268
4269 $this->o_acroform($this->acroFormId, 'font',
4270 ['objNum' => $font[0]['objNum'], 'fontNum' => $font[0]['fontNum']]);
4271
4272 $fieldId = ++$this->numObj;
4273 $this->o_field($fieldId, 'new', [
4274 'rect' => [$x0, $y0, $x1, $y1],
4275 'F' => 4,
4276 'FT' => "/$type",
4277 'T' => $name,
4278 'Ff' => $ff,
4279 'pageid' => $this->currentPage,
4280 'da' => "$color /F$this->currentFontNum " . sprintf('%.1F Tf ', $size)
4281 ]);
4282
4283 return $fieldId;
4284 }
4285
4286 /**
4287 * set Field value
4288 *
4289 * @param integer $numFieldObj
4290 * @param string $value
4291 */
4292 public function setFormFieldValue($numFieldObj, $value)
4293 {
4294 $this->o_field($numFieldObj, 'set', ['value' => $value]);
4295 }
4296
4297 /**
4298 * set Field value (reference)
4299 *
4300 * @param integer $numFieldObj
4301 * @param integer $numObj Object number
4302 */
4303 public function setFormFieldRefValue($numFieldObj, $numObj)
4304 {
4305 $this->o_field($numFieldObj, 'set', ['refvalue' => $numObj]);
4306 }
4307
4308 /**
4309 * set Field Appearanc (reference)
4310 *
4311 * @param integer $numFieldObj
4312 * @param integer $normalNumObj
4313 * @param integer|null $rolloverNumObj
4314 * @param integer|null $downNumObj
4315 */
4316 public function setFormFieldAppearance($numFieldObj, $normalNumObj, $rolloverNumObj = null, $downNumObj = null)
4317 {
4318 $appearance['N'] = $normalNumObj;
4319
4320 if ($rolloverNumObj !== null) {
4321 $appearance['R'] = $rolloverNumObj;
4322 }
4323
4324 if ($downNumObj !== null) {
4325 $appearance['D'] = $downNumObj;
4326 }
4327
4328 $this->o_field($numFieldObj, 'set', ['appearance' => $appearance]);
4329 }
4330
4331 /**
4332 * set Choice Field option values
4333 *
4334 * @param integer $numFieldObj
4335 * @param array $value
4336 */
4337 public function setFormFieldOpt($numFieldObj, $value)
4338 {
4339 $this->o_field($numFieldObj, 'set', ['options' => $value]);
4340 }
4341
4342 /**
4343 * add form to document
4344 *
4345 * @param integer $sigFlags
4346 * @param boolean $needAppearances
4347 */
4348 public function addForm($sigFlags = 0, $needAppearances = false)
4349 {
4350 $this->acroFormId = ++$this->numObj;
4351 $this->o_acroform($this->acroFormId, 'new', [
4352 'NeedAppearances' => $needAppearances ? 'true' : 'false',
4353 'SigFlags' => $sigFlags
4354 ]);
4355 }
4356
4357 /**
4358 * save the current graphic state
4359 */
4360 function save()
4361 {
4362 // we must reset the color cache or it will keep bad colors after clipping
4363 $this->currentColor = null;
4364 $this->currentStrokeColor = null;
4365 $this->addContent("\nq");
4366 }
4367
4368 /**
4369 * restore the last graphic state
4370 */
4371 function restore()
4372 {
4373 // we must reset the color cache or it will keep bad colors after clipping
4374 $this->currentColor = null;
4375 $this->currentStrokeColor = null;
4376 $this->addContent("\nQ");
4377 }
4378
4379 /**
4380 * draw a clipping rectangle, all the elements added after this will be clipped
4381 *
4382 * @param float $x1
4383 * @param float $y1
4384 * @param float $width
4385 * @param float $height
4386 */
4387 function clippingRectangle($x1, $y1, $width, $height)
4388 {
4389 $this->save();
4390 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re W n", $x1, $y1, $width, $height));
4391 }
4392
4393 /**
4394 * draw a clipping rounded rectangle, all the elements added after this will be clipped
4395 *
4396 * @param float $x1
4397 * @param float $y1
4398 * @param float $w
4399 * @param float $h
4400 * @param float $rTL
4401 * @param float $rTR
4402 * @param float $rBR
4403 * @param float $rBL
4404 */
4405 function clippingRectangleRounded($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL)
4406 {
4407 $this->save();
4408
4409 // start: top edge, left end
4410 $this->addContent(sprintf("\n%.3F %.3F m ", $x1, $y1 - $rTL + $h));
4411
4412 // line: bottom edge, left end
4413 $this->addContent(sprintf("\n%.3F %.3F l ", $x1, $y1 + $rBL));
4414
4415 // curve: bottom-left corner
4416 $this->ellipse($x1 + $rBL, $y1 + $rBL, $rBL, 0, 0, 8, 180, 270, false, false, false, true);
4417
4418 // line: right edge, bottom end
4419 $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w - $rBR, $y1));
4420
4421 // curve: bottom-right corner
4422 $this->ellipse($x1 + $w - $rBR, $y1 + $rBR, $rBR, 0, 0, 8, 270, 360, false, false, false, true);
4423
4424 // line: right edge, top end
4425 $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w, $y1 + $h - $rTR));
4426
4427 // curve: bottom-right corner
4428 $this->ellipse($x1 + $w - $rTR, $y1 + $h - $rTR, $rTR, 0, 0, 8, 0, 90, false, false, false, true);
4429
4430 // line: bottom edge, right end
4431 $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rTL, $y1 + $h));
4432
4433 // curve: top-right corner
4434 $this->ellipse($x1 + $rTL, $y1 + $h - $rTL, $rTL, 0, 0, 8, 90, 180, false, false, false, true);
4435
4436 // line: top edge, left end
4437 $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rBL, $y1));
4438
4439 // Close & clip
4440 $this->addContent(" W n");
4441 }
4442
4443 /**
4444 * draw a clipping polygon, the syntax for this is similar to the GD polygon command
4445 *
4446 * @param float[] $p
4447 */
4448 public function clippingPolygon(array $p): void
4449 {
4450 $this->save();
4451
4452 $this->addContent(sprintf("\n%.3F %.3F m ", $p[0], $p[1]));
4453
4454 $n = count($p);
4455 for ($i = 2; $i < $n; $i = $i + 2) {
4456 $this->addContent(sprintf("%.3F %.3F l ", $p[$i], $p[$i + 1]));
4457 }
4458
4459 $this->addContent("W n");
4460 }
4461
4462 /**
4463 * ends the last clipping shape
4464 */
4465 function clippingEnd()
4466 {
4467 $this->restore();
4468 }
4469
4470 /**
4471 * scale
4472 *
4473 * @param float $s_x scaling factor for width as percent
4474 * @param float $s_y scaling factor for height as percent
4475 * @param float $x Origin abscissa
4476 * @param float $y Origin ordinate
4477 */
4478 function scale($s_x, $s_y, $x, $y)
4479 {
4480 $y = $this->currentPageSize["height"] - $y;
4481
4482 $tm = [
4483 $s_x,
4484 0,
4485 0,
4486 $s_y,
4487 $x * (1 - $s_x),
4488 $y * (1 - $s_y)
4489 ];
4490
4491 $this->transform($tm);
4492 }
4493
4494 /**
4495 * translate
4496 *
4497 * @param float $t_x movement to the right
4498 * @param float $t_y movement to the bottom
4499 */
4500 function translate($t_x, $t_y)
4501 {
4502 $tm = [
4503 1,
4504 0,
4505 0,
4506 1,
4507 $t_x,
4508 -$t_y
4509 ];
4510
4511 $this->transform($tm);
4512 }
4513
4514 /**
4515 * rotate
4516 *
4517 * @param float $angle angle in degrees for counter-clockwise rotation
4518 * @param float $x Origin abscissa
4519 * @param float $y Origin ordinate
4520 */
4521 function rotate($angle, $x, $y)
4522 {
4523 $y = $this->currentPageSize["height"] - $y;
4524
4525 $a = deg2rad($angle);
4526 $cos_a = cos($a);
4527 $sin_a = sin($a);
4528
4529 $tm = [
4530 $cos_a,
4531 -$sin_a,
4532 $sin_a,
4533 $cos_a,
4534 $x - $sin_a * $y - $cos_a * $x,
4535 $y - $cos_a * $y + $sin_a * $x,
4536 ];
4537
4538 $this->transform($tm);
4539 }
4540
4541 /**
4542 * skew
4543 *
4544 * @param float $angle_x
4545 * @param float $angle_y
4546 * @param float $x Origin abscissa
4547 * @param float $y Origin ordinate
4548 */
4549 function skew($angle_x, $angle_y, $x, $y)
4550 {
4551 $y = $this->currentPageSize["height"] - $y;
4552
4553 $tan_x = tan(deg2rad($angle_x));
4554 $tan_y = tan(deg2rad($angle_y));
4555
4556 $tm = [
4557 1,
4558 -$tan_y,
4559 -$tan_x,
4560 1,
4561 $tan_x * $y,
4562 $tan_y * $x,
4563 ];
4564
4565 $this->transform($tm);
4566 }
4567
4568 /**
4569 * apply graphic transformations
4570 *
4571 * @param array $tm transformation matrix
4572 */
4573 function transform($tm)
4574 {
4575 $this->addContent(vsprintf("\n %.3F %.3F %.3F %.3F %.3F %.3F cm", $tm));
4576 }
4577
4578 /**
4579 * add a new page to the document
4580 * this also makes the new page the current active object
4581 *
4582 * @param int $insert
4583 * @param int $id
4584 * @param string $pos
4585 * @return int
4586 */
4587 function newPage($insert = 0, $id = 0, $pos = 'after')
4588 {
4589 // if there is a state saved, then go up the stack closing them
4590 // then on the new page, re-open them with the right setings
4591
4592 if ($this->nStateStack) {
4593 for ($i = $this->nStateStack; $i >= 1; $i--) {
4594 $this->restoreState($i);
4595 }
4596 }
4597
4598 $this->numObj++;
4599
4600 if ($insert) {
4601 // the id from the ezPdf class is the id of the contents of the page, not the page object itself
4602 // query that object to find the parent
4603 $rid = $this->objects[$id]['onPage'];
4604 $opt = ['rid' => $rid, 'pos' => $pos];
4605 $this->o_page($this->numObj, 'new', $opt);
4606 } else {
4607 $this->o_page($this->numObj, 'new');
4608 }
4609
4610 // if there is a stack saved, then put that onto the page
4611 if ($this->nStateStack) {
4612 for ($i = 1; $i <= $this->nStateStack; $i++) {
4613 $this->saveState($i);
4614 }
4615 }
4616
4617 // and if there has been a stroke or fill color set, then transfer them
4618 if (isset($this->currentColor)) {
4619 $this->setColor($this->currentColor, true);
4620 }
4621
4622 if (isset($this->currentStrokeColor)) {
4623 $this->setStrokeColor($this->currentStrokeColor, true);
4624 }
4625
4626 // if there is a line style set, then put this in too
4627 if (mb_strlen($this->currentLineStyle, '8bit')) {
4628 $this->addContent("\n$this->currentLineStyle");
4629 }
4630
4631 // the call to the o_page object set currentContents to the present page, so this can be returned as the page id
4632 return $this->currentContents;
4633 }
4634
4635 /**
4636 * Streams the PDF to the client.
4637 *
4638 * @param string $filename The filename to present to the client.
4639 * @param array $options Associative array: 'compress' => 1 or 0 (default 1); 'Attachment' => 1 or 0 (default 1).
4640 */
4641 function stream($filename = "document.pdf", $options = [])
4642 {
4643 if (headers_sent()) {
4644 die("Unable to stream pdf: headers already sent");
4645 }
4646
4647 if (!isset($options["compress"])) $options["compress"] = true;
4648 if (!isset($options["Attachment"])) $options["Attachment"] = true;
4649
4650 $debug = !$options['compress'];
4651 $tmp = ltrim($this->output($debug));
4652
4653 header("Cache-Control: private");
4654 header("Content-Type: application/pdf");
4655 header("Content-Length: " . mb_strlen($tmp, "8bit"));
4656
4657 $filename = str_replace(["\n", "'"], "", basename($filename, ".pdf")) . ".pdf";
4658 $attachment = $options["Attachment"] ? "attachment" : "inline";
4659
4660 $encoding = mb_detect_encoding($filename);
4661 $fallbackfilename = mb_convert_encoding($filename, "ISO-8859-1", $encoding);
4662 $fallbackfilename = str_replace("\"", "", $fallbackfilename);
4663 $encodedfilename = rawurlencode($filename);
4664
4665 $contentDisposition = "Content-Disposition: $attachment; filename=\"$fallbackfilename\"";
4666 if ($fallbackfilename !== $filename) {
4667 $contentDisposition .= "; filename*=UTF-8''$encodedfilename";
4668 }
4669 header($contentDisposition);
4670
4671 echo $tmp;
4672 flush();
4673 }
4674
4675 /**
4676 * return the height in units of the current font in the given size
4677 *
4678 * @param float $size
4679 *
4680 * @return float
4681 */
4682 public function getFontHeight(float $size): float
4683 {
4684 if (!$this->numFonts) {
4685 $this->selectFont($this->defaultFont);
4686 }
4687
4688 $font = $this->fonts[$this->currentFont];
4689
4690 // for the current font, and the given size, what is the height of the font in user units
4691 if (isset($font['Ascender']) && isset($font['Descender'])) {
4692 $h = $font['Ascender'] - $font['Descender'];
4693 } else {
4694 $h = $font['FontBBox'][3] - $font['FontBBox'][1];
4695 }
4696
4697 // have to adjust by a font offset for Windows fonts. unfortunately it looks like
4698 // the bounding box calculations are wrong and I don't know why.
4699 if (isset($font['FontHeightOffset'])) {
4700 // For CourierNew from Windows this needs to be -646 to match the
4701 // Adobe native Courier font.
4702 //
4703 // For FreeMono from GNU this needs to be -337 to match the
4704 // Courier font.
4705 //
4706 // Both have been added manually to the .afm and .ufm files.
4707 $h += (int)$font['FontHeightOffset'];
4708 }
4709
4710 return $size * $h / 1000;
4711 }
4712
4713 /**
4714 * @param float $size
4715 *
4716 * @return float
4717 */
4718 public function getFontXHeight(float $size): float
4719 {
4720 if (!$this->numFonts) {
4721 $this->selectFont($this->defaultFont);
4722 }
4723
4724 $font = $this->fonts[$this->currentFont];
4725
4726 // for the current font, and the given size, what is the height of the font in user units
4727 if (isset($font['XHeight'])) {
4728 $xh = $font['Ascender'] - $font['Descender'];
4729 } else {
4730 $xh = $this->getFontHeight($size) / 2;
4731 }
4732
4733 return $size * $xh / 1000;
4734 }
4735
4736 /**
4737 * return the font descender, this will normally return a negative number
4738 * if you add this number to the baseline, you get the level of the bottom of the font
4739 * it is in the pdf user units
4740 *
4741 * @param float $size
4742 *
4743 * @return float
4744 */
4745 public function getFontDescender(float $size): float
4746 {
4747 // note that this will most likely return a negative value
4748 if (!$this->numFonts) {
4749 $this->selectFont($this->defaultFont);
4750 }
4751
4752 //$h = $this->fonts[$this->currentFont]['FontBBox'][1];
4753 $h = $this->fonts[$this->currentFont]['Descender'];
4754
4755 return $size * $h / 1000;
4756 }
4757
4758 /**
4759 * filter the text, this is applied to all text just before being inserted into the pdf document
4760 * it escapes the various things that need to be escaped, and so on
4761 *
4762 * @param $text
4763 * @param bool $bom
4764 * @param bool $convert_encoding
4765 * @return string
4766 */
4767 function filterText($text, $bom = true, $convert_encoding = true)
4768 {
4769 if (!$this->numFonts) {
4770 $this->selectFont($this->defaultFont);
4771 }
4772
4773 if ($convert_encoding) {
4774 $cf = $this->currentFont;
4775 if (isset($this->fonts[$cf]) && $this->fonts[$cf]['isUnicode']) {
4776 $text = $this->utf8toUtf16BE($text, $bom);
4777 } else {
4778 //$text = html_entity_decode($text, ENT_QUOTES);
4779 $text = mb_convert_encoding($text, self::$targetEncoding, 'UTF-8');
4780 }
4781 } elseif ($bom) {
4782 $text = $this->utf8toUtf16BE($text, $bom);
4783 }
4784
4785 // the chr(13) substitution fixes a bug seen in TCPDF (bug #1421290)
4786 return strtr($text, [')' => '\\)', '(' => '\\(', '\\' => '\\\\', chr(13) => '\r']);
4787 }
4788
4789 /**
4790 * return array containing codepoints (UTF-8 character values) for the
4791 * string passed in.
4792 *
4793 * based on the excellent TCPDF code by Nicola Asuni and the
4794 * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html
4795 *
4796 * @param string $text UTF-8 string to process
4797 * @return array UTF-8 codepoints array for the string
4798 */
4799 function utf8toCodePointsArray(&$text)
4800 {
4801 $length = mb_strlen($text, '8bit'); // http://www.php.net/manual/en/function.mb-strlen.php#77040
4802 $unicode = []; // array containing unicode values
4803 $bytes = []; // array containing single character byte sequences
4804 $numbytes = 1; // number of octets needed to represent the UTF-8 character
4805
4806 for ($i = 0; $i < $length; $i++) {
4807 $c = ord($text[$i]); // get one string character at time
4808 if (count($bytes) === 0) { // get starting octect
4809 if ($c <= 0x7F) {
4810 $unicode[] = $c; // use the character "as is" because is ASCII
4811 $numbytes = 1;
4812 } elseif (($c >> 0x05) === 0x06) { // 2 bytes character (0x06 = 110 BIN)
4813 $bytes[] = ($c - 0xC0) << 0x06;
4814 $numbytes = 2;
4815 } elseif (($c >> 0x04) === 0x0E) { // 3 bytes character (0x0E = 1110 BIN)
4816 $bytes[] = ($c - 0xE0) << 0x0C;
4817 $numbytes = 3;
4818 } elseif (($c >> 0x03) === 0x1E) { // 4 bytes character (0x1E = 11110 BIN)
4819 $bytes[] = ($c - 0xF0) << 0x12;
4820 $numbytes = 4;
4821 } else {
4822 // use replacement character for other invalid sequences
4823 $unicode[] = 0xFFFD;
4824 $bytes = [];
4825 $numbytes = 1;
4826 }
4827 } elseif (($c >> 0x06) === 0x02) { // bytes 2, 3 and 4 must start with 0x02 = 10 BIN
4828 $bytes[] = $c - 0x80;
4829 if (count($bytes) === $numbytes) {
4830 // compose UTF-8 bytes to a single unicode value
4831 $c = $bytes[0];
4832 for ($j = 1; $j < $numbytes; $j++) {
4833 $c += ($bytes[$j] << (($numbytes - $j - 1) * 0x06));
4834 }
4835 if ((($c >= 0xD800) and ($c <= 0xDFFF)) or ($c >= 0x10FFFF)) {
4836 // The definition of UTF-8 prohibits encoding character numbers between
4837 // U+D800 and U+DFFF, which are reserved for use with the UTF-16
4838 // encoding form (as surrogate pairs) and do not directly represent
4839 // characters.
4840 $unicode[] = 0xFFFD; // use replacement character
4841 } else {
4842 $unicode[] = $c; // add char to array
4843 }
4844 // reset data for next char
4845 $bytes = [];
4846 $numbytes = 1;
4847 }
4848 } else {
4849 // use replacement character for other invalid sequences
4850 $unicode[] = 0xFFFD;
4851 $bytes = [];
4852 $numbytes = 1;
4853 }
4854 }
4855
4856 return $unicode;
4857 }
4858
4859 /**
4860 * convert UTF-8 to UTF-16 with an additional byte order marker
4861 * at the front if required.
4862 *
4863 * based on the excellent TCPDF code by Nicola Asuni and the
4864 * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html
4865 *
4866 * @param string $text UTF-8 string to process
4867 * @param boolean $bom whether to add the byte order marker
4868 * @return string UTF-16 result string
4869 */
4870 function utf8toUtf16BE(&$text, $bom = true)
4871 {
4872 $out = $bom ? "\xFE\xFF" : '';
4873
4874 $unicode = $this->utf8toCodePointsArray($text);
4875 foreach ($unicode as $c) {
4876 if ($c === 0xFFFD) {
4877 $out .= "\xFF\xFD"; // replacement character
4878 } elseif ($c < 0x10000) {
4879 $out .= chr($c >> 0x08) . chr($c & 0xFF);
4880 } else {
4881 $c -= 0x10000;
4882 $w1 = 0xD800 | ($c >> 0x10);
4883 $w2 = 0xDC00 | ($c & 0x3FF);
4884 $out .= chr($w1 >> 0x08) . chr($w1 & 0xFF) . chr($w2 >> 0x08) . chr($w2 & 0xFF);
4885 }
4886 }
4887
4888 return $out;
4889 }
4890
4891 /**
4892 * given a start position and information about how text is to be laid out, calculate where
4893 * on the page the text will end
4894 *
4895 * @param $x
4896 * @param $y
4897 * @param $angle
4898 * @param $size
4899 * @param $wa
4900 * @param $text
4901 * @return array
4902 */
4903 private function getTextPosition($x, $y, $angle, $size, $wa, $text)
4904 {
4905 // given this information return an array containing x and y for the end position as elements 0 and 1
4906 $w = $this->getTextWidth($size, $text);
4907
4908 // need to adjust for the number of spaces in this text
4909 $words = explode(' ', $text);
4910 $nspaces = count($words) - 1;
4911 $w += $wa * $nspaces;
4912 $a = deg2rad((float)$angle);
4913
4914 return [cos($a) * $w + $x, -sin($a) * $w + $y];
4915 }
4916
4917 /**
4918 * Callback method used by smallCaps
4919 *
4920 * @param array $matches
4921 *
4922 * @return string
4923 */
4924 function toUpper($matches)
4925 {
4926 return mb_strtoupper($matches[0]);
4927 }
4928
4929 function concatMatches($matches)
4930 {
4931 $str = "";
4932 foreach ($matches as $match) {
4933 $str .= $match[0];
4934 }
4935
4936 return $str;
4937 }
4938
4939 /**
4940 * register text for font subsetting
4941 *
4942 * @param string $font
4943 * @param string $text
4944 */
4945 function registerText($font, $text)
4946 {
4947 if (!$this->isUnicode || in_array(mb_strtolower(basename($font)), self::$coreFonts)) {
4948 return;
4949 }
4950
4951 if (!isset($this->stringSubsets[$font])) {
4952 $base_subset = "\u{fffd}\u{fffe}\u{ffff}";
4953 $this->stringSubsets[$font] = $this->utf8toCodePointsArray($base_subset);
4954 }
4955
4956 $this->stringSubsets[$font] = array_unique(
4957 array_merge($this->stringSubsets[$font], $this->utf8toCodePointsArray($text))
4958 );
4959 }
4960
4961 /**
4962 * add text to the document, at a specified location, size and angle on the page
4963 *
4964 * @param float $x
4965 * @param float $y
4966 * @param float $size
4967 * @param string $text
4968 * @param float $angle
4969 * @param float $wordSpaceAdjust
4970 * @param float $charSpaceAdjust
4971 * @param bool $smallCaps
4972 */
4973 function addText($x, $y, $size, $text, $angle = 0, $wordSpaceAdjust = 0, $charSpaceAdjust = 0, $smallCaps = false)
4974 {
4975 if (!$this->numFonts) {
4976 $this->selectFont($this->defaultFont);
4977 }
4978
4979 $text = str_replace(["\r", "\n"], "", $text);
4980
4981 // if ($smallCaps) {
4982 // preg_match_all("/(\P{Ll}+)/u", $text, $matches, PREG_SET_ORDER);
4983 // $lower = $this->concatMatches($matches);
4984 // d($lower);
4985
4986 // preg_match_all("/(\p{Ll}+)/u", $text, $matches, PREG_SET_ORDER);
4987 // $other = $this->concatMatches($matches);
4988 // d($other);
4989
4990 // $text = preg_replace_callback("/\p{Ll}/u", array($this, "toUpper"), $text);
4991 // }
4992
4993 // if there are any open callbacks, then they should be called, to show the start of the line
4994 if ($this->nCallback > 0) {
4995 for ($i = $this->nCallback; $i > 0; $i--) {
4996 // call each function
4997 $info = [
4998 'x' => $x,
4999 'y' => $y,
5000 'angle' => $angle,
5001 'status' => 'sol',
5002 'p' => $this->callback[$i]['p'],
5003 'nCallback' => $this->callback[$i]['nCallback'],
5004 'height' => $this->callback[$i]['height'],
5005 'descender' => $this->callback[$i]['descender']
5006 ];
5007
5008 $func = $this->callback[$i]['f'];
5009 $this->$func($info);
5010 }
5011 }
5012
5013 if ($angle == 0) {
5014 $this->addContent(sprintf("\nBT %.3F %.3F Td", $x, $y));
5015 } else {
5016 $a = deg2rad((float)$angle);
5017 $this->addContent(
5018 sprintf("\nBT %.3F %.3F %.3F %.3F %.3F %.3F Tm", cos($a), -sin($a), sin($a), cos($a), $x, $y)
5019 );
5020 }
5021
5022 if ($wordSpaceAdjust != 0) {
5023 $this->addContent(sprintf(" %.3F Tw", $wordSpaceAdjust));
5024 }
5025
5026 if ($charSpaceAdjust != 0) {
5027 $this->addContent(sprintf(" %.3F Tc", $charSpaceAdjust));
5028 }
5029
5030 $len = mb_strlen($text);
5031 $start = 0;
5032
5033 if ($start < $len) {
5034 $part = $text; // OAR - Don't need this anymore, given that $start always equals zero. substr($text, $start);
5035 $place_text = $this->filterText($part, false);
5036 // modify unicode text so that extra word spacing is manually implemented (bug #)
5037 if ($this->fonts[$this->currentFont]['isUnicode'] && $wordSpaceAdjust != 0) {
5038 $space_scale = 1000 / $size;
5039 $place_text = str_replace("\x00\x20", "\x00\x20)\x00\x20" . (-round($space_scale * $wordSpaceAdjust)) . "\x00\x20(", $place_text);
5040 }
5041 $this->addContent(" /F$this->currentFontNum " . sprintf('%.1F Tf ', $size));
5042 $this->addContent(" [($place_text)] TJ");
5043 }
5044
5045 if ($wordSpaceAdjust != 0) {
5046 $this->addContent(sprintf(" %.3F Tw", 0));
5047 }
5048
5049 if ($charSpaceAdjust != 0) {
5050 $this->addContent(sprintf(" %.3F Tc", 0));
5051 }
5052
5053 $this->addContent(' ET');
5054
5055 // if there are any open callbacks, then they should be called, to show the end of the line
5056 if ($this->nCallback > 0) {
5057 for ($i = $this->nCallback; $i > 0; $i--) {
5058 // call each function
5059 $tmp = $this->getTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, $text);
5060 $info = [
5061 'x' => $tmp[0],
5062 'y' => $tmp[1],
5063 'angle' => $angle,
5064 'status' => 'eol',
5065 'p' => $this->callback[$i]['p'],
5066 'nCallback' => $this->callback[$i]['nCallback'],
5067 'height' => $this->callback[$i]['height'],
5068 'descender' => $this->callback[$i]['descender']
5069 ];
5070 $func = $this->callback[$i]['f'];
5071 $this->$func($info);
5072 }
5073 }
5074
5075 if ($this->fonts[$this->currentFont]['isSubsetting']) {
5076 $this->registerText($this->currentFont, $text);
5077 }
5078 }
5079
5080 /**
5081 * calculate how wide a given text string will be on a page, at a given size.
5082 * this can be called externally, but is also used by the other class functions
5083 *
5084 * @param float $size
5085 * @param string $text
5086 * @param float $wordSpacing
5087 * @param float $charSpacing
5088 *
5089 * @return float
5090 */
5091 public function getTextWidth(float $size, string $text, float $wordSpacing = 0.0, float $charSpacing = 0.0): float
5092 {
5093 static $ord_cache = [];
5094
5095 // this function should not change any of the settings, though it will need to
5096 // track any directives which change during calculation, so copy them at the start
5097 // and put them back at the end.
5098 $store_currentTextState = $this->currentTextState;
5099
5100 if (!$this->numFonts) {
5101 $this->selectFont($this->defaultFont);
5102 }
5103
5104 $text = str_replace(["\r", "\n"], "", $text);
5105
5106 // hmm, this is where it all starts to get tricky - use the font information to
5107 // calculate the width of each character, add them up and convert to user units
5108 $w = 0;
5109 $cf = $this->currentFont;
5110 $current_font = $this->fonts[$cf];
5111 $space_scale = 1000 / ($size > 0 ? $size : 1);
5112
5113 if ($current_font['isUnicode']) {
5114 // for Unicode, use the code points array to calculate width rather
5115 // than just the string itself
5116 $unicode = $this->utf8toCodePointsArray($text);
5117
5118 foreach ($unicode as $char) {
5119 // check if we have to replace character
5120 if (isset($current_font['differences'][$char])) {
5121 $char = $current_font['differences'][$char];
5122 }
5123
5124 if (isset($current_font['C'][$char])) {
5125 $char_width = $current_font['C'][$char];
5126
5127 // add the character width
5128 $w += $char_width;
5129
5130 // add additional padding for space
5131 if (isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space') { // Space
5132 $w += $wordSpacing * $space_scale;
5133 }
5134 }
5135 }
5136
5137 // add additional char spacing
5138 if ($charSpacing != 0) {
5139 $w += $charSpacing * $space_scale * count($unicode);
5140 }
5141
5142 } else {
5143 // If CPDF is in Unicode mode but the current font does not support Unicode we need to convert the character set to Windows-1252
5144 if ($this->isUnicode) {
5145 $text = mb_convert_encoding($text, 'Windows-1252', 'UTF-8');
5146 }
5147
5148 $len = mb_strlen($text, 'Windows-1252');
5149
5150 for ($i = 0; $i < $len; $i++) {
5151 $c = $text[$i];
5152 $char = isset($ord_cache[$c]) ? $ord_cache[$c] : ($ord_cache[$c] = ord($c));
5153
5154 // check if we have to replace character
5155 if (isset($current_font['differences'][$char])) {
5156 $char = $current_font['differences'][$char];
5157 }
5158
5159 if (isset($current_font['C'][$char])) {
5160 $char_width = $current_font['C'][$char];
5161
5162 // add the character width
5163 $w += $char_width;
5164
5165 // add additional padding for space
5166 if (isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space') { // Space
5167 $w += $wordSpacing * $space_scale;
5168 }
5169 }
5170 }
5171
5172 // add additional char spacing
5173 if ($charSpacing != 0) {
5174 $w += $charSpacing * $space_scale * $len;
5175 }
5176 }
5177
5178 $this->currentTextState = $store_currentTextState;
5179 $this->setCurrentFont();
5180
5181 return $w * $size / 1000;
5182 }
5183
5184 /**
5185 * this will be called at a new page to return the state to what it was on the
5186 * end of the previous page, before the stack was closed down
5187 * This is to get around not being able to have open 'q' across pages
5188 *
5189 * @param int $pageEnd
5190 */
5191 function saveState($pageEnd = 0)
5192 {
5193 if ($pageEnd) {
5194 // this will be called at a new page to return the state to what it was on the
5195 // end of the previous page, before the stack was closed down
5196 // This is to get around not being able to have open 'q' across pages
5197 $opt = $this->stateStack[$pageEnd];
5198 // ok to use this as stack starts numbering at 1
5199 $this->setColor($opt['col'], true);
5200 $this->setStrokeColor($opt['str'], true);
5201 $this->addContent("\n" . $opt['lin']);
5202 // $this->currentLineStyle = $opt['lin'];
5203 } else {
5204 $this->nStateStack++;
5205 $this->stateStack[$this->nStateStack] = [
5206 'col' => $this->currentColor,
5207 'str' => $this->currentStrokeColor,
5208 'lin' => $this->currentLineStyle
5209 ];
5210 }
5211
5212 $this->save();
5213 }
5214
5215 /**
5216 * restore a previously saved state
5217 *
5218 * @param int $pageEnd
5219 */
5220 function restoreState($pageEnd = 0)
5221 {
5222 if (!$pageEnd) {
5223 $n = $this->nStateStack;
5224 $this->currentColor = $this->stateStack[$n]['col'];
5225 $this->currentStrokeColor = $this->stateStack[$n]['str'];
5226 $this->addContent("\n" . $this->stateStack[$n]['lin']);
5227 $this->currentLineStyle = $this->stateStack[$n]['lin'];
5228 $this->stateStack[$n] = null;
5229 unset($this->stateStack[$n]);
5230 $this->nStateStack--;
5231 }
5232
5233 $this->restore();
5234 }
5235
5236 /**
5237 * make a loose object, the output will go into this object, until it is closed, then will revert to
5238 * the current one.
5239 * this object will not appear until it is included within a page.
5240 * the function will return the object number
5241 *
5242 * @return int
5243 */
5244 function openObject()
5245 {
5246 $this->nStack++;
5247 $this->stack[$this->nStack] = ['c' => $this->currentContents, 'p' => $this->currentPage];
5248 // add a new object of the content type, to hold the data flow
5249 $this->numObj++;
5250 $this->o_contents($this->numObj, 'new');
5251 $this->currentContents = $this->numObj;
5252 $this->looseObjects[$this->numObj] = 1;
5253
5254 return $this->numObj;
5255 }
5256
5257 /**
5258 * open an existing object for editing
5259 *
5260 * @param $id
5261 */
5262 function reopenObject($id)
5263 {
5264 $this->nStack++;
5265 $this->stack[$this->nStack] = ['c' => $this->currentContents, 'p' => $this->currentPage];
5266 $this->currentContents = $id;
5267
5268 // also if this object is the primary contents for a page, then set the current page to its parent
5269 if (isset($this->objects[$id]['onPage'])) {
5270 $this->currentPage = $this->objects[$id]['onPage'];
5271 }
5272 }
5273
5274 /**
5275 * close an object
5276 */
5277 function closeObject()
5278 {
5279 // close the object, as long as there was one open in the first place, which will be indicated by
5280 // an objectId on the stack.
5281 if ($this->nStack > 0) {
5282 $this->currentContents = $this->stack[$this->nStack]['c'];
5283 $this->currentPage = $this->stack[$this->nStack]['p'];
5284 $this->nStack--;
5285 // easier to probably not worry about removing the old entries, they will be overwritten
5286 // if there are new ones.
5287 }
5288 }
5289
5290 /**
5291 * stop an object from appearing on pages from this point on
5292 *
5293 * @param $id
5294 */
5295 function stopObject($id)
5296 {
5297 // if an object has been appearing on pages up to now, then stop it, this page will
5298 // be the last one that could contain it.
5299 if (isset($this->addLooseObjects[$id])) {
5300 $this->addLooseObjects[$id] = '';
5301 }
5302 }
5303
5304 /**
5305 * after an object has been created, it wil only show if it has been added, using this function.
5306 *
5307 * @param $id
5308 * @param string $options
5309 */
5310 function addObject($id, $options = 'add')
5311 {
5312 // add the specified object to the page
5313 if (isset($this->looseObjects[$id]) && $this->currentContents != $id) {
5314 // then it is a valid object, and it is not being added to itself
5315 switch ($options) {
5316 case 'all':
5317 // then this object is to be added to this page (done in the next block) and
5318 // all future new pages.
5319 $this->addLooseObjects[$id] = 'all';
5320
5321 case 'add':
5322 if (isset($this->objects[$this->currentContents]['onPage'])) {
5323 // then the destination contents is the primary for the page
5324 // (though this object is actually added to that page)
5325 $this->o_page($this->objects[$this->currentContents]['onPage'], 'content', $id);
5326 }
5327 break;
5328
5329 case 'even':
5330 $this->addLooseObjects[$id] = 'even';
5331 $pageObjectId = $this->objects[$this->currentContents]['onPage'];
5332 if ($this->objects[$pageObjectId]['info']['pageNum'] % 2 == 0) {
5333 $this->addObject($id);
5334 // hacky huh :)
5335 }
5336 break;
5337
5338 case 'odd':
5339 $this->addLooseObjects[$id] = 'odd';
5340 $pageObjectId = $this->objects[$this->currentContents]['onPage'];
5341 if ($this->objects[$pageObjectId]['info']['pageNum'] % 2 == 1) {
5342 $this->addObject($id);
5343 // hacky huh :)
5344 }
5345 break;
5346
5347 case 'next':
5348 $this->addLooseObjects[$id] = 'all';
5349 break;
5350
5351 case 'nexteven':
5352 $this->addLooseObjects[$id] = 'even';
5353 break;
5354
5355 case 'nextodd':
5356 $this->addLooseObjects[$id] = 'odd';
5357 break;
5358 }
5359 }
5360 }
5361
5362 /**
5363 * return a storable representation of a specific object
5364 *
5365 * @param $id
5366 * @return string|null
5367 */
5368 function serializeObject($id)
5369 {
5370 if (array_key_exists($id, $this->objects)) {
5371 return serialize($this->objects[$id]);
5372 }
5373
5374 return null;
5375 }
5376
5377 /**
5378 * restore an object from its stored representation. Returns its new object id.
5379 *
5380 * @param $obj
5381 * @return int
5382 */
5383 function restoreSerializedObject($obj)
5384 {
5385 $obj_id = $this->openObject();
5386 $this->objects[$obj_id] = unserialize($obj);
5387 $this->closeObject();
5388
5389 return $obj_id;
5390 }
5391
5392 /**
5393 * Embeds a file inside the PDF
5394 *
5395 * @param string $filepath path to the file to store inside the PDF
5396 * @param string $embeddedFilename the filename displayed in the list of embedded files
5397 * @param string $description a description in the list of embedded files
5398 */
5399 public function addEmbeddedFile(string $filepath, string $embeddedFilename, string $description): void
5400 {
5401 $this->numObj++;
5402 $this->o_embedded_file_dictionary(
5403 $this->numObj,
5404 'new',
5405 [
5406 'filepath' => $filepath,
5407 'filename' => $embeddedFilename,
5408 'description' => $description
5409 ]
5410 );
5411 }
5412
5413 /**
5414 * Add content to the documents info object
5415 *
5416 * @param string|array $label
5417 * @param string $value
5418 */
5419 public function addInfo($label, string $value = ""): void
5420 {
5421 // this will only work if the label is one of the valid ones.
5422 // modify this so that arrays can be passed as well.
5423 // if $label is an array then assume that it is key => value pairs
5424 // else assume that they are both scalar, anything else will probably error
5425 if (is_array($label)) {
5426 foreach ($label as $l => $v) {
5427 $this->o_info($this->infoObject, $l, (string) $v);
5428 }
5429 } else {
5430 $this->o_info($this->infoObject, $label, $value);
5431 }
5432 }
5433
5434 /**
5435 * set the viewer preferences of the document, it is up to the browser to obey these.
5436 *
5437 * @param $label
5438 * @param int $value
5439 */
5440 function setPreferences($label, $value = 0)
5441 {
5442 // this will only work if the label is one of the valid ones.
5443 if (is_array($label)) {
5444 foreach ($label as $l => $v) {
5445 $this->o_catalog($this->catalogId, 'viewerPreferences', [$l => $v]);
5446 }
5447 } else {
5448 $this->o_catalog($this->catalogId, 'viewerPreferences', [$label => $value]);
5449 }
5450 }
5451
5452 /**
5453 * extract an integer from a position in a byte stream
5454 *
5455 * @param $data
5456 * @param $pos
5457 * @param $num
5458 * @return int
5459 */
5460 private function getBytes(&$data, $pos, $num)
5461 {
5462 // return the integer represented by $num bytes from $pos within $data
5463 $ret = 0;
5464 for ($i = 0; $i < $num; $i++) {
5465 $ret *= 256;
5466 $ret += ord($data[$pos + $i]);
5467 }
5468
5469 return $ret;
5470 }
5471
5472 /**
5473 * Check if image already added to pdf image directory.
5474 * If yes, need not to create again (pass empty data)
5475 *
5476 * @param string $imgname
5477 * @return bool
5478 */
5479 function image_iscached($imgname)
5480 {
5481 return isset($this->imagelist[$imgname]);
5482 }
5483
5484 /**
5485 * add a PNG image into the document, from a GD object
5486 * this should work with remote files
5487 *
5488 * @param \GdImage|resource $img A GD resource
5489 * @param string $file The PNG file
5490 * @param float $x X position
5491 * @param float $y Y position
5492 * @param float $w Width
5493 * @param float $h Height
5494 * @param bool $is_mask true if the image is a mask
5495 * @param bool $mask true if the image is masked
5496 * @throws Exception
5497 */
5498 function addImagePng(&$img, $file, $x, $y, $w = 0.0, $h = 0.0, $is_mask = false, $mask = null)
5499 {
5500 if (!function_exists("imagepng")) {
5501 throw new \Exception("The PHP GD extension is required, but is not installed.");
5502 }
5503
5504 //if already cached, need not to read again
5505 if (isset($this->imagelist[$file])) {
5506 $data = null;
5507 } else {
5508 // Example for transparency handling on new image. Retain for current image
5509 // $tIndex = imagecolortransparent($img);
5510 // if ($tIndex > 0) {
5511 // $tColor = imagecolorsforindex($img, $tIndex);
5512 // $new_tIndex = imagecolorallocate($new_img, $tColor['red'], $tColor['green'], $tColor['blue']);
5513 // imagefill($new_img, 0, 0, $new_tIndex);
5514 // imagecolortransparent($new_img, $new_tIndex);
5515 // }
5516 // blending mode (literal/blending) on drawing into current image. not relevant when not saved or not drawn
5517 //imagealphablending($img, true);
5518
5519 //default, but explicitely set to ensure pdf compatibility
5520 imagesavealpha($img, false/*!$is_mask && !$mask*/);
5521
5522 $error = 0;
5523 //DEBUG_IMG_TEMP
5524 //debugpng
5525 if (defined("DEBUGPNG") && DEBUGPNG) {
5526 print '[addImagePng ' . $file . ']';
5527 }
5528
5529 ob_start();
5530 @imagepng($img);
5531 $data = ob_get_clean();
5532
5533 if ($data == '') {
5534 $error = 1;
5535 $errormsg = 'trouble writing file from GD';
5536 //DEBUG_IMG_TEMP
5537 //debugpng
5538 if (defined("DEBUGPNG") && DEBUGPNG) {
5539 print 'trouble writing file from GD';
5540 }
5541 }
5542
5543 if ($error) {
5544 $this->addMessage('PNG error - (' . $file . ') ' . $errormsg);
5545
5546 return;
5547 }
5548 } //End isset($this->imagelist[$file]) (png Duplicate removal)
5549
5550 $this->addPngFromBuf($data, $file, $x, $y, $w, $h, $is_mask, $mask);
5551 }
5552
5553 /**
5554 * @param $file
5555 * @param $x
5556 * @param $y
5557 * @param $w
5558 * @param $h
5559 * @param $byte
5560 */
5561 protected function addImagePngAlpha($file, $x, $y, $w, $h, $byte)
5562 {
5563 // generate images
5564 $img = @imagecreatefrompng($file);
5565
5566 if ($img === false) {
5567 return;
5568 }
5569
5570 // FIXME The pixel transformation doesn't work well with 8bit PNGs
5571 $eight_bit = ($byte & 4) !== 4;
5572
5573 $wpx = imagesx($img);
5574 $hpx = imagesy($img);
5575
5576 imagesavealpha($img, false);
5577
5578 // create temp alpha file
5579 $tempfile_alpha = @tempnam($this->tmp, "cpdf_img_");
5580 @unlink($tempfile_alpha);
5581 $tempfile_alpha = "$tempfile_alpha.png";
5582
5583 // create temp plain file
5584 $tempfile_plain = @tempnam($this->tmp, "cpdf_img_");
5585 @unlink($tempfile_plain);
5586 $tempfile_plain = "$tempfile_plain.png";
5587
5588 $imgalpha = imagecreate($wpx, $hpx);
5589 imagesavealpha($imgalpha, false);
5590
5591 // generate gray scale palette (0 -> 255)
5592 for ($c = 0; $c < 256; ++$c) {
5593 imagecolorallocate($imgalpha, $c, $c, $c);
5594 }
5595
5596 // Use PECL gmagick + Graphics Magic to process transparent PNG images
5597 if (extension_loaded("gmagick")) {
5598 $gmagick = new \Gmagick($file);
5599 $gmagick->setimageformat('png');
5600
5601 // Get opacity channel (negative of alpha channel)
5602 $alpha_channel_neg = clone $gmagick;
5603 $alpha_channel_neg->separateimagechannel(\Gmagick::CHANNEL_OPACITY);
5604
5605 // Negate opacity channel
5606 $alpha_channel = new \Gmagick();
5607 $alpha_channel->newimage($wpx, $hpx, "#FFFFFF", "png");
5608 $alpha_channel->compositeimage($alpha_channel_neg, \Gmagick::COMPOSITE_DIFFERENCE, 0, 0);
5609 $alpha_channel->separateimagechannel(\Gmagick::CHANNEL_RED);
5610 $alpha_channel->writeimage($tempfile_alpha);
5611
5612 // Cast to 8bit+palette
5613 $imgalpha_ = @imagecreatefrompng($tempfile_alpha);
5614 imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx);
5615 if (PHP_MAJOR_VERSION < 8) {
5616 imagedestroy($imgalpha_);
5617 }
5618 imagepng($imgalpha, $tempfile_alpha);
5619
5620 // Make opaque image
5621 $color_channels = new \Gmagick();
5622 $color_channels->newimage($wpx, $hpx, "#FFFFFF", "png");
5623 $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYRED, 0, 0);
5624 $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYGREEN, 0, 0);
5625 $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYBLUE, 0, 0);
5626 $color_channels->writeimage($tempfile_plain);
5627
5628 $imgplain = @imagecreatefrompng($tempfile_plain);
5629 }
5630 // Use PECL imagick + ImageMagic to process transparent PNG images
5631 elseif (extension_loaded("imagick")) {
5632 // Native cloning was added to pecl-imagick in svn commit 263814
5633 // the first version containing it was 3.0.1RC1
5634 static $imagickClonable = null;
5635 if ($imagickClonable === null) {
5636 $imagickClonable = true;
5637 if (defined('Imagick::IMAGICK_EXTVER')) {
5638 $imagickVersion = \Imagick::IMAGICK_EXTVER;
5639 } else {
5640 $imagickVersion = '0';
5641 }
5642 if (version_compare($imagickVersion, '0.0.1', '>=')) {
5643 $imagickClonable = version_compare($imagickVersion, '3.0.1rc1', '>=');
5644 }
5645 }
5646
5647 $imagick = new \Imagick($file);
5648 $imagick->setFormat('png');
5649
5650 // Get opacity channel (negative of alpha channel)
5651 if ($imagick->getImageAlphaChannel()) {
5652 $alpha_channel = $imagickClonable ? clone $imagick : $imagick->clone();
5653 $alpha_channel->separateImageChannel(\Imagick::CHANNEL_ALPHA);
5654 // Since ImageMagick7 negate invert transparency as default
5655 if (\Imagick::getVersion()['versionNumber'] < 1800) {
5656 $alpha_channel->negateImage(true);
5657 }
5658 $alpha_channel->writeImage($tempfile_alpha);
5659
5660 // Cast to 8bit+palette
5661 $imgalpha_ = @imagecreatefrompng($tempfile_alpha);
5662 imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx);
5663 if (PHP_MAJOR_VERSION < 8) {
5664 imagedestroy($imgalpha_);
5665 }
5666 imagepng($imgalpha, $tempfile_alpha);
5667 } else {
5668 $tempfile_alpha = null;
5669 }
5670
5671 // Make opaque image
5672 $color_channels = new \Imagick();
5673 $color_channels->newImage($wpx, $hpx, "#FFFFFF", "png");
5674 $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYRED, 0, 0);
5675 $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYGREEN, 0, 0);
5676 $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYBLUE, 0, 0);
5677 $color_channels->writeImage($tempfile_plain);
5678
5679 $imgplain = @imagecreatefrompng($tempfile_plain);
5680 } else {
5681 // allocated colors cache
5682 $allocated_colors = [];
5683
5684 // extract alpha channel
5685 for ($xpx = 0; $xpx < $wpx; ++$xpx) {
5686 for ($ypx = 0; $ypx < $hpx; ++$ypx) {
5687 $color = imagecolorat($img, $xpx, $ypx);
5688 $col = imagecolorsforindex($img, $color);
5689 $alpha = $col['alpha'];
5690
5691 if ($eight_bit) {
5692 // with gamma correction
5693 $gammacorr = 2.2;
5694 $pixel = round(pow((((127 - $alpha) * 255 / 127) / 255), $gammacorr) * 255);
5695 } else {
5696 // without gamma correction
5697 $pixel = (127 - $alpha) * 2;
5698
5699 $key = $col['red'] . $col['green'] . $col['blue'];
5700
5701 if (!isset($allocated_colors[$key])) {
5702 $pixel_img = imagecolorallocate($img, $col['red'], $col['green'], $col['blue']);
5703 $allocated_colors[$key] = $pixel_img;
5704 } else {
5705 $pixel_img = $allocated_colors[$key];
5706 }
5707
5708 imagesetpixel($img, $xpx, $ypx, $pixel_img);
5709 }
5710
5711 imagesetpixel($imgalpha, $xpx, $ypx, $pixel);
5712 }
5713 }
5714
5715 // extract image without alpha channel
5716 $imgplain = imagecreatetruecolor($wpx, $hpx);
5717 imagecopy($imgplain, $img, 0, 0, 0, 0, $wpx, $hpx);
5718 if (PHP_MAJOR_VERSION < 8) {
5719 imagedestroy($img);
5720 }
5721
5722 imagepng($imgalpha, $tempfile_alpha);
5723 imagepng($imgplain, $tempfile_plain);
5724 }
5725
5726 $this->imageAlphaList[$file] = [$tempfile_alpha, $tempfile_plain];
5727
5728 // embed mask image
5729 if ($tempfile_alpha) {
5730 $this->addImagePng($imgalpha, $tempfile_alpha, $x, $y, $w, $h, true);
5731 if (PHP_MAJOR_VERSION < 8) {
5732 imagedestroy($imgalpha);
5733 }
5734 $this->imageCache[] = $tempfile_alpha;
5735 }
5736
5737 // embed image, masked with previously embedded mask
5738 $this->addImagePng($imgplain, $tempfile_plain, $x, $y, $w, $h, false, ($tempfile_alpha !== null));
5739 if (PHP_MAJOR_VERSION < 8) {
5740 imagedestroy($imgplain);
5741 }
5742 $this->imageCache[] = $tempfile_plain;
5743 }
5744
5745 /**
5746 * add a PNG image into the document, from a file
5747 * this should work with remote files
5748 *
5749 * @param $file
5750 * @param $x
5751 * @param $y
5752 * @param int $w
5753 * @param int $h
5754 * @throws Exception
5755 */
5756 function addPngFromFile($file, $x, $y, $w = 0, $h = 0)
5757 {
5758 if (!function_exists("imagecreatefrompng")) {
5759 throw new \Exception("The PHP GD extension is required, but is not installed.");
5760 }
5761
5762 if (isset($this->imageAlphaList[$file])) {
5763 [$alphaFile, $plainFile] = $this->imageAlphaList[$file];
5764
5765 if ($alphaFile) {
5766 $img = null;
5767 $this->addImagePng($img, $alphaFile, $x, $y, $w, $h, true);
5768 }
5769
5770 $img = null;
5771 $this->addImagePng($img, $plainFile, $x, $y, $w, $h, false, ($plainFile !== null));
5772 return;
5773 }
5774
5775 //if already cached, need not to read again
5776 if (isset($this->imagelist[$file])) {
5777 $img = null;
5778 } else {
5779 $info = file_get_contents($file, false, null, 24, 5);
5780 $meta = unpack("CbitDepth/CcolorType/CcompressionMethod/CfilterMethod/CinterlaceMethod", $info);
5781 $bit_depth = $meta["bitDepth"];
5782 $color_type = $meta["colorType"];
5783
5784 // http://www.w3.org/TR/PNG/#11IHDR
5785 // 3 => indexed
5786 // 4 => greyscale with alpha
5787 // 6 => fullcolor with alpha
5788 $is_alpha = in_array($color_type, [4, 6]) || ($color_type == 3 && $bit_depth != 4);
5789
5790 if ($is_alpha) { // exclude grayscale alpha
5791 $this->addImagePngAlpha($file, $x, $y, $w, $h, $color_type);
5792 return;
5793 }
5794
5795 //png files typically contain an alpha channel.
5796 //pdf file format or class.pdf does not support alpha blending.
5797 //on alpha blended images, more transparent areas have a color near black.
5798 //This appears in the result on not storing the alpha channel.
5799 //Correct would be the box background image or its parent when transparent.
5800 //But this would make the image dependent on the background.
5801 //Therefore create an image with white background and copy in
5802 //A more natural background than black is white.
5803 //Therefore create an empty image with white background and merge the
5804 //image in with alpha blending.
5805 $imgtmp = @imagecreatefrompng($file);
5806 if (!$imgtmp) {
5807 return;
5808 }
5809 $sx = imagesx($imgtmp);
5810 $sy = imagesy($imgtmp);
5811 $img = imagecreatetruecolor($sx, $sy);
5812 imagealphablending($img, true);
5813
5814 // @todo is it still needed ??
5815 $ti = imagecolortransparent($imgtmp);
5816 if ($ti >= 0) {
5817 $tc = imagecolorsforindex($imgtmp, $ti);
5818 $ti = imagecolorallocate($img, $tc['red'], $tc['green'], $tc['blue']);
5819 imagefill($img, 0, 0, $ti);
5820 imagecolortransparent($img, $ti);
5821 } else {
5822 imagefill($img, 1, 1, imagecolorallocate($img, 255, 255, 255));
5823 }
5824
5825 imagecopy($img, $imgtmp, 0, 0, 0, 0, $sx, $sy);
5826 if (PHP_MAJOR_VERSION < 8) {
5827 imagedestroy($imgtmp);
5828 }
5829 }
5830 $this->addImagePng($img, $file, $x, $y, $w, $h);
5831
5832 if ($img && PHP_MAJOR_VERSION < 8) {
5833 imagedestroy($img);
5834 }
5835 }
5836
5837 /**
5838 * add a PNG image into the document, from a memory buffer of the file
5839 *
5840 * @param $data
5841 * @param $file
5842 * @param $x
5843 * @param $y
5844 * @param float $w
5845 * @param float $h
5846 * @param bool $is_mask
5847 * @param null $mask
5848 */
5849 function addPngFromBuf(&$data, $file, $x, $y, $w = 0.0, $h = 0.0, $is_mask = false, $mask = null)
5850 {
5851 if (isset($this->imagelist[$file])) {
5852 $data = null;
5853 $info['width'] = $this->imagelist[$file]['w'];
5854 $info['height'] = $this->imagelist[$file]['h'];
5855 $label = $this->imagelist[$file]['label'];
5856 } else {
5857 if ($data == null) {
5858 $this->addMessage('addPngFromBuf error - data not present!');
5859
5860 return;
5861 }
5862
5863 $error = 0;
5864
5865 if (!$error) {
5866 $header = chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10);
5867
5868 if (mb_substr($data, 0, 8, '8bit') != $header) {
5869 $error = 1;
5870
5871 if (defined("DEBUGPNG") && DEBUGPNG) {
5872 print '[addPngFromFile this file does not have a valid header ' . $file . ']';
5873 }
5874
5875 $errormsg = 'this file does not have a valid header';
5876 }
5877 }
5878
5879 if (!$error) {
5880 // set pointer
5881 $p = 8;
5882 $len = mb_strlen($data, '8bit');
5883
5884 // cycle through the file, identifying chunks
5885 $haveHeader = 0;
5886 $info = [];
5887 $idata = '';
5888 $pdata = '';
5889
5890 while ($p < $len) {
5891 $chunkLen = $this->getBytes($data, $p, 4);
5892 $chunkType = mb_substr($data, $p + 4, 4, '8bit');
5893
5894 switch ($chunkType) {
5895 case 'IHDR':
5896 // this is where all the file information comes from
5897 $info['width'] = $this->getBytes($data, $p + 8, 4);
5898 $info['height'] = $this->getBytes($data, $p + 12, 4);
5899 $info['bitDepth'] = ord($data[$p + 16]);
5900 $info['colorType'] = ord($data[$p + 17]);
5901 $info['compressionMethod'] = ord($data[$p + 18]);
5902 $info['filterMethod'] = ord($data[$p + 19]);
5903 $info['interlaceMethod'] = ord($data[$p + 20]);
5904
5905 //print_r($info);
5906 $haveHeader = 1;
5907 if ($info['compressionMethod'] != 0) {
5908 $error = 1;
5909
5910 //debugpng
5911 if (defined("DEBUGPNG") && DEBUGPNG) {
5912 print '[addPngFromFile unsupported compression method ' . $file . ']';
5913 }
5914
5915 $errormsg = 'unsupported compression method';
5916 }
5917
5918 if ($info['filterMethod'] != 0) {
5919 $error = 1;
5920
5921 //debugpng
5922 if (defined("DEBUGPNG") && DEBUGPNG) {
5923 print '[addPngFromFile unsupported filter method ' . $file . ']';
5924 }
5925
5926 $errormsg = 'unsupported filter method';
5927 }
5928 break;
5929
5930 case 'PLTE':
5931 $pdata .= mb_substr($data, $p + 8, $chunkLen, '8bit');
5932 break;
5933
5934 case 'IDAT':
5935 $idata .= mb_substr($data, $p + 8, $chunkLen, '8bit');
5936 break;
5937
5938 case 'tRNS':
5939 //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk
5940 //print "tRNS found, color type = ".$info['colorType']."\n";
5941 $transparency = [];
5942
5943 switch ($info['colorType']) {
5944 // indexed color, rbg
5945 case 3:
5946 /* corresponding to entries in the plte chunk
5947 Alpha for palette index 0: 1 byte
5948 Alpha for palette index 1: 1 byte
5949 ...etc...
5950 */
5951 // there will be one entry for each palette entry. up until the last non-opaque entry.
5952 // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent)
5953 $transparency['type'] = 'indexed';
5954 $trans = 0;
5955
5956 for ($i = $chunkLen; $i >= 0; $i--) {
5957 if (ord($data[$p + 8 + $i]) == 0) {
5958 $trans = $i;
5959 }
5960 }
5961
5962 $transparency['data'] = $trans;
5963 break;
5964
5965 // grayscale
5966 case 0:
5967 /* corresponding to entries in the plte chunk
5968 Gray: 2 bytes, range 0 .. (2^bitdepth)-1
5969 */
5970 // $transparency['grayscale'] = $this->PRVT_getBytes($data,$p+8,2); // g = grayscale
5971 $transparency['type'] = 'indexed';
5972 $transparency['data'] = ord($data[$p + 8 + 1]);
5973 break;
5974
5975 // truecolor
5976 case 2:
5977 /* corresponding to entries in the plte chunk
5978 Red: 2 bytes, range 0 .. (2^bitdepth)-1
5979 Green: 2 bytes, range 0 .. (2^bitdepth)-1
5980 Blue: 2 bytes, range 0 .. (2^bitdepth)-1
5981 */
5982 $transparency['r'] = $this->getBytes($data, $p + 8, 2);
5983 // r from truecolor
5984 $transparency['g'] = $this->getBytes($data, $p + 10, 2);
5985 // g from truecolor
5986 $transparency['b'] = $this->getBytes($data, $p + 12, 2);
5987 // b from truecolor
5988
5989 $transparency['type'] = 'color-key';
5990 break;
5991
5992 //unsupported transparency type
5993 default:
5994 if (defined("DEBUGPNG") && DEBUGPNG) {
5995 print '[addPngFromFile unsupported transparency type ' . $file . ']';
5996 }
5997 break;
5998 }
5999
6000 // KS End new code
6001 break;
6002
6003 default:
6004 break;
6005 }
6006
6007 $p += $chunkLen + 12;
6008 }
6009
6010 if (!$haveHeader) {
6011 $error = 1;
6012
6013 //debugpng
6014 if (defined("DEBUGPNG") && DEBUGPNG) {
6015 print '[addPngFromFile information header is missing ' . $file . ']';
6016 }
6017
6018 $errormsg = 'information header is missing';
6019 }
6020
6021 if (isset($info['interlaceMethod']) && $info['interlaceMethod']) {
6022 $error = 1;
6023
6024 //debugpng
6025 if (defined("DEBUGPNG") && DEBUGPNG) {
6026 print '[addPngFromFile no support for interlaced images in pdf ' . $file . ']';
6027 }
6028
6029 $errormsg = 'There appears to be no support for interlaced images in pdf.';
6030 }
6031 }
6032
6033 if (!$error && $info['bitDepth'] > 8) {
6034 $error = 1;
6035
6036 //debugpng
6037 if (defined("DEBUGPNG") && DEBUGPNG) {
6038 print '[addPngFromFile bit depth of 8 or less is supported ' . $file . ']';
6039 }
6040
6041 $errormsg = 'only bit depth of 8 or less is supported';
6042 }
6043
6044 if (!$error) {
6045 switch ($info['colorType']) {
6046 case 3:
6047 $color = 'DeviceRGB';
6048 $ncolor = 1;
6049 break;
6050
6051 case 2:
6052 $color = 'DeviceRGB';
6053 $ncolor = 3;
6054 break;
6055
6056 case 0:
6057 $color = 'DeviceGray';
6058 $ncolor = 1;
6059 break;
6060
6061 default:
6062 $error = 1;
6063
6064 //debugpng
6065 if (defined("DEBUGPNG") && DEBUGPNG) {
6066 print '[addPngFromFile alpha channel not supported: ' . $info['colorType'] . ' ' . $file . ']';
6067 }
6068
6069 $errormsg = 'transparency alpha channel not supported, transparency only supported for palette images.';
6070 }
6071 }
6072
6073 if ($error) {
6074 $this->addMessage('PNG error - (' . $file . ') ' . $errormsg);
6075
6076 return;
6077 }
6078
6079 //print_r($info);
6080 // so this image is ok... add it in.
6081 $this->numImages++;
6082 $im = $this->numImages;
6083 $label = "I$im";
6084 $this->numObj++;
6085
6086 // $this->o_image($this->numObj,'new',array('label' => $label,'data' => $idata,'iw' => $w,'ih' => $h,'type' => 'png','ic' => $info['width']));
6087 $options = [
6088 'label' => $label,
6089 'data' => $idata,
6090 'bitsPerComponent' => $info['bitDepth'],
6091 'pdata' => $pdata,
6092 'iw' => $info['width'],
6093 'ih' => $info['height'],
6094 'type' => 'png',
6095 'color' => $color,
6096 'ncolor' => $ncolor,
6097 'masked' => $mask,
6098 'isMask' => $is_mask
6099 ];
6100
6101 if (isset($transparency)) {
6102 $options['transparency'] = $transparency;
6103 }
6104
6105 $this->o_image($this->numObj, 'new', $options);
6106 $this->imagelist[$file] = ['label' => $label, 'w' => $info['width'], 'h' => $info['height']];
6107 }
6108
6109 if ($is_mask) {
6110 return;
6111 }
6112
6113 if ($w <= 0 && $h <= 0) {
6114 $w = $info['width'];
6115 $h = $info['height'];
6116 }
6117
6118 if ($w <= 0) {
6119 $w = $h / $info['height'] * $info['width'];
6120 }
6121
6122 if ($h <= 0) {
6123 $h = $w * $info['height'] / $info['width'];
6124 }
6125
6126 $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ", $w, $h, $x, $y, $label));
6127 }
6128
6129 /**
6130 * add a JPEG image into the document, from a file
6131 *
6132 * @param $img
6133 * @param $x
6134 * @param $y
6135 * @param int $w
6136 * @param int $h
6137 */
6138 function addJpegFromFile($img, $x, $y, $w = 0, $h = 0)
6139 {
6140 // attempt to add a jpeg image straight from a file, using no GD commands
6141 // note that this function is unable to operate on a remote file.
6142
6143 if (!file_exists($img)) {
6144 return;
6145 }
6146
6147 if ($this->image_iscached($img)) {
6148 $data = null;
6149 $imageWidth = $this->imagelist[$img]['w'];
6150 $imageHeight = $this->imagelist[$img]['h'];
6151 $channels = $this->imagelist[$img]['c'];
6152 } else {
6153 $tmp = getimagesize($img);
6154 $imageWidth = $tmp[0];
6155 $imageHeight = $tmp[1];
6156
6157 if (isset($tmp['channels'])) {
6158 $channels = $tmp['channels'];
6159 } else {
6160 $channels = 3;
6161 }
6162
6163 $data = file_get_contents($img);
6164 }
6165
6166 if ($w <= 0 && $h <= 0) {
6167 $w = $imageWidth;
6168 }
6169
6170 if ($w == 0) {
6171 $w = $h / $imageHeight * $imageWidth;
6172 }
6173
6174 if ($h == 0) {
6175 $h = $w * $imageHeight / $imageWidth;
6176 }
6177
6178 $this->addJpegImage_common($data, $img, $imageWidth, $imageHeight, $x, $y, $w, $h, $channels);
6179 }
6180
6181 /**
6182 * common code used by the two JPEG adding functions
6183 * @param $data
6184 * @param $imgname
6185 * @param $imageWidth
6186 * @param $imageHeight
6187 * @param $x
6188 * @param $y
6189 * @param int $w
6190 * @param int $h
6191 * @param int $channels
6192 */
6193 private function addJpegImage_common(
6194 &$data,
6195 $imgname,
6196 $imageWidth,
6197 $imageHeight,
6198 $x,
6199 $y,
6200 $w = 0,
6201 $h = 0,
6202 $channels = 3
6203 ) {
6204 if ($this->image_iscached($imgname)) {
6205 $label = $this->imagelist[$imgname]['label'];
6206 //debugpng
6207 //if (DEBUGPNG) print '[addJpegImage_common Duplicate '.$imgname.']';
6208
6209 } else {
6210 if ($data == null) {
6211 $this->addMessage('addJpegImage_common error - (' . $imgname . ') data not present!');
6212
6213 return;
6214 }
6215
6216 // note that this function is not to be called externally
6217 // it is just the common code between the GD and the file options
6218 $this->numImages++;
6219 $im = $this->numImages;
6220 $label = "I$im";
6221 $this->numObj++;
6222
6223 $this->o_image(
6224 $this->numObj,
6225 'new',
6226 [
6227 'label' => $label,
6228 'data' => &$data,
6229 'iw' => $imageWidth,
6230 'ih' => $imageHeight,
6231 'channels' => $channels
6232 ]
6233 );
6234
6235 $this->imagelist[$imgname] = [
6236 'label' => $label,
6237 'w' => $imageWidth,
6238 'h' => $imageHeight,
6239 'c' => $channels
6240 ];
6241 }
6242
6243 $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ ", $w, $h, $x, $y, $label));
6244 }
6245
6246 /**
6247 * specify where the document should open when it first starts
6248 *
6249 * @param $style
6250 * @param int $a
6251 * @param int $b
6252 * @param int $c
6253 */
6254 function openHere($style, $a = 0, $b = 0, $c = 0)
6255 {
6256 // this function will open the document at a specified page, in a specified style
6257 // the values for style, and the required parameters are:
6258 // 'XYZ' left, top, zoom
6259 // 'Fit'
6260 // 'FitH' top
6261 // 'FitV' left
6262 // 'FitR' left,bottom,right
6263 // 'FitB'
6264 // 'FitBH' top
6265 // 'FitBV' left
6266 $this->numObj++;
6267 $this->o_destination(
6268 $this->numObj,
6269 'new',
6270 ['page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c]
6271 );
6272 $id = $this->catalogId;
6273 $this->o_catalog($id, 'openHere', $this->numObj);
6274 }
6275
6276 /**
6277 * Add JavaScript code to the PDF document
6278 *
6279 * @param string $code
6280 */
6281 function addJavascript($code)
6282 {
6283 $this->javascript .= $code;
6284 }
6285
6286 /**
6287 * create a labelled destination within the document
6288 *
6289 * @param $label
6290 * @param $style
6291 * @param int $a
6292 * @param int $b
6293 * @param int $c
6294 */
6295 function addDestination($label, $style, $a = 0, $b = 0, $c = 0)
6296 {
6297 // associates the given label with the destination, it is done this way so that a destination can be specified after
6298 // it has been linked to
6299 // styles are the same as the 'openHere' function
6300 $this->numObj++;
6301 $this->o_destination(
6302 $this->numObj,
6303 'new',
6304 ['page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c]
6305 );
6306 $id = $this->numObj;
6307
6308 // store the label->idf relationship, note that this means that labels can be used only once
6309 $this->destinations["$label"] = $id;
6310 }
6311
6312 /**
6313 * define font families, this is used to initialize the font families for the default fonts
6314 * and for the user to add new ones for their fonts. The default bahavious can be overridden should
6315 * that be desired.
6316 *
6317 * @param $family
6318 * @param string $options
6319 */
6320 function setFontFamily($family, $options = '')
6321 {
6322 if (!is_array($options)) {
6323 if ($family === 'init') {
6324 // set the known family groups
6325 // these font families will be used to enable bold and italic markers to be included
6326 // within text streams. html forms will be used... <b></b> <i></i>
6327 $this->fontFamilies['Helvetica.afm'] =
6328 [
6329 'b' => 'Helvetica-Bold.afm',
6330 'i' => 'Helvetica-Oblique.afm',
6331 'bi' => 'Helvetica-BoldOblique.afm',
6332 'ib' => 'Helvetica-BoldOblique.afm'
6333 ];
6334
6335 $this->fontFamilies['Courier.afm'] =
6336 [
6337 'b' => 'Courier-Bold.afm',
6338 'i' => 'Courier-Oblique.afm',
6339 'bi' => 'Courier-BoldOblique.afm',
6340 'ib' => 'Courier-BoldOblique.afm'
6341 ];
6342
6343 $this->fontFamilies['Times-Roman.afm'] =
6344 [
6345 'b' => 'Times-Bold.afm',
6346 'i' => 'Times-Italic.afm',
6347 'bi' => 'Times-BoldItalic.afm',
6348 'ib' => 'Times-BoldItalic.afm'
6349 ];
6350 }
6351 } else {
6352
6353 // the user is trying to set a font family
6354 // note that this can also be used to set the base ones to something else
6355 if (mb_strlen($family)) {
6356 $this->fontFamilies[$family] = $options;
6357 }
6358 }
6359 }
6360
6361 /**
6362 * used to add messages for use in debugging
6363 *
6364 * @param $message
6365 */
6366 function addMessage($message)
6367 {
6368 $this->messages .= $message . "\n";
6369 }
6370
6371 /**
6372 * a few functions which should allow the document to be treated transactionally.
6373 *
6374 * @param $action
6375 */
6376 function transaction($action)
6377 {
6378 switch ($action) {
6379 case 'start':
6380 // store all the data away into the checkpoint variable
6381 $data = get_object_vars($this);
6382 $this->checkpoint = $data;
6383 unset($data);
6384 break;
6385
6386 case 'commit':
6387 if (is_array($this->checkpoint) && isset($this->checkpoint['checkpoint'])) {
6388 $tmp = $this->checkpoint['checkpoint'];
6389 $this->checkpoint = $tmp;
6390 unset($tmp);
6391 } else {
6392 $this->checkpoint = '';
6393 }
6394 break;
6395
6396 case 'rewind':
6397 // do not destroy the current checkpoint, but move us back to the state then, so that we can try again
6398 if (is_array($this->checkpoint)) {
6399 // can only abort if were inside a checkpoint
6400 $tmp = $this->checkpoint;
6401
6402 foreach ($tmp as $k => $v) {
6403 if ($k !== 'checkpoint') {
6404 $this->$k = $v;
6405 }
6406 }
6407 unset($tmp);
6408 }
6409 break;
6410
6411 case 'abort':
6412 if (is_array($this->checkpoint)) {
6413 // can only abort if were inside a checkpoint
6414 $tmp = $this->checkpoint;
6415 foreach ($tmp as $k => $v) {
6416 $this->$k = $v;
6417 }
6418 unset($tmp);
6419 }
6420 break;
6421 }
6422 }
6423 }
6424