PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.22
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.22
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 All 169 releases
woocommerce-pos / vendor_prefixed / dompdf / dompdf / lib / Cpdf.php

Cpdf.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.22, at vendor_prefixed/dompdf/dompdf/lib/Cpdf.php

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