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