PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / vendor / phenx / php-svg-lib / src / Svg / Surface / CPdf.php

CPdf.php in Yatra – Travel Booking & Tour Operator Software 3.0.2.7, at vendor/phenx/php-svg-lib/src/Svg/Surface/CPdf.php

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