Passed
Push — main ( 829487...427f06 )
by Stefan
02:23
created

FPDF::isCoreFont()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
namespace OPlathey\FPDF;
3
4
/**
5
 * Modified version of O.Platheys FPDF.php
6
 *
7
 * Based on version 1.82 of FPDF.php, extended by 
8
 * - namespace to include through autoloader
9
 * - PHP 7.4 typehints
10
 * - phpDoc comments  
11
 *
12
 * @package OPlathey/FPDF
13
 * @version 1.82
14
 * @author O.Plathey
15
 * @copyright MIT License - see the LICENSE file for details
16
 */
17
18
/*******************************************************************************
19
* FPDF                                                                         *
20
*                                                                              *
21
* Version: 1.82                                                                *
22
* Date:    2019-12-07                                                          *
23
* Author:  Olivier PLATHEY                                                     *
24
* http://www.fpdf.org/en/doc/index.php
25
*******************************************************************************/
26
27
define('FPDF_VERSION','1.82');
28
29
class FPDF
30
{
31
    /** @var int current page number     */
32
    protected int $page = 0;
33
    /** @var int current object number     */
34
    protected int $n = 2;
35
    /** @var array array of object offsets     */
36
    protected array $offsets; 
37
    /** @var string buffer holding in-memory PDF     */
38
    protected string $buffer = ''; 
39
    /** @var array array containing pages     */
40
    protected array $pages = array(); 
41
    /** @var int current document state     */
42
    protected int $state = 0; 
43
    /** @var bool compression flag     */
44
    protected bool $compress;
45
    /** @var float scale factor (number of points in user unit)     */
46
    protected float $k;
47
    /** @var string default orientation     */
48
    protected string $DefOrientation;
49
    /** @var string current orientation     */
50
    protected string $CurOrientation;
51
    /** @var array default page size     */
52
    protected array $DefPageSize;
53
    /** @var array current page size     */
54
    protected array $CurPageSize;
55
    /** @var int current page rotation     */
56
    protected int $CurRotation;
57
    /** @var array page-related data     */
58
    protected array $PageInfo = array();
59
    /** @var float width of current page in points     */
60
    protected float $wPt;
61
    /** @var float height of current page in points     */
62
    protected float $hPt;
63
    /** @var float width of current page in user unit     */
64
    protected float $w;
65
    /** @var float height of current page in user unit     */
66
    protected float $h;
67
    /** @var float left margin     */
68
    protected float $lMargin;
69
    /** @var float top margin     */
70
    protected float $tMargin;
71
    /** @var float right margin     */
72
    protected float $rMargin;
73
    /** @var float page break margin     */
74
    protected float $bMargin;
75
    /** @var float cell margin     */
76
    protected float $cMargin;
77
    /** @var float current X-position in user unit     */
78
    protected float $x; 
79
    /** @var float current Y-position in user unit     */
80
    protected float $y;
81
    /** @var float height of last printed cell     */
82
    protected float $lasth = 0.0;
83
    /** @var float line width in user unit     */
84
    protected float $LineWidth;
85
    /** @var string path containing fonts     */
86
    protected string $fontpath;
87
    /** @var array array of used fonts     */
88
    protected array $fonts = array();
89
    /** @var array array of font files     */
90
    protected array $FontFiles = array();
91
    /** @var array array of encodings     */
92
    protected array $encodings = array();
93
    /** @var array array of ToUnicode CMaps     */
94
    protected array $cmaps = array();
95
    /** @var string current font family     */
96
    protected string $FontFamily = '';
97
    /** @var string current font style     */
98
    protected string $FontStyle = '';
99
    /** @var bool underlining flag     */
100
    protected bool $underline = false;
101
    /** @var array current font info     */
102
    protected array $CurrentFont;
103
    /** @var float current font size in points     */
104
    protected float $FontSizePt = 12.0;
105
    /** @var float current font size in user unit     */
106
    protected float $FontSize;
107
    /** @var string commands for drawing color     */
108
    protected string $DrawColor = '0 G';
109
    /** @var string commands for filling color     */
110
    protected string $FillColor = '0 g';
111
    /** @var string commands for text color     */
112
    protected string $TextColor = '0 g';
113
    /** @var bool indicates whether fill and text colors are different     */
114
    protected bool $ColorFlag = false;
115
    /** @var bool indicates whether alpha channel is used     */
116
    protected bool $WithAlpha = false;
117
    /** @var float word spacing     */
118
    protected float $ws = 0.0;
119
    /** @var array array of used images     */
120
    protected array $images = array();
121
    /** @var array array of links in pages     */
122
    protected array $PageLinks;
123
    /** @var array array of internal links     */
124
    protected array $links = array();
125
    /** @var bool automatic page breaking     */
126
    protected bool $AutoPageBreak;
127
    /** @var float threshold used to trigger page breaks     */
128
    protected float $PageBreakTrigger;
129
    /** @var bool flag set when processing header     */
130
    protected bool $InHeader = false;
131
    /** @var bool flag set when processing footer     */
132
    protected bool $InFooter = false;
133
    /** @var string alias for total number of pages     */
134
    protected string $AliasNbPages;
135
    /** @var string|float zoom display mode     */
136
    protected $ZoomMode;
137
    /** @var string layout display mode     */
138
    protected string $LayoutMode;
139
    /** @var array document properties     */
140
    protected array $metadata;
141
    /** @var string PDF version number     */
142
    protected string $PDFVersion;
143
    /** @var array   */
144
    protected array $outlines = array();
145
    /** @var int     */
146
    protected int $outlineRoot;
147
    
148
    /**
149
     * This is the class constructor. 
150
     * It allows to set up the page size, the orientation and the unit of measure used 
151
     * in all methods (except for font sizes).
152
     * @param string $orientation   Default page orientation. <br/>
153
     *                              Possible values are (case insensitive): <ul> 
154
     *                              <li>   'P' or 'Portrait' </li>
155
     *                              <li>   'L' or 'Landscape' </li></ul> 
156
     *                              Default value is 'P'. <br/>
157
     * @param string $unit          User unit.  <br/>
158
     *                              Possible values are: <ul>
159
     *                              <li>   'pt': point,  </li> 
160
     *                              <li>   'mm': millimeter,  </li>
161
     *                              <li>   'cm': centimeter,  </li>
162
     *                              <li>   'in': inch </li></ul>
163
     *                              A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). <br/>
164
     *                              This is a very common unit in typography; font sizes are expressed in that unit. <br/>
165
     *                              Default value is 'mm'. <br/>
166
     * @param string|array $size    The size used for pages. <br/>
167
     *                              It can be either one of the following values (case insensitive): <ul>
168
     *                              <li> 'A3' </li>
169
     *                              <li> 'A4' </li>
170
     *                              <li> 'A5' </li>
171
     *                              <li> 'Letter' </li>
172
     *                              <li> 'Legal' </li></ul>
173
     *                              or an array containing the width and the height (expressed in the unit given by unit). <br/>
174
     *                              Default value is 'A4'.
175
     */
176
    public function __construct(string $orientation='P', string $unit='mm', $size='A4')
177
    {
178
        // Some checks
179
        $this->doChecks();
180
        $this->getFontPath();
181
        
182
        // Scale factor
183
        switch ($unit) {
184
            case 'pt':
185
                $this->k = 1;
186
                break;
187
            case 'mm':
188
                $this->k = 72 / 25.4;
189
                break;
190
            case 'cm':
191
                $this->k = 72 / 2.54;
192
                break;
193
            case 'in':
194
                $this->k = 72;
195
                break;
196
            default:
197
                $this->error('Incorrect unit: ' . $unit);
198
                break;
199
        }
200
        
201
        // Page sizes
202
        $size = $this->getPageSize($size);
203
        $this->DefPageSize = $size;
204
        $this->CurPageSize = $size;
205
        
206
        // Page orientation
207
        $orientation = strtolower($orientation);
208
        if ($orientation == 'p' || $orientation == 'portrait') {
209
            $this->DefOrientation = 'P';
210
            $this->w = $size[0];
211
            $this->h = $size[1];
212
        } elseif ($orientation == 'l' || $orientation == 'landscape') {
213
            $this->DefOrientation = 'L';
214
            $this->w = $size[1];
215
            $this->h = $size[0];
216
        } else {
217
            $this->error('Incorrect orientation: ' . $orientation);
218
        }
219
        $this->CurOrientation = $this->DefOrientation;
220
        $this->wPt = $this->w * $this->k;
221
        $this->hPt = $this->h * $this->k;
222
        
223
        // set some default values 
224
        // - no page rotation
225
        // - page margins 1cm
226
        // - interior cell margin 1mm
227
        // - line width 0.2mm
228
        // - automatic page break
229
        // - default display mode
230
        // - enable compression
231
        // - PDF version 1.3
232
        $this->CurRotation = 0;
233
        
234
        $margin = 28.35 / $this->k;
235
        $this->setMargins($margin, $margin);
236
        $this->cMargin = $margin / 10;
237
        $this->LineWidth = 0.567 / $this->k;
238
        $this->setAutoPageBreak(true, 2 * $margin);
239
        $this->setDisplayMode('default');
240
        $this->setCompression(true);
241
        $this->PDFVersion = '1.3';
242
    }
243
244
    /**
245
     * Defines the left, top and right margins. 
246
     * By default, they equal 1 cm. Call this method to change them. 
247
     * @param float $left   Left margin.
248
     * @param float $top    Top margin.
249
     * @param float $right  Right margin. Default value is the left one.
250
     */
251
    public function setMargins(float $left, float $top, ?float $right=null) : void
252
    {
253
        // Set left, top and right margins
254
        $this->lMargin = $left;
255
        $this->tMargin = $top;
256
        if ($right === null) {
257
            $right = $left;
258
        }
259
        $this->rMargin = $right;
260
    }
261
262
    /**
263
     * Defines the left margin. 
264
     * The method can be called before creating the first page.
265
     * If the current X-position gets out of page, it is brought back to the margin. 
266
     * @param float $margin Left margin.
267
     */
268
    public function setLeftMargin(float $margin) : void
269
    {
270
        // Set left margin
271
        $this->lMargin = $margin;
272
        if ($this->page > 0 && $this->x < $margin) {
273
            $this->x = $margin;
274
        }
275
    }
276
277
    /**
278
     * Defines the top margin. 
279
     * The method can be called before creating the first page.  
280
     * @param float $margin
281
     */
282
    public function setTopMargin(float $margin) : void
283
    {
284
        // Set top margin
285
        $this->tMargin = $margin;
286
    }
287
288
    /**
289
     * Defines the right margin. 
290
     * The method can be called before creating the first page. 
291
     * @param float $margin
292
     */
293
    public function setRightMargin(float $margin) : void
294
    {
295
        // Set right margin
296
        $this->rMargin = $margin;
297
    }
298
299
    /**
300
     * Enables or disables the automatic page breaking mode. 
301
     * When enabling, the second parameter is the distance from the bottom of the page 
302
     * that defines the triggering limit.
303
     * By default, the mode is on and the margin is 2 cm. 
304
     * @param bool $auto    indicating if mode should be on or off. 
305
     * @param float $margin Distance from the bottom of the page. 
306
     */
307
    public function setAutoPageBreak(bool $auto, float $margin = 0) : void
308
    {
309
        // Set auto page break mode and triggering margin
310
        $this->AutoPageBreak = $auto;
311
        $this->bMargin = $margin;
312
        $this->PageBreakTrigger = $this->h-$margin;
313
    }
314
315
    /**
316
     * Defines the way the document is to be displayed by the viewer. 
317
     * The zoom level can be set: <br/> 
318
     * pages can be displayed <ul>
319
     * <li> entirely on screen </li>
320
     * <li> occupy the full width of the window </li>
321
     * <li> use real size </li>
322
     * <li> be scaled by a specific zooming factor </li>
323
     * <li> or use viewer default (configured in the Preferences menu of Adobe Reader). </li></ul> 
324
     * The page layout can be specified too: <ul> 
325
     * <li> single at once </li>
326
     * <li> continuous display </li>
327
     * <li> two columns </li>
328
     * <li> or viewer default. </li></ul> 
329
     * @param string|float $zoom    The zoom to use. <br/>
330
     *                              It can be one of the following string values: <ul>
331
     *                              <li> 'fullpage': displays the entire page on screen </li>
332
     *                              <li> 'fullwidth': uses maximum width of window </li>
333
     *                              <li> 'real': uses real size (equivalent to 100% zoom) </li>
334
     *                              <li> 'default': uses viewer default mode </li>
335
     *                              <li> or a number indicating the zooming factor to use. </li></ul> 
336
     * @param string $layout        The page layout. Possible values are: <ul>
337
     *                              <li> 'single': displays one page at once </li>
338
     *                              <li> 'continuous': displays pages continuously </li>
339
     *                              <li> 'two': displays two pages on two columns </li>
340
     *                              <li> 'defaul't: uses viewer default mode </li></ul>
341
     *                              Default value is default. 
342
     */
343
    public function setDisplayMode($zoom, string $layout='default') : void
344
    {
345
        // Set display mode in viewer
346
        if ($zoom == 'fullpage' || $zoom == 'fullwidth' || $zoom == 'real' || $zoom == 'default' || !is_string($zoom)) {
347
            $this->ZoomMode = $zoom;
348
        } else {
349
            $this->error('Incorrect zoom display mode: ' . $zoom);
350
        }
351
        if ($layout == 'single' || $layout == 'continuous' || $layout == 'two' || $layout == 'default') {
352
            $this->LayoutMode = $layout;
353
        } else {
354
            $this->error('Incorrect layout display mode: ' . $layout);
355
        }
356
    }
357
358
    /**
359
     * Activates or deactivates page compression. 
360
     * When activated, the internal representation of each page is compressed, which leads to 
361
     * a compression ratio of about 2 for the resulting document.
362
     * Compression is on by default. <br/>
363
     * <br/>
364
     * <b>Note: the Zlib extension is required for this feature. If not present, compression will be turned off.</b> 
365
     * @param bool $compress
366
     */
367
    public function setCompression(bool $compress) : void
368
    {
369
        // Set page compression
370
        if (function_exists('gzcompress')) {
371
            $this->compress = $compress;
372
        } else {
373
            $this->compress = false;
374
        }
375
    }
376
377
    /**
378
     * Defines the title of the document. 
379
     * @param string $title The title.
380
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: false. 
381
     */
382
    public function setTitle(string $title, bool $isUTF8 = false) : void
383
    {
384
        // Title of document
385
        $this->metadata['Title'] = $isUTF8 ? $title : utf8_encode($title);
386
    }
387
388
    /**
389
     * Defines the author of the document. 
390
     * @param string $author
391
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: false.
392
     */
393
    public function setAuthor(string $author, bool $isUTF8 = false) : void
394
    {
395
        // Author of document
396
        $this->metadata['Author'] = $isUTF8 ? $author : utf8_encode($author);
397
    }
398
399
    /**
400
     * Defines the subject of the document. 
401
     * @param string $subject
402
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: false.
403
     */
404
    public function setSubject(string $subject, bool $isUTF8 = false) : void
405
    {
406
        // Subject of document
407
        $this->metadata['Subject'] = $isUTF8 ? $subject : utf8_encode($subject);
408
    }
409
    
410
    /**
411
     * Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'. 
412
     * @param string $keywords
413
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: false.
414
     */
415
    public function setKeywords(string $keywords, bool $isUTF8 = false) : void
416
    {
417
        // Keywords of document
418
        $this->metadata['Keywords'] = $isUTF8 ? $keywords : utf8_encode($keywords);
419
    }
420
    
421
    /**
422
     * Defines the creator of the document. This is typically the name of the application that generates the PDF. 
423
     * @param string $creator
424
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: false.
425
     */
426
    public function setCreator(string $creator, bool $isUTF8 = false) : void
427
    {
428
        // Creator of document
429
        $this->metadata['Creator'] = $isUTF8 ? $creator : utf8_encode($creator);
430
    }
431
432
    /**
433
     * Defines an alias for the total number of pages. It will be substituted as the document is closed. 
434
     * @param string $alias The alias. Default value: {nb}. 
435
     */
436
    public function aliasNbPages(string $alias = '{nb}') : void
437
    {
438
        // Define an alias for total number of pages
439
        $this->AliasNbPages = $alias;
440
    }
441
    
442
    /**
443
     * This method is automatically called in case of a fatal error.
444
     * It simply throws an exception with the provided message.
445
     * An inherited class may override it to customize the error handling but 
446
     * the method should never return, otherwise the resulting document would probably be invalid. 
447
     * @param string $msg The error message.
448
     * @throws \Exception
449
     */
450
    public function error(string $msg) : void
451
    {
452
        // Fatal error
453
        throw new \Exception('FPDF error: ' . $msg);
454
    }
455
    
456
    /**
457
     * Terminates the PDF document. 
458
     * It is not necessary to call this method explicitly because Output() does it 
459
     * automatically. If the document contains no page, AddPage() is called to prevent 
460
     * from getting an invalid document.
461
     */
462
    public function close() : void
463
    {
464
        // Terminate document
465
        if ($this->state == 3) {
466
            return;
467
        }
468
        if ($this->page == 0) {
469
            $this->addPage();
470
        }
471
        // Page footer
472
        $this->InFooter = true;
473
        $this->footer();
474
        $this->InFooter = false;
475
        // Close page
476
        $this->endPage();
477
        // Close document
478
        $this->endDoc();
479
    }
480
    
481
    /**
482
     * Adds a new page to the document. 
483
     * If a page is already present, the Footer() method is called first to output the 
484
     * footer. Then the page is added, the current position set to the top-left corner 
485
     * according to the left and top margins, and Header() is called to display the header.
486
     * The font which was set before calling is automatically restored. There is no need 
487
     * to call SetFont() again if you want to continue with the same font. The same is 
488
     * true for colors and line width.
489
     * The origin of the Y-position system is at the top-left corner and increasing 
490
     * Y-positions go downwards.
491
     * @param string $orientation   Default page orientation. <br/>
492
     *                              Possible values are (case insensitive): <ul> 
493
     *                              <li> 'P' or 'Portrait' </li>
494
     *                              <li> 'L' or 'Landscape' </li></ul>
495
     *                              Default value is 'P'. <br/> 
496
     * @param string|array $size    The size used for pages. <br/>
497
     *                              It can be either one of the following values (case insensitive): <ul>
498
     *                              <li> 'A3' </li>
499
     *                              <li> 'A4' </li>
500
     *                              <li> 'A5' </li>
501
     *                              <li> 'Letter' </li>
502
     *                              <li> 'Legal' </li></ul>
503
     *                              or an array containing the width and the height (expressed in the unit given by unit). <br/>
504
     *                              Default value is 'A4'.  <br/>
505
     * @param int $rotation         Angle by which to rotate the page. <br/>
506
     *                              It must be a multiple of 90; positive values mean clockwise rotation. </br>
507
     *                              The default value is 0.
508
     */
509
    public function addPage(string $orientation = '', $size = '', int $rotation = 0) : void
510
    {
511
        // Start a new page
512
        if ($this->state == 3) {
513
            $this->error('The document is closed');
514
        }
515
        $family = $this->FontFamily;
516
        $style = $this->FontStyle . ($this->underline ? 'U' : '');
517
        $fontsize = $this->FontSizePt;
518
        $lw = $this->LineWidth;
519
        $dc = $this->DrawColor;
520
        $fc = $this->FillColor;
521
        $tc = $this->TextColor;
522
        $cf = $this->ColorFlag;
523
        if ($this->page > 0) {
524
            // Page footer
525
            $this->InFooter = true;
526
            $this->footer();
527
            $this->InFooter = false;
528
            // Close page
529
            $this->endPage();
530
        }
531
        // Start new page
532
        $this->beginPage($orientation, $size, $rotation);
533
        // Set line cap style to square
534
        $this->out('2 J');
535
        // Set line width
536
        $this->LineWidth = $lw;
537
        $this->out(sprintf('%.2F w', $lw * $this->k));
538
        // Set font
539
        if ($family) {
540
            $this->setFont($family, $style, $fontsize);
541
        }
542
        // Set colors
543
        $this->DrawColor = $dc;
544
        if ($dc != '0 G') {
545
            $this->out($dc);
546
        }
547
        $this->FillColor = $fc;
548
        if ($fc != '0 g') {
549
            $this->out($fc);
550
        }
551
        $this->TextColor = $tc;
552
        $this->ColorFlag = $cf;
553
        // Page header
554
        $this->InHeader = true;
555
        $this->header();
556
        $this->InHeader = false;
557
        // Restore line width
558
        if ($this->LineWidth != $lw) {
559
            $this->LineWidth = $lw;
560
            $this->out(sprintf('%.2F w', $lw * $this->k));
561
        }
562
        // Restore font
563
        if ($family) {
564
            $this->setFont($family, $style, $fontsize);
565
        }
566
        // Restore colors
567
        if ($this->DrawColor != $dc) {
568
            $this->DrawColor = $dc;
569
            $this->out($dc);
570
        }
571
        if ($this->FillColor != $fc) {
572
            $this->FillColor = $fc;
573
            $this->out($fc);
574
        }
575
        $this->TextColor = $tc;
576
        $this->ColorFlag = $cf;
577
    }
578
    
579
    /**
580
     * This method is used to render the page header. 
581
     * It is automatically called by AddPage() and should not be called directly by the 
582
     * application. The implementation in FPDF is empty, so you have to subclass it and 
583
     * override the method if you want a specific processing.
584
     */
585
    public function header() : void
586
    {
587
        // To be implemented in your own inherited class
588
    }
589
    
590
    /**
591
     * This method is used to render the page footer. 
592
     * It is automatically called by AddPage() and Close() and should not be called 
593
     * directly by the application. The implementation in FPDF is empty, so you have to 
594
     * subclass it and override the method if you want a specific processing.
595
     */
596
    public function footer() : void
597
    {
598
        // To be implemented in your own inherited class
599
    }
600
    
601
    /**
602
     * Returns the current page number.
603
     * @return int
604
     */
605
    public function pageNo() : int
606
    {
607
        // Get current page number
608
        return $this->page;
609
    }
610
    
611
    /**
612
     * Defines the color used for all drawing operations (lines, rectangles and cell borders). 
613
     * It can be expressed in RGB components or gray scale. The method can be called before 
614
     * the first page is created and the value is retained from page to page.
615
     * @param int $r    If g and b are given, red component; if not, indicates the gray level. Value between 0 and 255.
616
     * @param int $g    Green component (between 0 and 255).
617
     * @param int $b    Blue component (between 0 and 255).
618
     */
619
    public function setDrawColor(int $r, ?int $g = null, ?int $b = null) : void
620
    {
621
        // Set color for all stroking operations
622
        if (($r === 0 && $g === 0 && $b === 0) || $g === null) {
623
            $this->DrawColor = sprintf('%.3F G', $r / 255);
624
        } else {
625
            $this->DrawColor = sprintf('%.3F %.3F %.3F RG', $r / 255, $g / 255, $b / 255);
626
        }
627
        if ($this->page > 0) {
628
            $this->out($this->DrawColor);
629
        }
630
    }
631
    
632
    /**
633
     * Defines the color used for all filling operations (filled rectangles and cell backgrounds). 
634
     * It can be expressed in RGB components or gray scale. The method can be called before the 
635
     * first page is created and the value is retained from page to page.
636
     * @param int $r    If g and b are given, red component; if not, indicates the gray level. Value between 0 and 255.
637
     * @param int $g    Green component (between 0 and 255).
638
     * @param int $b    Blue component (between 0 and 255).
639
     */
640
    public function setFillColor(int $r, ?int $g = null, ?int $b = null) : void
641
    {
642
        // Set color for all filling operations
643
        if (($r === 0 && $g === 0 && $b === 0) || $g === null) {
644
            $this->FillColor = sprintf('%.3F g', $r / 255);
645
        } else {
646
            $this->FillColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255);
647
        }
648
        $this->ColorFlag = ($this->FillColor != $this->TextColor);
649
        if ($this->page > 0) {
650
            $this->out($this->FillColor);
651
        }
652
    }
653
    
654
    /**
655
     * Defines the color used for text. 
656
     * It can be expressed in RGB components or gray scale. The method can be called before the 
657
     * first page is created and the value is retained from page to page.
658
     * @param int $r    If g and b are given, red component; if not, indicates the gray level. Value between 0 and 255.
659
     * @param int $g    Green component (between 0 and 255).
660
     * @param int $b    Blue component (between 0 and 255).
661
     */
662
    public function setTextColor(int $r, ?int $g = null, ?int $b = null) : void
663
    {
664
        // Set color for text
665
        if (($r === 0 && $g === 0 && $b === 0) || $g === null) {
666
            $this->TextColor = sprintf('%.3F g', $r / 255);
667
        } else {
668
            $this->TextColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255);
669
        }
670
        $this->ColorFlag = ($this->FillColor != $this->TextColor);
671
    }
672
    
673
    /**
674
     * Returns the length of a string in user unit for current Font. 
675
     * A font must be selected.
676
     * @param string $s The string whose length is to be computed.
677
     * @return float
678
     */
679
    public function getStringWidth(string $s) : float
680
    {
681
        // Get width of a string in the current font
682
        $s = (string)$s;
683
        $cw = &$this->CurrentFont['cw'];
684
        $w = 0;
685
        $l = strlen($s);
686
        for ($i = 0; $i < $l; $i++) {
687
            $w += $cw[$s[$i]];
688
        }
689
        return $w * $this->FontSize / 1000;
690
    }
691
    
692
    /**
693
     * Defines the line width. 
694
     * By default, the value equals 0.2 mm. The method can be called before the first 
695
     * page is created and the value is retained from page to page.
696
     * @param float $width
697
     */
698
    public function setLineWidth(float $width) : void
699
    {
700
        // Set line width
701
        $this->LineWidth = $width;
702
        if ($this->page > 0) {
703
            $this->out(sprintf('%.2F w', $width * $this->k));
704
        }
705
    }
706
    
707
    /**
708
     * Draws a line between two points.
709
     * The X/Y-positions refer to the top left corner of the page. 
710
     * Set margins are NOT taken into account.
711
     * @param float $x1     X-position upper left corner
712
     * @param float $y1     Y-position upper left corner
713
     * @param float $x2     X-position lower right corner
714
     * @param float $y2     Y-position lower right corner
715
     */
716
    public function line(float $x1, float $y1, float $x2, float $y2) : void
717
    {
718
        // Draw a line
719
        $x1 *= $this->k;
720
        $x2 *= $this->k;
721
        $y1 = ($this->h - $y1) * $this->k;
722
        $y2 = ($this->h - $y2) * $this->k;
723
        $this->out(sprintf('%.2F %.2F m %.2F %.2F l S', $x1, $y1, $x2, $y2));
724
    }
725
726
    /**
727
     * Outputs a rectangle. 
728
     * It can be drawn (border only), filled (with no border) or both.
729
     * The X/Y-position refer to the top left corner of the page. 
730
     * Set margins are NOT taken into account.
731
     * @param float $x      X-position upper left corner
732
     * @param float $y      Y-position upper left corner
733
     * @param float $w      Width
734
     * @param float $h      Height
735
     * @param string $style Style of rendering. <br/>
736
     *                      Possible values are: <ul>
737
     *                      <li>   'D' or empty string: draw the shape. This is the default value. </li>
738
     *                      <li>   'F': fill. </li>
739
     *                      <li>   'DF' or 'FD': draw the shape and fill. </li></ul>
740
     */
741
    public function rect(float $x, float $y, float $w, float $h, string $style = '') : void
742
    {
743
        // Draw a rectangle
744
        if ($style=='F') {
745
            $op = 'f';
746
        } elseif ($style == 'FD' || $style=='DF') {
747
            $op = 'B';
748
        } else {
749
            $op = 'S';
750
        }
751
        $x *= $this->k;
752
        $w *= $this->k;
753
        $y = ($this->h - $y) * $this->k;
754
        $h *= -$this->k;
755
        $this->out(sprintf('%.2F %.2F %.2F %.2F re %s', $x, $y, $w, $h, $op));
756
    }
757
    
758
    /**
759
     * Imports a TrueType, OpenType or Type1 font and makes it available. 
760
     * It is necessary to generate a font definition file first with the MakeFont utility.
761
     * The definition file (and the font file itself when embedding) must be present in 
762
     * the font directory. If it is not found, the error "Could not include font definition file" 
763
     * is raised.
764
     * @param string $family    Font family. The name can be chosen arbitrarily. If it is a standard family name, it will override the corresponding font.
765
     * @param string $style     Font style. <br/>
766
     *                          Possible values are (case insensitive): <ul>
767
     *                          <li> empty string: regular </li>
768
     *                          <li> 'B': bold </li>
769
     *                          <li> 'I': italic </li>
770
     *                          <li> 'BI' or 'IB': bold italic </li></ul>
771
     *                          The default value is regular. <br/>
772
     * @param string $file      The font definition file. <br/>
773
     *                          By default, the name is built from the family and style, in lower case with no space.
774
     */
775
    public function addFont(string $family, string $style = '', string $file = '') : void
776
    {
777
        // Add a TrueType, OpenType or Type1 font
778
        $family = strtolower($family);
779
        if ($file == '') {
780
            $file = str_replace(' ', '', $family) . strtolower($style) . '.php';
781
        }
782
        $style = strtoupper($style);
783
        if ($style == 'IB') {
784
            $style = 'BI';
785
        }
786
        $fontkey = $family.$style;
787
        if (isset($this->fonts[$fontkey])) {
788
            return;
789
        }
790
        $info = $this->loadFont($file);
791
        $info['i'] = count($this->fonts) + 1;
792
        if (!empty($info['file'])) {
793
            // Embedded font
794
            if ($info['type'] == 'TrueType') {
795
                $this->FontFiles[$info['file']] = array('length1'=>$info['originalsize']);
796
            } else {
797
                $this->FontFiles[$info['file']] = array('length1'=>$info['size1'], 'length2'=>$info['size2']);
798
            }
799
        }
800
        $this->fonts[$fontkey] = $info;
801
    }
802
    
803
    /**
804
     * Sets the font used to print character strings. 
805
     * It is mandatory to call this method at least once before printing text or the 
806
     * resulting document would not be valid.
807
     * The font can be either a standard one or a font added via the AddFont() method. 
808
     * Standard fonts use the Windows encoding cp1252 (Western Europe).
809
     * The method can be called before the first page is created and the font is kept from page to page.
810
     * If you just wish to change the current font size, it is simpler to call SetFontSize().<br/>
811
     * 
812
     * <b>Note:</b><br/>
813
     * the font definition files must be accessible. 
814
     * They are searched successively in: <ul>
815
     * <li> The directory defined by the FPDF_FONTPATH constant (if this constant is defined) </li>
816
     * <li> The 'font' directory located in the same directory as fpdf.php (if it exists) </li>
817
     * <li> The directories accessible through include() </li></ul>
818
     * @param string $family    Family font. <br/>
819
     *                          It can be either a name defined by AddFont() or one of the standard families (case insensitive): <ul>
820
     *                          <li> 'Courier' (fixed-width) </li>
821
     *                          <li> 'Helvetica' or 'Arial' (synonymous; sans serif) </li>
822
     *                          <li> 'Times' (serif) </li>
823
     *                          <li> 'Symbol' (symbolic) </li>
824
     *                          <li> 'ZapfDingbats' (symbolic)</li></ul>
825
     *                          It is also possible to pass an empty string. In that case, the current family is kept.<br/>
826
     * @param string $style     Font style. <br>
827
     *                          ossible values are (case insensitive): <ul>
828
     *                          <li> empty string: regular </li>
829
     *                          <li> 'B': bold </li>
830
     *                          <li> 'I': italic </li>
831
     *                          <li> 'U': underline </li> 
832
     *                          <li> or any combination. </li></ul>
833
     *                          The default value is regular. Bold and italic styles do not apply to Symbol and ZapfDingbats.<br/>
834
     * @param float $size       Font size in points. <br/>
835
     *                          The default value is the current size. <br/>
836
     *                          If no size has been specified since the beginning of the document, the value taken is 12.
837
     */
838
    public function setFont(string $family, string $style = '', float $size = 0) : void
839
    {
840
        // Select a font; size given in points
841
        if ($family == '') {
842
            $family = $this->FontFamily;
843
        } else {
844
            $family = strtolower($family);
845
        }
846
        $style = strtoupper($style);
847
        if (strpos($style, 'U') !== false) {
848
            $this->underline = true;
849
            $style = str_replace('U','',$style);
850
        } else {
851
            $this->underline = false;
852
        }
853
        if ($style == 'IB') {
854
            $style = 'BI';
855
        }
856
        if ($size == 0) {
857
            $size = $this->FontSizePt;
858
        }
859
        // Test if font is already selected
860
        if ($this->FontFamily == $family && $this->FontStyle == $style && $this->FontSizePt == $size) {
861
            return;
862
        }
863
        // Test if font is already loaded
864
        $fontkey = $family.$style;
865
        if (!isset($this->fonts[$fontkey])) {
866
            // Test if one of the core fonts
867
            if( $this->isCoreFont($family)) {
868
                if ($family == 'symbol' || $family == 'zapfdingbats') {
869
                    $style = '';
870
                }
871
                $fontkey = $family . $style;
872
                if (!isset($this->fonts[$fontkey])) {
873
                    $this->addFont($family, $style);
874
                }
875
            } else {
876
                $this->error('Undefined font: ' . $family . ' ' . $style);
877
            }
878
        }
879
        // Select it
880
        $this->FontFamily = $family;
881
        $this->FontStyle = $style;
882
        $this->FontSizePt = $size;
883
        $this->FontSize = $size/$this->k;
884
        $this->CurrentFont = &$this->fonts[$fontkey];
885
        if($this->page > 0) {
886
            $this->out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt));
887
        }
888
    }
889
    
890
    /**
891
     * Defines the size of the current font.
892
     * @param float $size   The size (in points).
893
     */
894
    public function setFontSize(float $size) : void
895
    {
896
        // Set font size in points
897
        if ($this->FontSizePt == $size) {
898
            return;
899
        }
900
        $this->FontSizePt = $size;
901
        $this->FontSize = $size / $this->k;
902
        if ($this->page > 0) {
903
            $this->out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt));
904
        }
905
    }
906
907
    /**
908
     * Check for core font
909
     * @param string $strFamily
910
     * @return bool
911
     */
912
    protected function isCoreFont(string &$strFamily) : bool
913
    {
914
        $strFamily = strtolower($strFamily);
915
        if ($strFamily == 'arial') {
916
            $strFamily = 'helvetica';
917
        }
918
        // Core fonts
919
        $aCoreFonts = array('courier', 'helvetica', 'times', 'symbol', 'zapfdingbats');
920
        return in_array($strFamily, $aCoreFonts);
921
    }
922
    
923
    /**
924
     * Creates a new internal link and returns its identifier. 
925
     * An internal link is a clickable area which directs to another place within the document.
926
     * The identifier can then be passed to Cell(), Write(), Image() or Link(). 
927
     * The destination is defined with SetLink().
928
     * @return int
929
     */
930
    public function addLink() : int
931
    {
932
        // Create a new internal link
933
        $n = count($this->links) + 1;
934
        $this->links[$n] = array(0, 0);
935
        return $n;
936
    }
937
    
938
    /**
939
     * Defines the page and position a link points to.
940
     * @param int $link The link identifier created by AddLink().
941
     * @param float $y  Y-position of target position; -1 indicates the current position. The default value is 0 (top of page).
942
     * @param int $page Number of target page; -1 indicates the current page. This is the default value.
943
     */
944
    public function setLink(int $link, float $y = 0, int $page = -1) : void
945
    {
946
        // Set destination of internal link
947
        if ($y == -1) {
948
            $y = $this->y;
949
        }
950
        if ($page == -1) {
951
            $page = $this->page;
952
        }
953
        $this->links[$link] = array($page, $y);
954
    }
955
    
956
    /**
957
     * Puts a link on a rectangular area of the page. 
958
     * Text or image links are generally put via Cell(), Write() or Image(), but this 
959
     * method can be useful for instance to define a clickable area inside an image.
960
     * Target can be an external URL or an internal link ID created and specified by AddLink()/SetLink() 
961
     * @param float $x          X-position
962
     * @param float $y          Y-position
963
     * @param float $w          Width
964
     * @param float $h          Height
965
     * @param string|int $link  URL or link-ID
966
     */
967
    public function link(float $x, float $y, float $w, float $h, $link) : void
968
    {
969
        // Put a link on the page
970
        $this->PageLinks[$this->page][] = array($x * $this->k, $this->hPt - $y * $this->k, $w * $this->k, $h * $this->k, $link);
971
    }
972
    
973
    /**
974
     * Prints a character string. 
975
     * The origin is on the left of the first character, on the baseline. 
976
     * This method allows to place a string precisely on the page, but it is usually 
977
     * easier to use Cell(), MultiCell() or Write() which are the standard methods 
978
     * to print text.
979
     * @param float $x      X-position
980
     * @param float $y      Y-position
981
     * @param string $txt   String to print.
982
     */
983
    public function text(float $x, float $y, string $txt) : void
984
    {
985
        // Output a string
986
        if (!isset($this->CurrentFont)) {
987
            $this->error('No font has been set');
988
        }
989
        $s = sprintf('BT %.2F %.2F Td (%s) Tj ET', $x * $this->k, ($this->h - $y) * $this->k, $this->escape($txt));
990
        if ($this->underline && $txt != '') {
991
            $s .= ' ' . $this->doUnderline($x, $y, $txt);
992
        }
993
        if ($this->ColorFlag) {
994
            $s = 'q ' . $this->TextColor . ' ' . $s . ' Q';
995
        }
996
        $this->out($s);
997
    }
998
    
999
    /**
1000
     * Whenever a page break condition is met, the method is called, and the break is 
1001
     * issued or not depending on the returned value. 
1002
     * The default implementation returns a value according to the mode selected by 
1003
     * SetAutoPageBreak().
1004
     * This method is called automatically and should not be called directly by the application.<br/>
1005
     * <br/>
1006
     * For usage in derived classes see example at http://www.fpdf.org/en/doc/acceptpagebreak.htm.
1007
     * @link http://www.fpdf.org/en/doc/acceptpagebreak.htm
1008
     * @return bool
1009
     */
1010
    public function acceptPageBreak() : bool
1011
    {
1012
        // Accept automatic page break or not
1013
        return $this->AutoPageBreak;
1014
    }
1015
    
1016
    /**
1017
     * Prints a cell (rectangular area) with optional borders, background color and character string. 
1018
     * The upper-left corner of the cell corresponds to the current position. The text can be 
1019
     * aligned or centered. After the call, the current position moves to the right or to the next line. 
1020
     * It is possible to put a link on the text.
1021
     * If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done 
1022
     * before outputting.
1023
     * @param float $w          Cell width. If 0, the cell extends up to the right margin.
1024
     * @param float $h          Cell height. Default value: 0.
1025
     * @param string $txt       String to print. Default value: empty string.
1026
     * @param int|string $border    Indicates if borders must be drawn around the cell. <br/>
1027
     *                          The value can be either a number: <ul>
1028
     *                          <li>0: no border </li>
1029
     *                          <li>1: frame </li></ul>
1030
     *                          or a string containing some or all of the following characters (in any order): <ul>
1031
     *                          <li> 'L': left </li>
1032
     *                          <li> 'T': top </li>
1033
     *                          <li> 'R': right </li>
1034
     *                          <li> 'B': bottom </li></ul>
1035
     *                          Default value: 0. <br/>
1036
     * @param float $ln         Indicates where the current position should go after the call. <br/>
1037
     *                          Possible values are: <ul>
1038
     *                          <li> 0: to the right </li>
1039
     *                          <li> 1: to the beginning of the next line </li>
1040
     *                          <li> 2: below </li></ul>
1041
     *                          Putting 1 is equivalent to putting 0 and calling Ln() just after. <br/>
1042
     *                          Default value: 0. <br/>
1043
     * @param string $align     Allows to center or align the text. <br/>
1044
     *                          Possible values are: <ul>
1045
     *                          <li> 'L' or empty string: left align (default value) </li> 
1046
     *                          <li> 'C': center </li> 
1047
     *                          <li> 'R': right align </li></ul>
1048
     * @param boolean $fill     Indicates if the cell background must be painted (true) or transparent (false). <br/>
1049
     *                          If set to true, current FillColor is used for the background. <br/>
1050
     *                          Default value: false. <br/>
1051
     * @param string|int $link  URL or identifier for internal link created by AddLink().
1052
     */
1053
    public function cell(float $w, float $h = 0, string $txt = '', $border = 0, float $ln = 0, $align = '', $fill = false, $link = '') : void
1054
    {
1055
        // Output a cell
1056
        $k = $this->k;
1057
        if ($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->acceptPageBreak()) {
1058
            // Automatic page break
1059
            $x = $this->x;
1060
            $ws = $this->ws;
1061
            if ($ws > 0) {
1062
                $this->ws = 0;
1063
                $this->out('0 Tw');
1064
            }
1065
            $this->addPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation);
1066
            $this->x = $x;
1067
            if ($ws > 0) {
1068
                $this->ws = $ws;
1069
                $this->out(sprintf('%.3F Tw', $ws * $k));
1070
            }
1071
        }
1072
        if ($w == 0) {
1073
            $w = $this->w - $this->rMargin - $this->x;
1074
        }
1075
        $s = '';
1076
        if ($fill || $border == 1) {
1077
            if ($fill) {
1078
                $op = ($border == 1) ? 'B' : 'f';
1079
            } else {
1080
                $op = 'S';
1081
            }
1082
            $s = sprintf('%.2F %.2F %.2F %.2F re %s ', $this->x * $k, ($this->h - $this->y) * $k, $w * $k, -$h * $k, $op);
1083
        }
1084
        if (is_string($border)) {
1085
            $x = $this->x;
1086
            $y = $this->y;
1087
            if (strpos($border, 'L') !== false) {
1088
                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, $x * $k, ($this->h - ($y + $h)) * $k);
1089
            }
1090
            if (strpos($border, 'T') !== false) {
1091
                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - $y) * $k);
1092
            }
1093
            if (strpos($border, 'R') !== false) {
1094
                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', ($x + $w) * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k);
1095
            }
1096
            if (strpos($border, 'B') !== false) {
1097
                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - ($y + $h)) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k);
1098
            }
1099
        }
1100
        if ($txt !== '') {
1101
            if (!isset($this->CurrentFont)) {
1102
                $this->error('No font has been set');
1103
            }
1104
            if ($align == 'R') {
1105
                $dx = $w - $this->cMargin - $this->getStringWidth($txt);
1106
            } elseif ($align == 'C') {
1107
                $dx = ($w - $this->getStringWidth($txt)) / 2;
1108
            } else {
1109
                $dx = $this->cMargin;
1110
            }
1111
            if ($this->ColorFlag) {
1112
                $s .= 'q ' . $this->TextColor . ' ';
1113
            }
1114
            $s .= sprintf('BT %.2F %.2F Td (%s) Tj ET', ($this->x + $dx) * $k, ($this->h - ($this->y + 0.5 * $h + 0.3 * $this->FontSize)) * $k, $this->escape($txt));
1115
            if ($this->underline) {
1116
                $s .= ' ' . $this->doUnderline($this->x + $dx, $this->y + 0.5 * $h + 0.3 * $this->FontSize, $txt);
1117
            }
1118
            if ($this->ColorFlag) {
1119
                $s .= ' Q';
1120
            }
1121
            if ($link) {
1122
                $this->link($this->x + $dx, $this->y + 0.5 * $h - 0.5 * $this->FontSize, $this->getStringWidth($txt), $this->FontSize, $link);
1123
            }
1124
        }
1125
        if ($s) {
1126
            $this->out($s);
1127
        }
1128
        $this->lasth = $h;
1129
        if ($ln > 0) {
1130
            // Go to next line
1131
            $this->y += $h;
1132
            if ($ln == 1) {
1133
                $this->x = $this->lMargin;
1134
            }
1135
        } else {
1136
            $this->x += $w;
1137
        }
1138
    }
1139
    
1140
    /**
1141
     * This method allows printing text with line breaks. 
1142
     * They can be automatic (as soon as the text reaches the right border of the cell) or 
1143
     * explicit (via the \n character). 
1144
     * As many cells as necessary are output, one below the other.
1145
     * Text can be aligned, centered or justified. The cell block can be framed and the background painted.
1146
     * @param float $w          Cell width. If 0, the cell extends up to the right margin.
1147
     * @param float $h          Cell height. Default value: 0.
1148
     * @param string $txt       String to print. Default value: empty string.
1149
     * @param int|string $border    Indicates if borders must be drawn around the cell. <br/>
1150
     *                          The value can be either a number: <ul>
1151
     *                          <li>0: no border </li>
1152
     *                          <li>1: frame </li></ul>
1153
     *                          or a string containing some or all of the following characters (in any order): <ul>
1154
     *                          <li> 'L': left </li>
1155
     *                          <li> 'T': top </li>
1156
     *                          <li> 'R': right </li>
1157
     *                          <li> 'B': bottom </li></ul>
1158
     *                          Default value: 0. <br/>
1159
     * @param string $align     Allows to center or align the text. <br/>
1160
     *                          Possible values are: <ul>
1161
     *                          <li> 'L' or empty string: left align (default value) </li> 
1162
     *                          <li> 'C': center </li> 
1163
     *                          <li> 'R': right align </li></ul>
1164
     * @param boolean $fill     Indicates if the cell background must be painted (true) or transparent (false). <br/>
1165
     *                          If set to true, current FillColor is used for the background. <br/>
1166
     *                          Default value: false.
1167
     */
1168
    public function multiCell(float $w, float $h, string $txt, $border=0, string $align='J', bool $fill=false) : void
1169
    {
1170
        // Output text with automatic or explicit line breaks
1171
        if (!isset($this->CurrentFont)) {
1172
            $this->error('No font has been set');
1173
        }
1174
        $cw = &$this->CurrentFont['cw'];
1175
        if ($w == 0) {
1176
            $w = $this->w - $this->rMargin - $this->x;
1177
        }
1178
        $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1179
        $s = str_replace("\r", '', $txt);
1180
        $nb = strlen($s);
1181
        if ($nb > 0 && $s[$nb-1] == "\n") {
1182
            $nb--;
1183
        }
1184
        $b = 0;
1185
        $b2 = '';
1186
        if ($border) {
1187
            if ($border == 1) {
1188
                $border = 'LTRB';
1189
                $b = 'LRT';
1190
                $b2 = 'LR';
1191
            } else {
1192
                $b2 = '';
1193
                if (strpos($border, 'L') !== false) {
1194
                    $b2 .= 'L';
1195
                }
1196
                if (strpos($border, 'R') !== false) {
1197
                    $b2 .= 'R';
1198
                }
1199
                $b = (strpos($border,'T') !== false) ? $b2.'T' : $b2;
1200
            }
1201
        }
1202
        $sep = -1;
1203
        $i = 0;
1204
        $j = 0;
1205
        $l = 0;
1206
        $ns = 0;
1207
        $nl = 1;
1208
        $ls = 0;
1209
        while ($i < $nb) {
1210
            // Get next character
1211
            $c = $s[$i];
1212
            if ($c == "\n") {
1213
                // Explicit line break
1214
                if ($this->ws > 0) {
1215
                    $this->ws = 0;
1216
                    $this->out('0 Tw');
1217
                }
1218
                $this->cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill);
1219
                $i++;
1220
                $sep = -1;
1221
                $j = $i;
1222
                $l = 0;
1223
                $ns = 0;
1224
                $nl++;
1225
                if ($border && $nl == 2) {
1226
                    $b = $b2;
1227
                }
1228
                continue;
1229
            }
1230
            if ($c == ' ') {
1231
                $sep = $i;
1232
                $ls = $l;
1233
                $ns++;
1234
            }
1235
            $l += $cw[$c];
1236
            if ($l > $wmax) {
1237
                // Automatic line break
1238
                if ($sep == -1) {
1239
                    if ($i == $j) {
1240
                        $i++;
1241
                    }
1242
                    if ($this->ws > 0) {
1243
                        $this->ws = 0;
1244
                        $this->out('0 Tw');
1245
                    }
1246
                    $this->cell($w, $h, substr($s, $j, $i-$j), $b, 2, $align, $fill);
1247
                } else {
1248
                    if ($align == 'J') {
1249
                        $this->ws = ($ns > 1) ? ($wmax - $ls) / 1000 * $this->FontSize / ($ns - 1) : 0;
1250
                        $this->out(sprintf('%.3F Tw', $this->ws * $this->k));
1251
                    }
1252
                    $this->cell($w, $h, substr($s, $j, $sep - $j), $b, 2, $align, $fill);
1253
                    $i = $sep + 1;
1254
                }
1255
                $sep = -1;
1256
                $j = $i;
1257
                $l = 0;
1258
                $ns = 0;
1259
                $nl++;
1260
                if ($border && $nl == 2) {
1261
                    $b = $b2;
1262
                }
1263
            } else {
1264
                $i++;
1265
            }
1266
        }
1267
        // Last chunk
1268
        if ($this->ws > 0) {
1269
            $this->ws = 0;
1270
            $this->out('0 Tw');
1271
        }
1272
        if ($border && strpos($border, 'B') !== false) {
1273
            $b .= 'B';
1274
        }
1275
        $this->cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill);
1276
        $this->x = $this->lMargin;
1277
    }
1278
    
1279
    /**
1280
     * This method prints text from the current position. 
1281
     * When the right margin is reached (or the \n character is met) a line break occurs 
1282
     * and text continues from the left margin. Upon method exit, the current position 
1283
     * is left just at the end of the text.
1284
     * It is possible to put a link on the text.
1285
     * @param float $h          Line height.
1286
     * @param string $txt       String to print.
1287
     * @param string|int $link  URL or identifier for internal link created by AddLink().
1288
     */
1289
    public function write(float $h, string $txt, $link = '') : void
1290
    {
1291
        // Output text in flowing mode
1292
        if (!isset($this->CurrentFont)) {
1293
            $this->error('No font has been set');
1294
        }
1295
        $cw = &$this->CurrentFont['cw'];
1296
        $w = $this->w - $this->rMargin - $this->x;
1297
        $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1298
        $s = str_replace("\r", '', $txt);
1299
        $nb = strlen($s);
1300
        $sep = -1;
1301
        $i = 0;
1302
        $j = 0;
1303
        $l = 0;
1304
        $nl = 1;
1305
        while ($i < $nb) {
1306
            // Get next character
1307
            $c = $s[$i];
1308
            if ($c == "\n") {
1309
                // Explicit line break
1310
                $this->cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link);
1311
                $i++;
1312
                $sep = -1;
1313
                $j = $i;
1314
                $l = 0;
1315
                if ($nl == 1) {
1316
                    $this->x = $this->lMargin;
1317
                    $w = $this->w - $this->rMargin - $this->x;
1318
                    $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1319
                }
1320
                $nl++;
1321
                continue;
1322
            }
1323
            if ($c == ' ') {
1324
                $sep = $i;
1325
            }
1326
            $l += $cw[$c];
1327
            if ($l > $wmax) {
1328
                // Automatic line break
1329
                if($sep == -1) {
1330
                    if($this->x > $this->lMargin) {
1331
                        // Move to next line
1332
                        $this->x = $this->lMargin;
1333
                        $this->y += $h;
1334
                        $w = $this->w - $this->rMargin - $this->x;
1335
                        $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1336
                        $i++;
1337
                        $nl++;
1338
                        continue;
1339
                    }
1340
                    if ($i == $j) {
1341
                        $i++;
1342
                    }
1343
                    $this->cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link);
1344
                } else {
1345
                    $this->cell($w, $h, substr($s, $j, $sep - $j), 0, 2, '', false, $link);
1346
                    $i = $sep + 1;
1347
                }
1348
                $sep = -1;
1349
                $j = $i;
1350
                $l = 0;
1351
                if ($nl == 1) {
1352
                    $this->x = $this->lMargin;
1353
                    $w = $this->w - $this->rMargin - $this->x;
1354
                    $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1355
                }
1356
                $nl++;
1357
            } else {
1358
                $i++;
1359
            }
1360
        }
1361
        // Last chunk
1362
        if ($i != $j) {
1363
            $this->cell($l / 1000 * $this->FontSize, $h, substr($s, $j), 0, 0, '', false, $link);
1364
        }
1365
    }
1366
    
1367
    /**
1368
     * Performs a line break. 
1369
     * The current X-position goes back to the left margin and the Y-position increases by 
1370
     * the amount passed in parameter.
1371
     * @param float $h  The height of the break. <br/>
1372
     *                  By default, the value equals the height of the last printed cell.
1373
     */
1374
    public function ln(float $h = null) : void
1375
    {
1376
        // Line feed; default value is the last cell height
1377
        $this->x = $this->lMargin;
1378
        if ($h === null) {
1379
            $this->y += $this->lasth;
1380
        } else {
1381
            $this->y += $h;
1382
        }
1383
    }
1384
    
1385
    /**
1386
     * Puts an image. 
1387
     * The size it will take on the page can be specified in different ways: <ul>
1388
     * <li> explicit width and height (expressed in user unit or dpi) </li> 
1389
     * <li> one explicit dimension, the other being calculated automatically in order to keep the original proportions </li> 
1390
     * <li> no explicit dimension, in which case the image is put at 96 dpi </li></ul>
1391
     * Supported formats are JPEG, PNG and GIF. <b>The GD extension is required for GIF.</b><br/>
1392
     * For JPEGs, all flavors are allowed: <ul>
1393
     * <li> gray scales </li> 
1394
     * <li> true colors (24 bits) </li>
1395
     * <li> CMYK (32 bits) </li></ul>
1396
     * For PNGs, are allowed: <ul>
1397
     * <li> gray scales on at most 8 bits (256 levels) </li>
1398
     * <li> indexed colors </li>
1399
     * <li> true colors (24 bits) </li></ul>
1400
     * For GIFs: in case of an animated GIF, only the first frame is displayed. <br/><br/>
1401
     * Transparency is supported. <br/><br/>
1402
     * The format can be specified explicitly or inferred from the file extension. <br/><br/>
1403
     * It is possible to put a link on the image. <br/><br/>
1404
     * <b>Remark:</b> if an image is used several times, only one copy is embedded in the file.
1405
     * @param string $file  Path or URL of the image.
1406
     * @param float $x      X-position of the upper-left corner. <br/> <br/>
1407
     *                      If not specified or equal to null, the current X-position is used. <br/>
1408
     * @param float $y      Y-position of the upper-left corner. <br/>
1409
     *                      If not specified or equal to null, the current Y-position is used; <br/>
1410
     *                      moreover, a page break is triggered first if necessary (in case automatic page breaking is enabled) and, <br/>
1411
     *                      after the call, the current Y-position is moved to the bottom of the image. <br/>
1412
     * @param float $w      Width of the image in the page. <br/>
1413
     *                      There are three cases: <ul>
1414
     *                      <li> If the value is positive, it represents the width in user unit </li> 
1415
     *                      <li> If the value is negative, the absolute value represents the horizontal resolution in dpi </li> 
1416
     *                      <li> If the value is not specified or equal to zero, it is automatically calculated </li><ul>
1417
     * @param float $h      Height of the image in the page. <br/>
1418
     *                      There are three cases: <ul>
1419
     *                      <li> If the value is positive, it represents the height in user unit </li> 
1420
     *                      <li> If the value is negative, the absolute value represents the vertical resolution in dpi </li> 
1421
     *                      <li> If the value is not specified or equal to zero, it is automatically calculated </li><ul>
1422
     * @param string $type  Image format. <br/>
1423
     *                      Possible values are (case insensitive): <ul> 
1424
     *                      <li> JPG </li> 
1425
     *                      <li> JPEG </li> 
1426
     *                      <li> PNG </li>
1427
     *                      <li> GIF </li></ul>
1428
     *                      If not specified, the type is inferred from the file extension. <br/>
1429
     * @param string|int $link  URL or identifier for internal link created by AddLink().
1430
     */
1431
    public function image(string $file, ?float $x = null, ?float $y = null, float $w = 0, float $h = 0, string $type = '', $link = '') : void
1432
    {
1433
        // Put an image on the page
1434
        if ($file == '') {
1435
            $this->error('Image file name is empty');
1436
            return;
1437
        }
1438
        if (!isset($this->images[$file])) {
1439
            // First use of this image, get info
1440
            if ($type == '') {
1441
                $pos = strrpos($file, '.');
1442
                if (!$pos) {
1443
                    $this->error('Image file has no extension and no type was specified: ' . $file);
1444
                    return;
1445
                }
1446
                $type = substr($file, $pos + 1);
1447
            }
1448
            $type = strtolower($type);
1449
            if($type=='jpeg') {
1450
                $type = 'jpg';
1451
            }
1452
            $mtd = 'parse' . ucfirst($type);
1453
            if (!method_exists($this, $mtd)) {
1454
                $this->error('Unsupported image type: ' . $type);
1455
            }
1456
            $info = $this->$mtd($file);
1457
            $info['i'] = count($this->images) + 1;
1458
            $this->images[$file] = $info;
1459
        } else {
1460
            $info = $this->images[$file];
1461
        }
1462
    
1463
        // Automatic width and height calculation if needed
1464
        if ($w == 0 && $h == 0) {
1465
            // Put image at 96 dpi
1466
            $w = -96;
1467
            $h = -96;
1468
        }
1469
        if ($w < 0) {
1470
            $w = -$info['w'] * 72 / $w / $this->k;
1471
        }
1472
        if ($h < 0) {
1473
            $h = -$info['h'] * 72 / $h / $this->k;
1474
        }
1475
        if ($w == 0) {
1476
            $w = $h * $info['w'] / $info['h'];
1477
        }
1478
        if ($h == 0) {
1479
            $h = $w * $info['h'] / $info['w'];
1480
        }
1481
    
1482
        // Flowing mode
1483
        if ($y === null) {
1484
            if ($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->acceptPageBreak()) {
1485
                // Automatic page break
1486
                $x2 = $this->x;
1487
                $this->addPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation);
1488
                $this->x = $x2;
1489
            }
1490
            $y = $this->y;
1491
            $this->y += $h;
1492
        }
1493
    
1494
        if ($x === null) {
1495
            $x = $this->x;
1496
        }
1497
        $this->out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q', $w * $this->k, $h * $this->k, $x * $this->k, ($this->h - ($y + $h)) * $this->k, $info['i']));
1498
        if ($link) {
1499
            $this->link($x,$y,$w,$h,$link);
1500
        }
1501
    }
1502
    
1503
    /**
1504
     * Set bookmark at current position.
1505
     * <b>from FPDF.org extension to create Bookmarks</b>
1506
     * @param string $txt
1507
     * @param bool $isUTF8
1508
     * @param int $level
1509
     * @param int $y
1510
     */
1511
    public function bookmark(string $txt, bool $isUTF8=false, int $level=0, int $y=0) : void
1512
    {
1513
        if (!$isUTF8) {
1514
            $txt = utf8_encode($txt);
1515
        }
1516
        if ($y == -1) {
1517
            $y = $this->getY();
1518
        }
1519
        $this->outlines[] = array('t' => $txt, 'l' => $level, 'y' => ($this->h - $y) * $this->k, 'p' => $this->pageNo());
1520
    }
1521
    
1522
    /**
1523
     * Get current page width.
1524
     * @return float
1525
     */
1526
    public function getPageWidth() : float
1527
    {
1528
        // Get current page width
1529
        return $this->w;
1530
    }
1531
    
1532
    /**
1533
     * Get current page height.
1534
     * @return float
1535
     */
1536
    public function getPageHeight() : float
1537
    {
1538
        // Get current page height
1539
        return $this->h;
1540
    }
1541
    
1542
    /**
1543
     * Get current x position.
1544
     * @return float
1545
     */
1546
    public function getX() : float
1547
    {
1548
        // GetX position
1549
        return $this->x;
1550
    }
1551
    
1552
    /**
1553
     * Set new X position.
1554
     * If the passed value is negative, it is relative to the right of the page.
1555
     * @param float $x
1556
     */
1557
    public function setX(float $x) : void
1558
    {
1559
        // Set x position
1560
        if ($x >= 0) {
1561
            $this->x = $x;
1562
        } else {
1563
            $this->x = $this->w + $x;
1564
        }
1565
    }
1566
1567
    /**
1568
     * Get current Y position.
1569
     * @return float
1570
     */
1571
    public function getY() : float
1572
    {
1573
        // Get y position
1574
        return $this->y;
1575
    }
1576
1577
    /**
1578
     * Set new Y position and optionally moves the current X-position back to the left margin.
1579
     * If the passed value is negative, it is relative to the bottom of the page.
1580
     * @param float $y
1581
     * @param bool $resetX
1582
     */
1583
    public function setY(float $y, bool $resetX = true) : void
1584
    {
1585
        // Set y position and optionally reset x
1586
        if ($y >= 0) {
1587
            $this->y = $y;
1588
        } else {
1589
            $this->y = $this->h+$y;
1590
        }
1591
        if ($resetX) {
1592
            $this->x = $this->lMargin;
1593
        }
1594
    }
1595
    
1596
    /**
1597
     * Set new X and Y position.
1598
     * If the passed values are negative, they are relative respectively to the right and bottom of the page.
1599
     * @param float $x
1600
     * @param float $y
1601
     */
1602
    public function setXY(float $x, float $y) : void
1603
    {
1604
        // Set x and y positions
1605
        $this->setX($x);
1606
        $this->setY($y, false);
1607
    }
1608
    
1609
    /**
1610
     * Send the document to a given destination: browser, file or string. 
1611
     * In the case of a browser, the PDF viewer may be used or a download may be forced.
1612
     * The method first calls Close() if necessary to terminate the document.
1613
     * @param string $dest  Destination where to send the document. <br/>
1614
     *                      It can be one of the following: <ul> 
1615
     *                      <li> 'I': send the file inline to the browser. The PDF viewer is used if available. </li> 
1616
     *                      <li> 'D': send to the browser and force a file download with the name given by name. </li> 
1617
     *                      <li> 'F': save to a local file with the name given by name (may include a path). </li> 
1618
     *                      <li> 'S': return the document as a string. </li></ul>
1619
     *                      The default value is I. <br/>
1620
     * @param string $name  The name of the file. It is ignored in case of destination 'S'. <br/>
1621
     *                      The default value is doc.pdf. <br/>
1622
     * @param bool $isUTF8  Indicates if name is encoded in ISO-8859-1 (false) or UTF-8 (true). <br/>
1623
     *                      Only used for destinations I and D. <br/>
1624
     *                      The default value is false. <br/>
1625
     * @return string
1626
     */
1627
    public function output(string $dest = '', string $name = '', bool $isUTF8 = false) : string
1628
    {
1629
        // Output PDF to some destination
1630
        $this->close();
1631
        if (strlen($name) == 1 && strlen($dest) != 1) {
1632
            // Fix parameter order
1633
            $tmp = $dest;
1634
            $dest = $name;
1635
            $name = $tmp;
1636
        }
1637
        if ($dest == '') {
1638
            $dest = 'I';
1639
        }
1640
        if ($name == '') {
1641
            $name = 'doc.pdf';
1642
        }
1643
        switch (strtoupper($dest)) {
1644
            case 'I':
1645
                // Send to standard output
1646
                $this->checkOutput();
1647
                if (PHP_SAPI != 'cli') {
1648
                    // We send to a browser
1649
                    header('Content-Type: application/pdf; charset=UTF-8');
1650
                    header('Content-Disposition: inline; ' . $this->httpEncode('filename', $name, $isUTF8));
1651
                    header('Cache-Control: private, max-age=0, must-revalidate');
1652
                    header('Pragma: public');
1653
                }
1654
                echo $this->buffer;
1655
                break;
1656
            case 'D':
1657
                // Download file
1658
                $this->checkOutput();
1659
                header('Content-Type: application/x-download');
1660
                header('Content-Disposition: attachment; ' . $this->httpEncode('filename', $name, $isUTF8));
1661
                header('Cache-Control: private, max-age=0, must-revalidate');
1662
                header('Pragma: public');
1663
                echo $this->buffer;
1664
                break;
1665
            case 'F':
1666
                // Save to local file
1667
                if (!file_put_contents($name, $this->buffer)) {
1668
                    $this->error('Unable to create output file: ' . $name);
1669
                }
1670
                break;
1671
            case 'S':
1672
                // Return as a string
1673
                return $this->buffer;
1674
            default:
1675
                $this->error('Incorrect output destination: ' . $dest);
1676
        }
1677
        return '';
1678
    }
1679
1680
    /**
1681
     * Some internal checks before starting.
1682
     */
1683
    protected function doChecks() : void
1684
    {
1685
        // Check mbstring overloading
1686
        if ((ini_get('mbstring.func_overload') & 2) !== 0) {
1687
            $this->error('mbstring overloading must be disabled');
1688
        }
1689
    }
1690
1691
    /**
1692
     * get the path for the font definitions
1693
     */
1694
    protected function getFontPath() : void 
1695
    {
1696
        // Font path
1697
        $this->fontpath = '';
1698
        if (defined('FPDF_FONTPATH')) {
1699
            $this->fontpath = FPDF_FONTPATH;
1700
            if (substr($this->fontpath, -1) != '/' && substr($this->fontpath, -1) != '\\') {
1701
                $this->fontpath .= '/';
1702
            }
1703
        } elseif (is_dir(dirname(__FILE__) . '/font')) {
1704
            $this->fontpath = dirname(__FILE__) . '/font/';
1705
        }
1706
    }
1707
    
1708
    /**
1709
     * Some internal checks before output.
1710
     */
1711
    protected function checkOutput() : void
1712
    {
1713
        if (PHP_SAPI != 'cli') {
1714
            $file = '';
1715
            $line = 0;
1716
            if (headers_sent($file, $line)) {
1717
                $this->error("Some data has already been output, can't send PDF file (output started at $file:$line)");
1718
            }
1719
        }
1720
        if (ob_get_length()) {
1721
            // The output buffer is not empty
1722
            if (preg_match('/^(\xEF\xBB\xBF)?\s*$/', ob_get_contents())) {
1723
                // It contains only a UTF-8 BOM and/or whitespace, let's clean it
1724
                ob_clean();
1725
            } else {
1726
                $this->error("Some data has already been output, can't send PDF file");
1727
            }
1728
        }
1729
    }
1730
    
1731
    /**
1732
     * Get dimensions of selected pagesize.
1733
     * @param string|array $size
1734
     * @return array
1735
     */
1736
    protected function getPageSize($size) : array
1737
    {
1738
        if (is_string($size)) {
1739
            $aStdPageSizes = array(
1740
                'a3'=>array(841.89, 1190.55),
1741
                'a4'=>array(595.28, 841.89),
1742
                'a5'=>array(420.94, 595.28),
1743
                'letter'=>array(612, 792),
1744
                'legal'=>array(612, 1008)
1745
            );
1746
            
1747
            $size = strtolower($size);
1748
            if (!isset($aStdPageSizes[$size])) {
1749
                $this->error('Unknown page size: ' . $size);
1750
                $a = $aStdPageSizes['a4'];
1751
            } else {
1752
                $a = $aStdPageSizes[$size];
1753
            }
1754
            return array($a[0] / $this->k, $a[1] / $this->k);
1755
        } else {
1756
            if ($size[0] > $size[1]) {
1757
                return array($size[1], $size[0]);
1758
            } else {
1759
                return $size;
1760
            }
1761
        }
1762
    }
1763
    
1764
    /**
1765
     * Start new page.
1766
     * @param string $orientation
1767
     * @param string|array $size
1768
     * @param int $rotation
1769
     */
1770
    protected function beginPage(string $orientation, $size, int $rotation) : void
1771
    {
1772
        $this->page++;
1773
        $this->pages[$this->page] = '';
1774
        $this->state = 2;
1775
        $this->x = $this->lMargin;
1776
        $this->y = $this->tMargin;
1777
        $this->FontFamily = '';
1778
        // Check page size and orientation
1779
        if ($orientation == '') {
1780
            $orientation = $this->DefOrientation;
1781
        } else {
1782
            $orientation = strtoupper($orientation[0]);
1783
        }
1784
        if ($size == '') {
1785
            $size = $this->DefPageSize;
1786
        } else {
1787
            $size = $this->getPageSize($size);
1788
        }
1789
        if ($orientation != $this->CurOrientation || $size[0] != $this->CurPageSize[0] || $size[1] != $this->CurPageSize[1]) {
1790
            // New size or orientation
1791
            if ($orientation == 'P') {
1792
                $this->w = $size[0];
1793
                $this->h = $size[1];
1794
            } else {
1795
                $this->w = $size[1];
1796
                $this->h = $size[0];
1797
            }
1798
            $this->wPt = $this->w * $this->k;
1799
            $this->hPt = $this->h * $this->k;
1800
            $this->PageBreakTrigger = $this->h - $this->bMargin;
1801
            $this->CurOrientation = $orientation;
1802
            $this->CurPageSize = $size;
1803
        }
1804
        if($orientation != $this->DefOrientation || $size[0] != $this->DefPageSize[0] || $size[1] != $this->DefPageSize[1]) {
1805
            $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt);
1806
        }
1807
        if ($rotation != 0) {
1808
            if ($rotation % 90 != 0) {
1809
                $this->error('Incorrect rotation value: ' . $rotation);
1810
            }
1811
            $this->CurRotation = $rotation;
1812
            $this->PageInfo[$this->page]['rotation'] = $rotation;
1813
        }
1814
    }
1815
    
1816
    /**
1817
     * End of current page.
1818
     */
1819
    protected function endPage() : void
1820
    {
1821
        $this->state = 1;
1822
    }
1823
    
1824
    /**
1825
     * Load a font definition file from the font directory.
1826
     * @param string $font
1827
     * @return array
1828
     */
1829
    protected function loadFont(string $font) : array
1830
    {
1831
        // Load a font definition file from the font directory
1832
        if (strpos($font, '/') !== false || strpos($font, "\\") !== false) {
1833
            $this->error('Incorrect font definition file name: ' . $font);
1834
            return [];
1835
        }
1836
        // following vars must be initialized in the font definition file beeing included
1837
        $name = null; 
1838
        $enc = null;
1839
        $subsetted = null;
1840
        include($this->fontpath . $font);
1841
        
1842
        // phpstan can't see the code dynamicly included before so assuming $name, $enc, $subsetted always set to null!
1843
        if (!isset($name)) {            /* @phpstan-ignore-line */
1844
            $this->error('Could not include font definition file');
1845
        }
1846
        if (isset($enc)) {              /* @phpstan-ignore-line */
1847
            $enc = strtolower($enc);
1848
        }
1849
        if (!isset($subsetted)) {       /* @phpstan-ignore-line */
1850
            $subsetted = false;
1851
        }
1852
        return get_defined_vars();
1853
    }
1854
    
1855
    /**
1856
     * Check if string only contains ascii chars (0...127).
1857
     * @param string $s
1858
     * @return bool
1859
     */
1860
    protected function isAscii(string $s) : bool
1861
    {
1862
        // Test if string is ASCII
1863
        $nb = strlen($s);
1864
        for ($i = 0; $i < $nb; $i++) {
1865
            if (ord($s[$i]) > 127) {
1866
                return false;
1867
            }
1868
        }
1869
        return true;
1870
    }
1871
    
1872
    /**
1873
     * @param string $param
1874
     * @param string $value
1875
     * @param bool $isUTF8
1876
     * @return string
1877
     */
1878
    protected function httpEncode(string $param, string $value, bool $isUTF8) : string
1879
    {
1880
        // Encode HTTP header field parameter
1881
        if ($this->isAscii($value)) {
1882
            return $param . '="' . $value . '"';
1883
        }
1884
        if (!$isUTF8) {
1885
            $value = utf8_encode($value);
1886
        }
1887
        if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false) {
1888
            return $param . '="' . rawurlencode($value) . '"';
1889
        } else {
1890
            return $param . "*=UTF-8''" . rawurlencode($value);
1891
        }
1892
    }
1893
    
1894
    /**
1895
     * Convert UTF8 to UTF16.
1896
     * @param string $s
1897
     * @return string
1898
     */
1899
    protected function convUTF8toUTF16(string $s) : string
1900
    {
1901
        // Convert UTF-8 to UTF-16BE with BOM
1902
        $res = "\xFE\xFF";
1903
        $nb = strlen($s);
1904
        $i = 0;
1905
        while ($i < $nb) {
1906
            $c1 = ord($s[$i++]);
1907
            if ($c1 >= 224) {
1908
                // 3-byte character
1909
                $c2 = ord($s[$i++]);
1910
                $c3 = ord($s[$i++]);
1911
                $res .= chr((($c1 & 0x0F) << 4) + (($c2 & 0x3C) >> 2));
1912
                $res .= chr((($c2 & 0x03) << 6) + ($c3 & 0x3F));
1913
            } elseif ($c1 >= 192) {
1914
                // 2-byte character
1915
                $c2 = ord($s[$i++]);
1916
                $res .= chr(($c1 & 0x1C) >> 2);
1917
                $res .= chr((($c1 & 0x03) << 6) + ($c2 & 0x3F));
1918
            } else {
1919
                // Single-byte character
1920
                $res .= "\0" . chr($c1);
1921
            }
1922
        }
1923
        return $res;
1924
    }
1925
    
1926
    /**
1927
     * Escape special characters.
1928
     * @param string $s
1929
     * @return string
1930
     */
1931
    protected function escape(string $s) : string
1932
    {
1933
        // Escape special characters 
1934
        if (strpos($s, '(') !== false || 
1935
            strpos($s, ')') !== false || 
1936
            strpos($s, '\\') !== false || 
1937
            strpos($s, "\r") !== false) {
1938
            $s = str_replace(array('\\','(',')',"\r"), array('\\\\','\\(','\\)','\\r'), $s);
1939
        }
1940
        return $s;
1941
    }
1942
    
1943
    /**
1944
     * Format a text string.
1945
     * @param string $s
1946
     * @return string
1947
     */
1948
    protected function textString(string $s) : string
1949
    {
1950
        // Format a text string
1951
        if (!$this->isAscii($s)) {
1952
            $s = $this->convUTF8toUTF16($s);
1953
        }
1954
        return '(' . $this->escape($s) . ')';
1955
    }
1956
    
1957
    /**
1958
     * Underline text with 'simple' line.
1959
     * @param float $x
1960
     * @param float $y
1961
     * @param string $txt
1962
     * @return string
1963
     */
1964
    protected function doUnderline(float $x, float $y, string $txt) : string
1965
    {
1966
        // Underline text
1967
        $up = $this->CurrentFont['up'];
1968
        $ut = $this->CurrentFont['ut'];
1969
        $w = $this->getStringWidth($txt) + $this->ws * substr_count($txt,' ');
1970
        return sprintf('%.2F %.2F %.2F %.2F re f', $x * $this->k, ($this->h - ($y - $up / 1000 * $this->FontSize)) * $this->k, $w * $this->k, -$ut / 1000 * $this->FontSizePt);
1971
    }
1972
    
1973
    /**
1974
     * Extract info from a JPEG file.
1975
     * @param string $file
1976
     * @return array
1977
     */
1978
    protected function parseJpg(string $file) : array
1979
    {
1980
        // Extract info from a JPEG file
1981
        $a = getimagesize($file);
1982
        if (!$a) {
1983
            $this->error('Missing or incorrect image file: ' . $file);
1984
            return [];
1985
        }
1986
        if ($a[2] != 2) {
1987
            $this->error('Not a JPEG file: ' . $file);
1988
            return [];
1989
        }
1990
        if (!isset($a['channels']) || $a['channels'] == 3) {
1991
            $colspace = 'DeviceRGB';
1992
        } elseif ($a['channels'] == 4) {
1993
            $colspace = 'DeviceCMYK';
1994
        } else {
1995
            $colspace = 'DeviceGray';
1996
        }
1997
        $bpc = isset($a['bits']) ? $a['bits'] : 8;
1998
        $data = file_get_contents($file);
1999
        return array('w' => $a[0], 'h' => $a[1], 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'DCTDecode', 'data' => $data);
2000
    }
2001
    
2002
    /**
2003
     * Extract info from a PNG file.
2004
     * @param string $file
2005
     * @return array
2006
     */
2007
    protected function parsePng(string $file) : array
2008
    {
2009
        // Extract info from a PNG file
2010
        $f = fopen($file, 'rb');
2011
        if ($f === false) {
2012
            $this->error('Can\'t open image file: ' . $file);
2013
            return [];
2014
        }
2015
        $info = $this->parsePngStream($f, $file);
2016
        fclose($f);
2017
        return $info;
2018
    }
2019
    
2020
    /**
2021
     * Extract info from a PNG stream
2022
     * @param resource $f
2023
     * @param string $file
2024
     * @return array
2025
     */
2026
    protected function parsePngStream($f, string $file) : array
2027
    {
2028
        // Check signature
2029
        if ($this->readStream($f, 8) != chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) {
2030
            $this->error('Not a PNG file: ' . $file);
2031
            return [];
2032
        }
2033
    
2034
        // Read header chunk
2035
        $this->readStream($f, 4);
2036
        if ($this->readStream($f, 4) != 'IHDR') {
2037
            $this->error('Incorrect PNG file: ' . $file);
2038
            return [];
2039
        }
2040
        $w = $this->readInt($f);
2041
        $h = $this->readInt($f);
2042
        $bpc = ord($this->readStream($f, 1));
2043
        if ($bpc > 8) {
2044
            $this->error('16-bit depth not supported: ' . $file);
2045
            return [];
2046
        }
2047
        $ct = ord($this->readStream($f, 1));
2048
        $colspace = '';
2049
        if ($ct == 0 || $ct == 4) {
2050
            $colspace = 'DeviceGray';
2051
        } elseif ($ct == 2 || $ct == 6) {
2052
            $colspace = 'DeviceRGB';
2053
        } elseif ($ct == 3) {
2054
            $colspace = 'Indexed';
2055
        } else {
2056
            $this->error('Unknown color type: ' . $file);
2057
            return [];
2058
        }
2059
        if (ord($this->readStream($f, 1)) != 0) {
2060
            $this->error('Unknown compression method: ' . $file);
2061
            return [];
2062
        }
2063
        if (ord($this->readStream($f, 1)) != 0) {
2064
            $this->error('Unknown filter method: ' . $file);
2065
            return [];
2066
        }
2067
        if (ord($this->readStream($f, 1)) != 0) {
2068
            $this->error('Interlacing not supported: ' . $file);
2069
            return [];
2070
        }
2071
        $this->readStream($f, 4);
2072
        $dp = '/Predictor 15 /Colors ' . ($colspace == 'DeviceRGB' ? 3 : 1) . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $w;
2073
    
2074
        // Scan chunks looking for palette, transparency and image data
2075
        $pal = '';
2076
        $trns = '';
2077
        $data = '';
2078
        do {
2079
            $n = $this->readInt($f);
2080
            $type = $this->readStream($f, 4);
2081
            if ($type == 'PLTE') {
2082
                // Read palette
2083
                $pal = $this->readStream($f, $n);
2084
                $this->readStream($f, 4);
2085
            } elseif ($type == 'tRNS') {
2086
                // Read transparency info
2087
                $t = $this->readStream($f, $n);
2088
                if ($ct == 0) {
2089
                    $trns = array(ord(substr($t, 1, 1)));
2090
                } elseif ($ct == 2) {
2091
                    $trns = array(ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1)));
2092
                } else {
2093
                    $pos = strpos($t, chr(0));
2094
                    if ($pos !== false) {
2095
                        $trns = array($pos);
2096
                    }
2097
                }
2098
                $this->readStream($f,4);
2099
            } elseif ($type == 'IDAT') {
2100
                // Read image data block
2101
                $data .= $this->readStream($f, $n);
2102
                $this->readStream($f, 4);
2103
            } elseif ($type == 'IEND') {
2104
                break;
2105
            } else {
2106
                $this->readStream($f, $n + 4);
2107
            }
2108
        } while ($n);
2109
    
2110
        if ($colspace == 'Indexed' && empty($pal)) {
2111
            $this->error('Missing palette in ' . $file);
2112
            return [];
2113
        }
2114
        $info = array(
2115
            'w' => $w, 
2116
            'h' => $h, 
2117
            'cs' => $colspace, 
2118
            'bpc' => $bpc, 
2119
            'f' => 'FlateDecode', 
2120
            'dp' => $dp, 
2121
            'pal' => $pal, 
2122
            'trns' => $trns
2123
        );
2124
        
2125
        if ($ct >= 4) {
2126
            // Extract alpha channel
2127
            if (!function_exists('gzuncompress')) {
2128
                $this->error('Zlib not available, can\'t handle alpha channel: ' . $file);
2129
                return [];
2130
            }
2131
            $data = gzuncompress($data);
2132
            $color = '';
2133
            $alpha = '';
2134
            if ($ct == 4) {
2135
                // Gray image
2136
                $len = 2*$w;
2137
                for ($i = 0; $i < $h; $i++) {
2138
                    $pos = (1 + $len) * $i;
2139
                    $color .= $data[$pos];
2140
                    $alpha .= $data[$pos];
2141
                    $line = substr($data, $pos + 1, $len);
2142
                    $color .= preg_replace('/(.)./s', '$1', $line);
2143
                    $alpha .= preg_replace('/.(.)/s', '$1', $line);
2144
                }
2145
            } else {
2146
                // RGB image
2147
                $len = 4 * $w;
2148
                for ($i = 0; $i < $h; $i++) {
2149
                    $pos = (1 + $len) * $i;
2150
                    $color .= $data[$pos];
2151
                    $alpha .= $data[$pos];
2152
                    $line = substr($data, $pos + 1, $len);
2153
                    $color .= preg_replace('/(.{3})./s', '$1', $line);
2154
                    $alpha .= preg_replace('/.{3}(.)/s', '$1', $line);
2155
                }
2156
            }
2157
            unset($data);
2158
            $data = gzcompress($color);
2159
            $info['smask'] = gzcompress($alpha);
2160
            $this->WithAlpha = true;
2161
            if ($this->PDFVersion < '1.4') {
2162
                $this->PDFVersion = '1.4';
2163
            }
2164
        }
2165
        $info['data'] = $data;
2166
        return $info;
2167
    }
2168
    
2169
    /**
2170
     * Read n bytes from stream.
2171
     * @param resource $f
2172
     * @param int $n
2173
     * @return string
2174
     */
2175
    protected function readStream($f, int $n) : string
2176
    {
2177
        // Read n bytes from stream
2178
        $res = '';
2179
        while ($n > 0 && !feof($f)) {
2180
            $s = fread($f, $n);
2181
            if ($s === false) {
2182
                $this->error('Error while reading stream');
2183
                return '';
2184
            }
2185
            $n -= strlen($s);
2186
            $res .= $s;
2187
        }
2188
        if ($n > 0) {
2189
            $this->error('Unexpected end of stream');
2190
        }
2191
        return $res;
2192
    }
2193
    
2194
    /**
2195
     * Read a 4-byte integer from stream.
2196
     * @param resource $f
2197
     * @return int
2198
     */
2199
    protected function readInt($f) : int
2200
    {
2201
        // Read a 4-byte integer from stream
2202
        $a = unpack('Ni', $this->readStream($f, 4));
2203
        return $a['i'];
2204
    }
2205
    
2206
    /**
2207
     * Extract info from a GIF file.
2208
     * 1. GIF is converted to PNG using GD extension since PDF don't support 
2209
     * GIF image format. <br/>
2210
     * 2. The converted image is written to memory stream <br/>
2211
     * 3. The PNG-memory stream is parsed using internal function for PNG 
2212
     * @param string $file
2213
     * @return array
2214
     */
2215
    protected function parseGif(string $file) : array
2216
    {
2217
        // Extract info from a GIF file (via PNG conversion)
2218
        if (!function_exists('imagepng')) {
2219
            $this->error('GD extension is required for GIF support');
2220
            return [];
2221
        }
2222
        if (!function_exists('imagecreatefromgif')) {
2223
            $this->error('GD has no GIF read support');
2224
            return [];
2225
        }
2226
        $im = imagecreatefromgif($file);
2227
        if ($im === false) {
2228
            $this->error('Missing or incorrect image file: ' . $file);
2229
            return [];
2230
        }
2231
        imageinterlace($im, 0);
2232
        ob_start();
2233
        imagepng($im);
2234
        $data = ob_get_clean();
2235
        imagedestroy($im);
2236
        $f = fopen('php://temp', 'rb+');
2237
        if ($f === false) {
2238
            $this->error('Unable to create memory stream');
2239
            return [];
2240
        }
2241
        fwrite($f, $data);
2242
        rewind($f);
2243
        $info = $this->parsePngStream($f, $file);
2244
        fclose($f);
2245
        return $info;
2246
    }
2247
    
2248
    /**
2249
     * Add a line to the document.
2250
     * @param string $s
2251
     */
2252
    protected function out(string $s) : void
2253
    {
2254
        // Add a line to the document
2255
        switch ($this->state) {
2256
            case 0: // no page added so far
2257
                $this->error('No page has been added yet');
2258
                break;
2259
            case 1: // all output in the endDoc() method goes direct to the buffer
2260
                $this->put($s);
2261
                break;
2262
            case 2: // page output is stored in internal array
2263
                $this->pages[$this->page] .= $s . "\n";
2264
                break;
2265
            case 3: // document is closed so far
2266
                $this->error('The document is closed');
2267
                break;
2268
        }
2269
    }
2270
    
2271
    /**
2272
     * Add a command to the document.
2273
     * @param string $s
2274
     */
2275
    protected function put(string $s) : void
2276
    {
2277
        $this->buffer .= $s . "\n";
2278
    }
2279
    
2280
    /**
2281
     * Get current length of the output buffer.
2282
     * @return int
2283
     */
2284
    protected function getOffset() : int
2285
    {
2286
        return strlen($this->buffer);
2287
    }
2288
    
2289
    /**
2290
     * Begin a new object.
2291
     * @param int $n
2292
     */
2293
    protected function newObject(?int $n=null) : void
2294
    {
2295
        // Begin a new object
2296
        if ($n === null) {
2297
            $n = ++$this->n;
2298
        }
2299
        $this->offsets[$n] = $this->getOffset();
2300
        $this->put($n . ' 0 obj');
2301
    }
2302
    
2303
    /**
2304
     * Add data from stream to the document.
2305
     * @param string $data
2306
     */
2307
    protected function putStream(string $data) : void
2308
    {
2309
        $this->put('stream');
2310
        $this->put($data);
2311
        $this->put('endstream');
2312
    }
2313
    
2314
    /**
2315
     * Add Stream-object to the document. 
2316
     * @param string $data
2317
     */
2318
    protected function putStreamObject(string $data) : void
2319
    {
2320
        $entries = '';
2321
        if ($this->compress) {
2322
            $entries = '/Filter /FlateDecode ';
2323
            $data = gzcompress($data);
2324
        }
2325
        $entries .= '/Length ' . strlen($data);
2326
        $this->newObject();
2327
        $this->put('<<' . $entries . '>>');
2328
        $this->putStream($data);
2329
        $this->put('endobj');
2330
    }
2331
    
2332
    /**
2333
     * Add all pages to the document.
2334
     */
2335
    protected function putPages() : void
2336
    {
2337
        $nb = $this->page;
2338
        for ($n = 1; $n <= $nb; $n++) {
2339
            $this->PageInfo[$n]['n'] = $this->n + 1 + 2 * ($n - 1);
2340
        }
2341
        for ($n = 1; $n <= $nb; $n++) {
2342
            $this->putPage($n);
2343
        }
2344
        // Pages root
2345
        $this->newObject(1);
2346
        $this->put('<</Type /Pages');
2347
        $kids = '/Kids [';
2348
        for ($n = 1; $n <= $nb; $n++) {
2349
            $kids .= $this->PageInfo[$n]['n'] . ' 0 R ';
2350
        }
2351
        $this->put($kids . ']');
2352
        $this->put('/Count ' . $nb);
2353
        if ($this->DefOrientation == 'P') {
2354
            $w = $this->DefPageSize[0];
2355
            $h = $this->DefPageSize[1];
2356
        } else {
2357
            $w = $this->DefPageSize[1];
2358
            $h = $this->DefPageSize[0];
2359
        }
2360
        $this->put(sprintf('/MediaBox [0 0 %.2F %.2F]', $w * $this->k, $h * $this->k));
2361
        $this->put('>>');
2362
        $this->put('endobj');
2363
    }
2364
    
2365
    /**
2366
     * Add Pageinfos to the document.
2367
     * @param int $n
2368
     */
2369
    protected function putPage(int $n) : void
2370
    {
2371
        $this->newObject();
2372
        $this->put('<</Type /Page');
2373
        $this->put('/Parent 1 0 R');
2374
        if (isset($this->PageInfo[$n]['size'])) {
2375
            $this->put(sprintf('/MediaBox [0 0 %.2F %.2F]', $this->PageInfo[$n]['size'][0], $this->PageInfo[$n]['size'][1]));
2376
        }
2377
        if (isset($this->PageInfo[$n]['rotation'])) {
2378
            $this->put('/Rotate '.$this->PageInfo[$n]['rotation']);
2379
        }
2380
        $this->put('/Resources 2 0 R');
2381
        if (isset($this->PageLinks[$n])) {
2382
            // Links
2383
            $annots = '/Annots [';
2384
            foreach ($this->PageLinks[$n] as $pl) {
2385
                $rect = sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]);
2386
                $annots .= '<</Type /Annot /Subtype /Link /Rect [' . $rect . '] /Border [0 0 0] ';
2387
                if (is_string($pl[4])) {
2388
                    $annots .= '/A <</S /URI /URI ' . $this->textString($pl[4]) . '>>>>';
2389
                } else {
2390
                    $l = $this->links[$pl[4]];
2391
                    if (isset($this->PageInfo[$l[0]]['size'])) {
2392
                        $h = $this->PageInfo[$l[0]]['size'][1];
2393
                    } else {
2394
                        $h = ($this->DefOrientation=='P') ? $this->DefPageSize[1] * $this->k : $this->DefPageSize[0] * $this->k;
2395
                    }
2396
                    $annots .= sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>', $this->PageInfo[$l[0]]['n'], $h - $l[1] * $this->k);
2397
                }
2398
            }
2399
            $this->put($annots.']');
2400
        }
2401
        if ($this->WithAlpha) {
2402
            $this->put('/Group <</Type /Group /S /Transparency /CS /DeviceRGB>>');
2403
        }
2404
        $this->put('/Contents ' . ($this->n + 1) . ' 0 R>>');
2405
        $this->put('endobj');
2406
        // Page content
2407
        if (!empty($this->AliasNbPages)) {
2408
            $this->pages[$n] = str_replace($this->AliasNbPages, strval($this->page), $this->pages[$n]);
2409
        }
2410
        $this->putStreamObject($this->pages[$n]);
2411
    }
2412
    
2413
    /**
2414
     * Add fonts to the document.
2415
     */
2416
    protected function putFonts() : void
2417
    {
2418
        foreach ($this->FontFiles as $file => $info) {
2419
            // Font file embedding
2420
            $this->newObject();
2421
            $this->FontFiles[$file]['n'] = $this->n;
2422
            $font = file_get_contents($this->fontpath . $file, true);
2423
            if (!$font) {
2424
                $this->error('Font file not found: ' . $file);
2425
            }
2426
            $compressed = (substr($file, -2) == '.z');
2427
            if (!$compressed && isset($info['length2'])) {
2428
                $font = substr($font, 6, $info['length1']) . substr($font, 6 + $info['length1'] + 6, $info['length2']);
2429
            }
2430
            $this->put('<</Length ' . strlen($font));
2431
            if ($compressed) {
2432
                $this->put('/Filter /FlateDecode');
2433
            }
2434
            $this->put('/Length1 ' . $info['length1']);
2435
            if (isset($info['length2'])) {
2436
                $this->put('/Length2 '.$info['length2'].' /Length3 0');
2437
            }
2438
            $this->put('>>');
2439
            $this->putStream($font);
2440
            $this->put('endobj');
2441
        }
2442
        foreach ($this->fonts as $k => $font) {
2443
            // Encoding
2444
            if (isset($font['diff'])) {
2445
                if (!isset($this->encodings[$font['enc']])) {
2446
                    $this->newObject();
2447
                    $this->put('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [' . $font['diff'] . ']>>');
2448
                    $this->put('endobj');
2449
                    $this->encodings[$font['enc']] = $this->n;
2450
                }
2451
            }
2452
            // ToUnicode CMap
2453
            $cmapkey = '';
2454
            if (isset($font['uv'])) {
2455
                if (isset($font['enc'])) {
2456
                    $cmapkey = $font['enc'];
2457
                } else {
2458
                    $cmapkey = $font['name'];
2459
                }
2460
                if (!isset($this->cmaps[$cmapkey])) {
2461
                    $cmap = $this->toUnicodeCMap($font['uv']);
2462
                    $this->putStreamObject($cmap);
2463
                    $this->cmaps[$cmapkey] = $this->n;
2464
                }
2465
            }
2466
            // Font object
2467
            $this->fonts[$k]['n'] = $this->n + 1;
2468
            $type = $font['type'];
2469
            $name = $font['name'];
2470
            if ($font['subsetted']) {
2471
                $name = 'AAAAAA+' . $name;
2472
            }
2473
            if ($type=='Core') {
2474
                // Core font
2475
                $this->newObject();
2476
                $this->put('<</Type /Font');
2477
                $this->put('/BaseFont /' . $name);
2478
                $this->put('/Subtype /Type1');
2479
                if ($name != 'Symbol' && $name != 'ZapfDingbats') {
2480
                    $this->put('/Encoding /WinAnsiEncoding');
2481
                }
2482
                if (isset($font['uv'])) {
2483
                    $this->put('/ToUnicode ' . $this->cmaps[$cmapkey] . ' 0 R');
2484
                }
2485
                $this->put('>>');
2486
                $this->put('endobj');
2487
            } elseif ($type == 'Type1' || $type == 'TrueType') {
2488
                // Additional Type1 or TrueType/OpenType font
2489
                $this->newObject();
2490
                $this->put('<</Type /Font');
2491
                $this->put('/BaseFont /' . $name);
2492
                $this->put('/Subtype /' . $type);
2493
                $this->put('/FirstChar 32 /LastChar 255');
2494
                $this->put('/Widths ' . ($this->n + 1) . ' 0 R');
2495
                $this->put('/FontDescriptor ' . ($this->n + 2) . ' 0 R');
2496
                if (isset($font['diff'])) {
2497
                    $this->put('/Encoding ' . $this->encodings[$font['enc']] . ' 0 R');
2498
                } else {
2499
                    $this->put('/Encoding /WinAnsiEncoding');
2500
                }
2501
                if (isset($font['uv'])) {
2502
                    $this->put('/ToUnicode ' . $this->cmaps[$cmapkey] . ' 0 R');
2503
                }
2504
                $this->put('>>');
2505
                $this->put('endobj');
2506
                // Widths
2507
                $this->newObject();
2508
                $cw = &$font['cw'];
2509
                $s = '[';
2510
                for ($i = 32; $i <= 255; $i++) {
2511
                    $s .= $cw[chr($i)] . ' ';
2512
                }
2513
                $this->put($s . ']');
2514
                $this->put('endobj');
2515
                // Descriptor
2516
                $this->newObject();
2517
                $s = '<</Type /FontDescriptor /FontName /' . $name;
2518
                foreach ($font['desc'] as $k2 => $v) {
2519
                    $s .= ' /' . $k2 . ' ' . $v;
2520
                }
2521
                if (!empty($font['file'])) {
2522
                    $s .= ' /FontFile' . ($type == 'Type1' ? '' : '2') . ' ' . $this->FontFiles[$font['file']]['n'] . ' 0 R';
2523
                }
2524
                $this->put($s . '>>');
2525
                $this->put('endobj');
2526
            } else {
2527
                // Allow for additional types
2528
                $mtd = '_put' . strtolower($type);
2529
                if (!method_exists($this, $mtd)) {
2530
                    $this->error('Unsupported font type: ' . $type);
2531
                }
2532
                $this->$mtd($font);
2533
            }
2534
        }
2535
    }
2536
    
2537
    /**
2538
     * @param array $uv
2539
     * @return string
2540
     */
2541
    protected function toUnicodeCMap(array $uv) : string
2542
    {
2543
        $ranges = '';
2544
        $nbr = 0;
2545
        $chars = '';
2546
        $nbc = 0;
2547
        foreach ($uv as $c => $v) {
2548
            if (is_array($v)) {
2549
                $ranges .= sprintf("<%02X> <%02X> <%04X>\n", $c, $c + $v[1] - 1, $v[0]);
2550
                $nbr++;
2551
            } else {
2552
                $chars .= sprintf("<%02X> <%04X>\n", $c, $v);
2553
                $nbc++;
2554
            }
2555
        }
2556
        $s = "/CIDInit /ProcSet findresource begin\n";
2557
        $s .= "12 dict begin\n";
2558
        $s .= "begincmap\n";
2559
        $s .= "/CIDSystemInfo\n";
2560
        $s .= "<</Registry (Adobe)\n";
2561
        $s .= "/Ordering (UCS)\n";
2562
        $s .= "/Supplement 0\n";
2563
        $s .= ">> def\n";
2564
        $s .= "/CMapName /Adobe-Identity-UCS def\n";
2565
        $s .= "/CMapType 2 def\n";
2566
        $s .= "1 begincodespacerange\n";
2567
        $s .= "<00> <FF>\n";
2568
        $s .= "endcodespacerange\n";
2569
        if ($nbr > 0) {
2570
            $s .= "$nbr beginbfrange\n";
2571
            $s .= $ranges;
2572
            $s .= "endbfrange\n";
2573
        }
2574
        if ($nbc > 0) {
2575
            $s .= "$nbc beginbfchar\n";
2576
            $s .= $chars;
2577
            $s .= "endbfchar\n";
2578
        }
2579
        $s .= "endcmap\n";
2580
        $s .= "CMapName currentdict /CMap defineresource pop\n";
2581
        $s .= "end\n";
2582
        $s .= "end";
2583
        return $s;
2584
    }
2585
    
2586
    /**
2587
     * Add all containing images to the document.
2588
     */
2589
    protected function putImages() : void
2590
    {
2591
        foreach(array_keys($this->images) as $file) {
2592
            $this->putImage($this->images[$file]);
2593
            unset($this->images[$file]['data']);
2594
            unset($this->images[$file]['smask']);
2595
        }
2596
    }
2597
    
2598
    /**
2599
     * Add image to the document.
2600
     * @param array $info
2601
     */
2602
    protected function putImage(array &$info) : void
2603
    {
2604
        $this->newObject();
2605
        $info['n'] = $this->n;
2606
        $this->put('<</Type /XObject');
2607
        $this->put('/Subtype /Image');
2608
        $this->put('/Width ' . $info['w']);
2609
        $this->put('/Height ' . $info['h']);
2610
        if ($info['cs'] == 'Indexed') {
2611
            $this->put('/ColorSpace [/Indexed /DeviceRGB ' . (strlen($info['pal']) / 3 - 1) . ' ' . ($this->n + 1) . ' 0 R]');
2612
        } else {
2613
            $this->put('/ColorSpace /' . $info['cs']);
2614
            if($info['cs']=='DeviceCMYK') {
2615
                $this->put('/Decode [1 0 1 0 1 0 1 0]');
2616
            }
2617
        }
2618
        $this->put('/BitsPerComponent ' . $info['bpc']);
2619
        if (isset($info['f'])) {
2620
            $this->put('/Filter /' . $info['f']);
2621
        }
2622
        if (isset($info['dp'])) {
2623
            $this->put('/DecodeParms <<' . $info['dp'] . '>>');
2624
        }
2625
        if (isset($info['trns']) && is_array($info['trns']))    {
2626
            $trns = '';
2627
            $cnt = count($info['trns']);
2628
            for ($i = 0; $i < $cnt; $i++) {
2629
                $trns .= $info['trns'][$i] . ' ' . $info['trns'][$i] . ' ';
2630
            }
2631
            $this->put('/Mask [' . $trns . ']');
2632
        }
2633
        if (isset($info['smask'])) {
2634
            $this->put('/SMask ' . ($this->n+1) . ' 0 R');
2635
        }
2636
        $this->put('/Length ' . strlen($info['data']) . '>>');
2637
        $this->putStream($info['data']);
2638
        $this->put('endobj');
2639
        // Soft mask
2640
        if (isset($info['smask'])) {
2641
            $dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns ' . $info['w'];
2642
            $smask = array(
2643
                'w'=>$info['w'], 
2644
                'h'=>$info['h'], 
2645
                'cs'=>'DeviceGray', 
2646
                'bpc'=>8, 
2647
                'f'=>$info['f'], 
2648
                'dp'=>$dp, 
2649
                'data'=>$info['smask']
2650
            );
2651
            $this->putImage($smask);
2652
        }
2653
        // Palette
2654
        if ($info['cs'] == 'Indexed') {
2655
            $this->putStreamObject($info['pal']);
2656
        }
2657
    }
2658
    
2659
    /**
2660
     * 
2661
     */
2662
    protected function putXObjectDict() : void
2663
    {
2664
        $this->put('/XObject <<');
2665
        foreach ($this->images as $image) {
2666
            $this->put('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R');
2667
        }
2668
        $this->put('>>');
2669
    }
2670
    
2671
    /**
2672
     * Insert the fonts dictionary.
2673
     */
2674
    protected function putFontsDict() : void
2675
    {
2676
        $this->put('/Font <<');
2677
        foreach ($this->fonts as $font) {
2678
            $this->put('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R');
2679
        }
2680
        $this->put('>>');
2681
    }
2682
    
2683
    /**
2684
     * Insert the resource dictionary.
2685
     */
2686
    protected function putResourceDict() : void
2687
    {
2688
        $this->newObject(2);
2689
        $this->put('<<');
2690
        $this->put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
2691
        $this->putFontsDict();
2692
        $this->putXObjectDict();
2693
        $this->put('>>');
2694
        $this->put('endobj');
2695
    }
2696
    
2697
    /**
2698
     * Insert all resources.
2699
     * - fonts
2700
     * - images
2701
     * - resource dictonary
2702
     */
2703
    protected function putResources() : void
2704
    {
2705
        $this->putFonts();
2706
        $this->putImages();
2707
    }
2708
    
2709
    /**
2710
     * 
2711
     */
2712
    protected function putBookmarks() : void
2713
    {
2714
        $nb = count($this->outlines);
2715
        if( $nb==0 ) {
2716
            return;
2717
        }
2718
        $lru = array();
2719
        $level = 0;
2720
        foreach ($this->outlines as $i => $o) {
2721
            if ($o['l']>0) {
2722
                $parent = $lru[$o['l'] - 1];
2723
                // Set parent and last pointers
2724
                $this->outlines[$i]['parent'] = $parent;
2725
                $this->outlines[$parent]['last'] = $i;
2726
                if ($o['l'] > $level) {
2727
                    // Level increasing: set first pointer
2728
                    $this->outlines[$parent]['first'] = $i;
2729
                }
2730
            }
2731
            else
2732
                $this->outlines[$i]['parent'] = $nb;
2733
                if ($o['l'] <= $level && $i > 0) {
2734
                    // Set prev and next pointers
2735
                    $prev = $lru[$o['l']];
2736
                    $this->outlines[$prev]['next'] = $i;
2737
                    $this->outlines[$i]['prev'] = $prev;
2738
                }
2739
                $lru[$o['l']] = $i;
2740
                $level = $o['l'];
2741
        }
2742
        // Outline items
2743
        $n = $this->n+1;
2744
        foreach ($this->outlines as $i=>$o) {
2745
            $this->newObject();
2746
            $this->put('<</Title ' . $this->textString($o['t']));
2747
            $this->put('/Parent ' . ($n + $o['parent']) . ' 0 R');
2748
            if (isset($o['prev'])) {
2749
                $this->put('/Prev ' . ($n + $o['prev']) . ' 0 R');
2750
            }
2751
            if (isset($o['next'])) {
2752
                $this->put('/Next ' . ($n + $o['next']) . ' 0 R');
2753
            }
2754
            if (isset($o['first'])) {
2755
                $this->put('/First ' . ($n + $o['first']) . ' 0 R');
2756
            }
2757
            if (isset($o['last'])) {
2758
                $this->put('/Last ' . ($n + $o['last']) . ' 0 R');
2759
            }
2760
            $this->put(sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]', $this->PageInfo[$o['p']]['n'], $o['y']));
2761
            $this->put('/Count 0>>');
2762
            $this->put('endobj');
2763
        }
2764
        // Outline root
2765
        $this->newObject();
2766
        $this->outlineRoot = $this->n;
2767
        $this->put('<</Type /Outlines /First ' . $n . ' 0 R');
2768
        $this->put('/Last ' . ($n+$lru[0]) . ' 0 R>>');
2769
        $this->put('endobj');
2770
    }
2771
    
2772
    /**
2773
     * create the file info.
2774
     */
2775
    protected function putInfo() : void
2776
    {
2777
        $this->newObject();
2778
        $this->put('<<');
2779
        $this->metadata['Producer'] = 'FPDF ' . FPDF_VERSION;
2780
        $this->metadata['CreationDate'] = 'D:' . @date('YmdHis');
2781
        foreach ($this->metadata as $key => $value) {
2782
            $this->put('/' . $key . ' ' . $this->textString($value));
2783
        }
2784
        $this->put('>>');
2785
        $this->put('endobj');
2786
    }
2787
    
2788
    /**
2789
     * Create catalog.
2790
     * - zoom mode
2791
     * - page layout
2792
     * - outlines
2793
     */
2794
    protected function putCatalog() : void
2795
    {
2796
        $this->newObject();
2797
        $this->put('<<');
2798
        
2799
        $n = $this->PageInfo[1]['n'];
2800
        $this->put('/Type /Catalog');
2801
        $this->put('/Pages 1 0 R');
2802
        if($this->ZoomMode=='fullpage') {
2803
            $this->put('/OpenAction [' . $n . ' 0 R /Fit]');
2804
        } elseif ($this->ZoomMode=='fullwidth') {
2805
            $this->put('/OpenAction [' . $n . ' 0 R /FitH null]');
2806
        } elseif($this->ZoomMode=='real') {
2807
            $this->put('/OpenAction [' . $n . ' 0 R /XYZ null null 1]');
2808
        } elseif(!is_string($this->ZoomMode)) {
2809
            $this->put('/OpenAction [' . $n . ' 0 R /XYZ null null ' . sprintf('%.2F', $this->ZoomMode / 100) . ']');
2810
        }
2811
        if($this->LayoutMode=='single') {
2812
            $this->put('/PageLayout /SinglePage');
2813
        } elseif($this->LayoutMode=='continuous') {
2814
            $this->put('/PageLayout /OneColumn');
2815
        } elseif($this->LayoutMode=='two') {
2816
            $this->put('/PageLayout /TwoColumnLeft');
2817
        }
2818
        if (count($this->outlines) > 0) {
2819
            $this->put('/Outlines ' . $this->outlineRoot.' 0 R');
2820
            $this->put('/PageMode /UseOutlines');
2821
        }
2822
        
2823
        $this->put('>>');
2824
        $this->put('endobj');
2825
    }
2826
    
2827
    /**
2828
     * Create Header.
2829
     * Header only contains the PDF Version
2830
     */
2831
    protected function putHeader() : void
2832
    {
2833
        $this->put('%PDF-' . $this->PDFVersion);
2834
    }
2835
    
2836
    /**
2837
     * Create Trailer.
2838
     * 
2839
     */
2840
    protected function putTrailer() : void
2841
    {
2842
        $this->put('trailer');
2843
        $this->put('<<');
2844
        $this->put('/Size ' . ($this->n + 1));
2845
        $this->put('/Root ' . $this->n . ' 0 R');
2846
        $this->put('/Info ' . ($this->n - 1) . ' 0 R');
2847
        $this->put('>>');
2848
    }
2849
    
2850
    /**
2851
     *  End of document.
2852
     *  Generate the whole document into internal buffer: <ul>
2853
     *  <li> header (PDF Version)</li> 
2854
     *  <li> pages </li> 
2855
     *  <li> ressources (fonts, images) </li> 
2856
     *  <li> ressources dictionary </li> 
2857
     *  <li> bookmarks </li> 
2858
     *  <li> fileinfo </li> 
2859
     *  <li> zoommode, layout </li> 
2860
     *  <li> page - ref's </li> 
2861
     *  <li> trailer </li></ul>
2862
     */
2863
    protected function endDoc() : void
2864
    {
2865
        $this->putHeader();
2866
        $this->putPages();
2867
        $this->putResources();
2868
        $this->putResourceDict();
2869
        $this->putBookmarks();
2870
        
2871
        // Info
2872
        $this->putInfo();
2873
        
2874
        // Catalog (zoommode, page layout) 
2875
        $this->putCatalog();
2876
        
2877
        // Cross-ref
2878
        $offset = $this->getOffset();
2879
        $this->put('xref');
2880
        $this->put('0 ' . ($this->n + 1));
2881
        $this->put('0000000000 65535 f ');
2882
        for ($i = 1; $i <= $this->n; $i++) {
2883
            $this->put(sprintf('%010d 00000 n ', $this->offsets[$i]));
2884
        }
2885
        // Trailer
2886
        $this->putTrailer();
2887
2888
        // EOF
2889
        $this->put('startxref');
2890
        $this->put(strval($offset));
2891
        $this->put('%%EOF');
2892
        $this->state = 3;
2893
    }
2894
}
2895