PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
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 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 All 164 releases
woocommerce-pos / vendor_prefixed / dompdf / php-svg-lib / src / Svg / Surface / CPdf.php

CPdf.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.20, at vendor_prefixed/dompdf/php-svg-lib/src/Svg/Surface/CPdf.php

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