Completed
Branch development (b1b115)
by Johannes
10:28
created

Cpdf   F

Complexity

Total Complexity 801

Size/Duplication

Total Lines 5617
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 801
c 0
b 0
f 0
dl 0
loc 5617
rs 0.8

How to fix   Complexity   

Complex Class

Complex classes like Cpdf often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Cpdf, and based on these observations, apply Extract Interface, too.

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