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