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

FPDF::cell()   F

Complexity

Conditions 28
Paths > 20000

Size

Total Lines 84
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 28
eloc 59
nc 395760
nop 8
dl 0
loc 84
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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