Passed
Push — main ( ddfb19...b08f68 )
by Stefan
02:11
created

FPDF::__construct()   B

Complexity

Conditions 9
Paths 15

Size

Total Lines 70
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 45
nc 15
nop 3
dl 0
loc 70
rs 7.6444
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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