FPDF::parsePngStream()   F
last analyzed

Complexity

Conditions 29
Paths 349

Size

Total Lines 141
Code Lines 108

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 29
eloc 108
nc 349
nop 2
dl 0
loc 141
rs 1.1766
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
namespace OPlathey\FPDF;
3
4
5
6
/*******************************************************************************
7
 * FPDF                                                                         *
8
 *                                                                              *
9
 * Version: 1.82                                                                *
10
 * Date:    2019-12-07                                                          *
11
 * Author:  Olivier PLATHEY                                                     *
12
 * http://www.fpdf.org/en/doc/index.php
13
 *******************************************************************************/
14
15
define('FPDF_VERSION', '1.82');
16
17
/**
18
 * Modified version of O.Platheys FPDF.php
19
 *
20
 * Based on version 1.82 of FPDF.php, extended by
21
 * - namespace to include through autoloader
22
 * - PHP 7.4 typehints
23
 * - phpDoc comments
24
 *
25
 * @link http://www.fpdf.org/en/doc/index.php
26
 * @package OPlathey/FPDF
27
 * @author O.Plathey
28
 * @copyright MIT License - see the LICENSE file for details
29
 */
30
class FPDF
31
{
32
    /** @var int current page number     */
33
    protected int $page = 0;
34
    /** @var int current object number     */
35
    protected int $n = 2;
36
    /** @var array array of object offsets     */
37
    protected array $offsets;
38
    /** @var string buffer holding in-memory PDF     */
39
    protected string $buffer = '';
40
    /** @var array array containing pages     */
41
    protected array $pages = array();
42
    /** @var int current document state     */
43
    protected int $state = 0;
44
    /** @var bool compression flag     */
45
    protected bool $compress;
46
    /** @var float scale factor (number of points in user unit)     */
47
    protected float $k;
48
    /** @var string default orientation     */
49
    protected string $DefOrientation;
50
    /** @var string current orientation     */
51
    protected string $CurOrientation;
52
    /** @var array default page size     */
53
    protected array $DefPageSize;
54
    /** @var array current page size     */
55
    protected array $CurPageSize;
56
    /** @var int current page rotation     */
57
    protected int $CurRotation;
58
    /** @var array page-related data     */
59
    protected array $PageInfo = array();
60
    /** @var float width of current page in points     */
61
    protected float $wPt;
62
    /** @var float height of current page in points     */
63
    protected float $hPt;
64
    /** @var float width of current page in user unit     */
65
    protected float $w;
66
    /** @var float height of current page in user unit     */
67
    protected float $h;
68
    /** @var float left margin     */
69
    protected float $lMargin;
70
    /** @var float top margin     */
71
    protected float $tMargin;
72
    /** @var float right margin     */
73
    protected float $rMargin;
74
    /** @var float page break margin     */
75
    protected float $bMargin;
76
    /** @var float cell margin     */
77
    protected float $cMargin;
78
    /** @var float current X-position in user unit     */
79
    protected float $x;
80
    /** @var float current Y-position in user unit     */
81
    protected float $y;
82
    /** @var float height of last printed cell     */
83
    protected float $lasth = 0.0;
84
    /** @var float line width in user unit     */
85
    protected float $LineWidth;
86
    /** @var string path containing fonts     */
87
    protected string $fontpath;
88
    /** @var array array of used fonts     */
89
    protected array $fonts = array();
90
    /** @var array array of font files     */
91
    protected array $FontFiles = array();
92
    /** @var array array of encodings     */
93
    protected array $encodings = array();
94
    /** @var array array of ToUnicode CMaps     */
95
    protected array $cmaps = array();
96
    /** @var string current font family     */
97
    protected string $FontFamily = '';
98
    /** @var string current font style     */
99
    protected string $FontStyle = '';
100
    /** @var bool underlining flag     */
101
    protected bool $underline = false;
102
    /** @var array current font info     */
103
    protected array $CurrentFont;
104
    /** @var float current font size in points     */
105
    protected float $FontSizePt = 12.0;
106
    /** @var float current font size in user unit     */
107
    protected float $FontSize;
108
    /** @var string commands for drawing color     */
109
    protected string $DrawColor = '0 G';
110
    /** @var string commands for filling color     */
111
    protected string $FillColor = '0 g';
112
    /** @var string commands for text color     */
113
    protected string $TextColor = '0 g';
114
    /** @var bool indicates whether fill and text colors are different     */
115
    protected bool $ColorFlag = false;
116
    /** @var bool indicates whether alpha channel is used     */
117
    protected bool $WithAlpha = false;
118
    /** @var float word spacing     */
119
    protected float $ws = 0.0;
120
    /** @var array array of used images     */
121
    protected array $images = array();
122
    /** @var array array of links in pages     */
123
    protected array $PageLinks;
124
    /** @var array array of internal links     */
125
    protected array $links = array();
126
    /** @var bool automatic page breaking     */
127
    protected bool $AutoPageBreak;
128
    /** @var float threshold used to trigger page breaks     */
129
    protected float $PageBreakTrigger;
130
    /** @var bool flag set when processing header     */
131
    protected bool $InHeader = false;
132
    /** @var bool flag set when processing footer     */
133
    protected bool $InFooter = false;
134
    /** @var string alias for total number of pages     */
135
    protected string $AliasNbPages;
136
    /** @var string|float zoom display mode     */
137
    protected $ZoomMode;
138
    /** @var string layout display mode     */
139
    protected string $LayoutMode;
140
    /** @var array document properties     */
141
    protected array $metadata;
142
    /** @var string PDF version number     */
143
    protected string $PDFVersion;
144
    /** @var array   */
145
    protected array $outlines = array();
146
    /** @var int     */
147
    protected int $outlineRoot;
148
    /** @var string     */
149
    protected string $charset = 'UTF-8';
150
151
    /**
152
     * This is the class constructor.
153
     * It allows to set up the page size, the orientation and the unit of measure used
154
     * in all methods (except for font sizes).
155
     * @param string $orientation   Default page orientation. <br/>
156
     *                              Possible values are (case insensitive): <ul>
157
     *                              <li>   'P' or 'Portrait' </li>
158
     *                              <li>   'L' or 'Landscape' </li></ul>
159
     *                              Default value is 'P'. <br/>
160
     * @param string $unit          User unit.  <br/>
161
     *                              Possible values are: <ul>
162
     *                              <li>   'pt': point,  </li>
163
     *                              <li>   'mm': millimeter,  </li>
164
     *                              <li>   'cm': centimeter,  </li>
165
     *                              <li>   'in': inch </li></ul>
166
     *                              A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). <br/>
167
     *                              This is a very common unit in typography; font sizes are expressed in that unit. <br/>
168
     *                              Default value is 'mm'. <br/>
169
     * @param string|array $size    The size used for pages. <br/>
170
     *                              It can be either one of the following values (case insensitive): <ul>
171
     *                              <li> 'A3' </li>
172
     *                              <li> 'A4' </li>
173
     *                              <li> 'A5' </li>
174
     *                              <li> 'Letter' </li>
175
     *                              <li> 'Legal' </li></ul>
176
     *                              or an array containing the width and the height (expressed in the unit given by unit). <br/>
177
     *                              Default value is 'A4'.
178
     */
179
    public function __construct(string $orientation = 'P', string $unit = 'mm', $size = 'A4')
180
    {
181
        // Some checks
182
        $this->doChecks();
183
        $this->getFontPath();
184
185
        // Scale factor
186
        switch ($unit) {
187
            case 'pt':
188
                $this->k = 1;
189
                break;
190
            case 'mm':
191
                $this->k = 72 / 25.4;
192
                break;
193
            case 'cm':
194
                $this->k = 72 / 2.54;
195
                break;
196
            case 'in':
197
                $this->k = 72;
198
                break;
199
            default:
200
                $this->error('Incorrect unit: ' . $unit);
201
                break;
202
        }
203
204
        // Page sizes
205
        $size = $this->getPageSize($size);
206
        $this->DefPageSize = $size;
207
        $this->CurPageSize = $size;
208
209
        // Page orientation
210
        $orientation = strtolower($orientation);
211
        if ($orientation == 'p' || $orientation == 'portrait') {
212
            $this->DefOrientation = 'P';
213
            $this->w = $size[0];
214
            $this->h = $size[1];
215
        } elseif ($orientation == 'l' || $orientation == 'landscape') {
216
            $this->DefOrientation = 'L';
217
            $this->w = $size[1];
218
            $this->h = $size[0];
219
        } else {
220
            $this->error('Incorrect orientation: ' . $orientation);
221
        }
222
        $this->CurOrientation = $this->DefOrientation;
223
        $this->wPt = $this->w * $this->k;
224
        $this->hPt = $this->h * $this->k;
225
226
        // set some default values
227
        // - no page rotation
228
        // - page margins 1cm
229
        // - interior cell margin 1mm
230
        // - line width 0.2mm
231
        // - automatic page break
232
        // - default display mode
233
        // - enable compression
234
        // - PDF version 1.3
235
        $this->CurRotation = 0;
236
237
        $margin = 28.35 / $this->k;
238
        $this->setMargins($margin, $margin);
239
        $this->cMargin = $margin / 10;
240
        $this->LineWidth = 0.567 / $this->k;
241
        $this->setAutoPageBreak(true, 2 * $margin);
242
        $this->setDisplayMode('default');
243
        $this->setCompression(true);
244
        $this->PDFVersion = '1.3';
245
    }
246
247
    /**
248
     * Defines the left, top and right margins.
249
     * By default, they equal 1 cm. Call this method to change them.
250
     * @param float $left   Left margin.
251
     * @param float $top    Top margin.
252
     * @param float $right  Right margin. Default value is the left one.
253
     */
254
    public function setMargins(float $left, float $top, ?float $right = null) : void
255
    {
256
        // Set left, top and right margins
257
        $this->lMargin = $left;
258
        $this->tMargin = $top;
259
        if ($right === null) {
260
            $right = $left;
261
        }
262
        $this->rMargin = $right;
263
    }
264
265
    /**
266
     * Defines the left margin.
267
     * The method can be called before creating the first page.
268
     * If the current X-position gets out of page, it is brought back to the margin.
269
     * @param float $margin Left margin.
270
     */
271
    public function setLeftMargin(float $margin) : void
272
    {
273
        // Set left margin
274
        $this->lMargin = $margin;
275
        if ($this->page > 0 && $this->x < $margin) {
276
            $this->x = $margin;
277
        }
278
    }
279
280
    /**
281
     * Defines the top margin.
282
     * The method can be called before creating the first page.
283
     * @param float $margin
284
     */
285
    public function setTopMargin(float $margin) : void
286
    {
287
        // Set top margin
288
        $this->tMargin = $margin;
289
    }
290
291
    /**
292
     * Defines the right margin.
293
     * The method can be called before creating the first page.
294
     * @param float $margin
295
     */
296
    public function setRightMargin(float $margin) : void
297
    {
298
        // Set right margin
299
        $this->rMargin = $margin;
300
    }
301
302
    /**
303
     * Enables or disables the automatic page breaking mode.
304
     * When enabling, the second parameter is the distance from the bottom of the page
305
     * that defines the triggering limit.
306
     * By default, the mode is on and the margin is 2 cm.
307
     * @param bool $auto    indicating if mode should be on or off.
308
     * @param float $margin Distance from the bottom of the page.
309
     */
310
    public function setAutoPageBreak(bool $auto, float $margin = 0) : void
311
    {
312
        // Set auto page break mode and triggering margin
313
        $this->AutoPageBreak = $auto;
314
        $this->bMargin = $margin;
315
        $this->PageBreakTrigger = $this->h - $margin;
316
    }
317
318
    /**
319
     * Defines the way the document is to be displayed by the viewer.
320
     * The zoom level can be set: <br/>
321
     * pages can be displayed <ul>
322
     * <li> entirely on screen </li>
323
     * <li> occupy the full width of the window </li>
324
     * <li> use real size </li>
325
     * <li> be scaled by a specific zooming factor </li>
326
     * <li> or use viewer default (configured in the Preferences menu of Adobe Reader). </li></ul>
327
     * The page layout can be specified too: <ul>
328
     * <li> single at once </li>
329
     * <li> continuous display </li>
330
     * <li> two columns </li>
331
     * <li> or viewer default. </li></ul>
332
     * <b>Note: this settings are ignored when displaying the PDF inside of a browser!</b>
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
        }
355
        if ($layout == 'single' || $layout == 'continuous' || $layout == 'two' || $layout == 'default') {
356
            $this->LayoutMode = $layout;
357
        } else {
358
            $this->error('Incorrect layout display mode: ' . $layout);
359
        }
360
    }
361
362
    /**
363
     * Activates or deactivates page compression.
364
     * When activated, the internal representation of each page is compressed, which leads to
365
     * a compression ratio of about 2 for the resulting document.
366
     * Compression is on by default. <br/>
367
     * <br/>
368
     * <b>Note: the Zlib extension is required for this feature. If not present, compression will be turned off.</b>
369
     * @param bool $compress
370
     */
371
    public function setCompression(bool $compress) : void
372
    {
373
        // Set page compression
374
        if (function_exists('gzcompress')) {
375
            $this->compress = $compress;
376
        } else {
377
            $this->compress = false;
378
        }
379
    }
380
381
    /**
382
     * Defines the title of the document.
383
     * @param string $title The title.
384
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: true.
385
     */
386
    public function setTitle(string $title, bool $isUTF8 = true) : void
387
    {
388
        // Title of document
389
        $this->metadata['Title'] = $isUTF8 ? $title : utf8_encode($title);
390
    }
391
392
    /**
393
     * Defines the author of the document.
394
     * @param string $author
395
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: true.
396
     */
397
    public function setAuthor(string $author, bool $isUTF8 = true) : void
398
    {
399
        // Author of document
400
        $this->metadata['Author'] = $isUTF8 ? $author : utf8_encode($author);
401
    }
402
403
    /**
404
     * Defines the subject of the document.
405
     * @param string $subject
406
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: true.
407
     */
408
    public function setSubject(string $subject, bool $isUTF8 = true) : void
409
    {
410
        // Subject of document
411
        $this->metadata['Subject'] = $isUTF8 ? $subject : utf8_encode($subject);
412
    }
413
414
    /**
415
     * Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'.
416
     * @param string $keywords
417
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: true.
418
     */
419
    public function setKeywords(string $keywords, bool $isUTF8 = true) : void
420
    {
421
        // Keywords of document
422
        $this->metadata['Keywords'] = $isUTF8 ? $keywords : utf8_encode($keywords);
423
    }
424
425
    /**
426
     * Defines the creator of the document. This is typically the name of the application that generates the PDF.
427
     * @param string $creator
428
     * @param bool $isUTF8  Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true). Default value: true.
429
     */
430
    public function setCreator(string $creator, bool $isUTF8 = true) : void
431
    {
432
        // Creator of document
433
        $this->metadata['Creator'] = $isUTF8 ? $creator : utf8_encode($creator);
434
    }
435
436
    /**
437
     * Defines an alias for the total number of pages. It will be substituted as the document is closed.
438
     * @param string $alias The alias. Default value: {nb}.
439
     */
440
    public function aliasNbPages(string $alias = '{nb}') : void
441
    {
442
        // Define an alias for total number of pages
443
        $this->AliasNbPages = $alias;
444
    }
445
446
    /**
447
     * This method is automatically called in case of a fatal error.
448
     * It simply throws an exception with the provided message.
449
     * An inherited class may override it to customize the error handling but
450
     * the method should never return, otherwise the resulting document would probably be invalid.
451
     * @param string $msg The error message.
452
     * @throws \Exception
453
     */
454
    public function error(string $msg) : void
455
    {
456
        // Fatal error
457
        throw new \Exception('FPDF error: ' . $msg);
458
    }
459
460
    /**
461
     * Terminates the PDF document.
462
     * It is not necessary to call this method explicitly because Output() does it
463
     * automatically. If the document contains no page, AddPage() is called to prevent
464
     * from getting an invalid document.
465
     */
466
    public function close() : void
467
    {
468
        // Terminate document
469
        if ($this->state == 3) {
470
            return;
471
        }
472
        if ($this->page == 0) {
473
            $this->addPage();
474
        }
475
        // Page footer
476
        $this->InFooter = true;
477
        $this->footer();
478
        $this->InFooter = false;
479
        // Close page
480
        $this->endPage();
481
        // Close document
482
        $this->endDoc();
483
    }
484
485
    /**
486
     * Adds a new page to the document.
487
     * If a page is already present, the Footer() method is called first to output the
488
     * footer. Then the page is added, the current position set to the top-left corner
489
     * according to the left and top margins, and Header() is called to display the header.
490
     * The font which was set before calling is automatically restored. There is no need
491
     * to call SetFont() again if you want to continue with the same font. The same is
492
     * true for colors and line width.
493
     * The origin of the Y-position system is at the top-left corner and increasing
494
     * Y-positions go downwards.
495
     * @param string $orientation   Default page orientation. <br/>
496
     *                              Possible values are (case insensitive): <ul>
497
     *                              <li> 'P' or 'Portrait' </li>
498
     *                              <li> 'L' or 'Landscape' </li></ul>
499
     *                              Default value is 'P'. <br/>
500
     * @param string|array $size    The size used for pages. <br/>
501
     *                              It can be either one of the following values (case insensitive): <ul>
502
     *                              <li> 'A3' </li>
503
     *                              <li> 'A4' </li>
504
     *                              <li> 'A5' </li>
505
     *                              <li> 'Letter' </li>
506
     *                              <li> 'Legal' </li></ul>
507
     *                              or an array containing the width and the height (expressed in the unit given by unit). <br/>
508
     *                              Default value is 'A4'.  <br/>
509
     * @param int $rotation         Angle by which to rotate the page. <br/>
510
     *                              It must be a multiple of 90; positive values mean clockwise rotation. </br>
511
     *                              The default value is 0.
512
     */
513
    public function addPage(string $orientation = '', $size = '', int $rotation = 0) : void
514
    {
515
        // Start a new page
516
        if ($this->state == 3) {
517
            $this->error('The document is closed');
518
        }
519
        $family = $this->FontFamily;
520
        $style = $this->FontStyle . ($this->underline ? 'U' : '');
521
        $fontsize = $this->FontSizePt;
522
        $lw = $this->LineWidth;
523
        $dc = $this->DrawColor;
524
        $fc = $this->FillColor;
525
        $tc = $this->TextColor;
526
        $cf = $this->ColorFlag;
527
        if ($this->page > 0) {
528
            // Page footer
529
            $this->InFooter = true;
530
            $this->footer();
531
            $this->InFooter = false;
532
            // Close page
533
            $this->endPage();
534
        }
535
        // Start new page
536
        $this->beginPage($orientation, $size, $rotation);
537
        // Set line cap style to square
538
        $this->out('2 J');
539
        // Set line width
540
        $this->LineWidth = $lw;
541
        $this->out(sprintf('%.2F w', $lw * $this->k));
542
        // Set font
543
        if ($family) {
544
            $this->setFont($family, $style, $fontsize);
545
        }
546
        // Set colors
547
        $this->DrawColor = $dc;
548
        if ($dc != '0 G') {
549
            $this->out($dc);
550
        }
551
        $this->FillColor = $fc;
552
        if ($fc != '0 g') {
553
            $this->out($fc);
554
        }
555
        $this->TextColor = $tc;
556
        $this->ColorFlag = $cf;
557
        // Page header
558
        $this->InHeader = true;
559
        $this->header();
560
        $this->InHeader = false;
561
        // Restore line width
562
        if ($this->LineWidth != $lw) {
563
            $this->LineWidth = $lw;
564
            $this->out(sprintf('%.2F w', $lw * $this->k));
565
        }
566
        // Restore font
567
        if ($family) {
568
            $this->setFont($family, $style, $fontsize);
569
        }
570
        // Restore colors
571
        if ($this->DrawColor != $dc) {
572
            $this->DrawColor = $dc;
573
            $this->out($dc);
574
        }
575
        if ($this->FillColor != $fc) {
576
            $this->FillColor = $fc;
577
            $this->out($fc);
578
        }
579
        $this->TextColor = $tc;
580
        $this->ColorFlag = $cf;
581
    }
582
583
    /**
584
     * This method is used to render the page header.
585
     * It is automatically called by AddPage() and should not be called directly by the
586
     * application. The implementation in FPDF is empty, so you have to subclass it and
587
     * override the method if you want a specific processing.
588
     */
589
    public function header() : void
590
    {
591
        // To be implemented in your own inherited class
592
    }
593
594
    /**
595
     * This method is used to render the page footer.
596
     * It is automatically called by AddPage() and Close() and should not be called
597
     * directly by the application. The implementation in FPDF is empty, so you have to
598
     * subclass it and override the method if you want a specific processing.
599
     */
600
    public function footer() : void
601
    {
602
        // To be implemented in your own inherited class
603
    }
604
605
    /**
606
     * Returns the current page number.
607
     * @return int
608
     */
609
    public function pageNo() : int
610
    {
611
        // Get current page number
612
        return $this->page;
613
    }
614
615
    /**
616
     * Defines the color used for all drawing operations (lines, rectangles and cell borders).
617
     * It can be expressed in RGB components or gray scale. The method can be called before
618
     * the first page is created and the value is retained from page to page.
619
     * @param int $r    If g and b are given, red component; if not, indicates the gray level. Value between 0 and 255.
620
     * @param int $g    Green component (between 0 and 255).
621
     * @param int $b    Blue component (between 0 and 255).
622
     */
623
    public function setDrawColor(int $r, ?int $g = null, ?int $b = null) : void
624
    {
625
        // Set color for all stroking operations
626
        if (($r === 0 && $g === 0 && $b === 0) || $g === null) {
627
            $this->DrawColor = sprintf('%.3F G', $r / 255);
628
        } else {
629
            $this->DrawColor = sprintf('%.3F %.3F %.3F RG', $r / 255, $g / 255, $b / 255);
630
        }
631
        if ($this->page > 0) {
632
            $this->out($this->DrawColor);
633
        }
634
    }
635
636
    /**
637
     * Defines the color used for all filling operations (filled rectangles and cell backgrounds).
638
     * It can be expressed in RGB components or gray scale. The method can be called before the
639
     * first page is created and the value is retained from page to page.
640
     * @param int $r    If g and b are given, red component; if not, indicates the gray level. Value between 0 and 255.
641
     * @param int $g    Green component (between 0 and 255).
642
     * @param int $b    Blue component (between 0 and 255).
643
     */
644
    public function setFillColor(int $r, ?int $g = null, ?int $b = null) : void
645
    {
646
        // Set color for all filling operations
647
        if (($r === 0 && $g === 0 && $b === 0) || $g === null) {
648
            $this->FillColor = sprintf('%.3F g', $r / 255);
649
        } else {
650
            $this->FillColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255);
651
        }
652
        $this->ColorFlag = ($this->FillColor != $this->TextColor);
653
        if ($this->page > 0) {
654
            $this->out($this->FillColor);
655
        }
656
    }
657
658
    /**
659
     * Defines the color used for text.
660
     * It can be expressed in RGB components or gray scale. The method can be called before the
661
     * first page is created and the value is retained from page to page.
662
     * @param int $r    If g and b are given, red component; if not, indicates the gray level. Value between 0 and 255.
663
     * @param int $g    Green component (between 0 and 255).
664
     * @param int $b    Blue component (between 0 and 255).
665
     */
666
    public function setTextColor(int $r, ?int $g = null, ?int $b = null) : void
667
    {
668
        // Set color for text
669
        if (($r === 0 && $g === 0 && $b === 0) || $g === null) {
670
            $this->TextColor = sprintf('%.3F g', $r / 255);
671
        } else {
672
            $this->TextColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255);
673
        }
674
        $this->ColorFlag = ($this->FillColor != $this->TextColor);
675
    }
676
677
    /**
678
     * Returns the length of a string in user unit for current Font.
679
     * A font must be selected.
680
     * @param string $s The string whose length is to be computed.
681
     * @return float
682
     */
683
    public function getStringWidth(string $s) : float
684
    {
685
        // Get width of a string in the current font
686
        $s = $this->conv((string) $s);
687
        $cw = &$this->CurrentFont['cw'];
688
        $w = 0;
689
        $l = strlen($s);
690
        for ($i = 0; $i < $l; $i++) {
691
            $w += $cw[$s[$i]];
692
        }
693
        return $w * $this->FontSize / 1000;
694
    }
695
696
    /**
697
     * Defines the line width.
698
     * By default, the value equals 0.2 mm. The method can be called before the first
699
     * page is created and the value is retained from page to page.
700
     * @param float $width
701
     */
702
    public function setLineWidth(float $width) : void
703
    {
704
        // Set line width
705
        $this->LineWidth = $width;
706
        if ($this->page > 0) {
707
            $this->out(sprintf('%.2F w', $width * $this->k));
708
        }
709
    }
710
711
    /**
712
     * Draws a line between two points.
713
     * The X/Y-positions refer to the top left corner of the page.
714
     * Set margins are NOT taken into account.
715
     * @param float $x1     X-position upper left corner
716
     * @param float $y1     Y-position upper left corner
717
     * @param float $x2     X-position lower right corner
718
     * @param float $y2     Y-position lower right corner
719
     */
720
    public function line(float $x1, float $y1, float $x2, float $y2) : void
721
    {
722
        // Draw a line
723
        $x1 *= $this->k;
724
        $x2 *= $this->k;
725
        $y1 = ($this->h - $y1) * $this->k;
726
        $y2 = ($this->h - $y2) * $this->k;
727
        $this->out(sprintf('%.2F %.2F m %.2F %.2F l S', $x1, $y1, $x2, $y2));
728
    }
729
730
    /**
731
     * Outputs a rectangle.
732
     * It can be drawn (border only), filled (with no border) or both.
733
     * The X/Y-position refer to the top left corner of the page.
734
     * Set margins are NOT taken into account.
735
     * @param float $x      X-position upper left corner
736
     * @param float $y      Y-position upper left corner
737
     * @param float $w      Width
738
     * @param float $h      Height
739
     * @param string $style Style of rendering. <br/>
740
     *                      Possible values are: <ul>
741
     *                      <li>   'D' or empty string: draw the shape. This is the default value. </li>
742
     *                      <li>   'F': fill. </li>
743
     *                      <li>   'DF' or 'FD': draw the shape and fill. </li></ul>
744
     */
745
    public function rect(float $x, float $y, float $w, float $h, string $style = '') : void
746
    {
747
        // Draw a rectangle
748
        if ($style == 'F') {
749
            $op = 'f';
750
        } elseif ($style == 'FD' || $style == 'DF') {
751
            $op = 'B';
752
        } else {
753
            $op = 'S';
754
        }
755
        $x *= $this->k;
756
        $w *= $this->k;
757
        $y = ($this->h - $y) * $this->k;
758
        $h *= -$this->k;
759
        $this->out(sprintf('%.2F %.2F %.2F %.2F re %s', $x, $y, $w, $h, $op));
760
    }
761
762
    /**
763
     * Imports a TrueType, OpenType or Type1 font and makes it available.
764
     * It is necessary to generate a font definition file first with the MakeFont utility.
765
     * The definition file (and the font file itself when embedding) must be present in
766
     * the font directory. If it is not found, the error "Could not include font definition file"
767
     * is raised.
768
     * @param string $family    Font family. The name can be chosen arbitrarily. If it is a standard family name, it will override the corresponding font.
769
     * @param string $style     Font style. <br/>
770
     *                          Possible values are (case insensitive): <ul>
771
     *                          <li> empty string: regular </li>
772
     *                          <li> 'B': bold </li>
773
     *                          <li> 'I': italic </li>
774
     *                          <li> 'BI' or 'IB': bold italic </li></ul>
775
     *                          The default value is regular. <br/>
776
     * @param string $file      The font definition file. <br/>
777
     *                          By default, the name is built from the family and style, in lower case with no space.
778
     */
779
    public function addFont(string $family, string $style = '', string $file = '') : void
780
    {
781
        // Add a TrueType, OpenType or Type1 font
782
        $family = strtolower($family);
783
        if ($file == '') {
784
            $file = str_replace(' ', '', $family) . strtolower($style) . '.php';
785
        }
786
        $style = strtoupper($style);
787
        if ($style == 'IB') {
788
            $style = 'BI';
789
        }
790
        $fontkey = $family . $style;
791
        if (isset($this->fonts[$fontkey])) {
792
            return;
793
        }
794
        $info = $this->loadFont($file);
795
        $info['i'] = count($this->fonts) + 1;
796
        if (!empty($info['file'])) {
797
            // Embedded font
798
            if ($info['type'] == 'TrueType') {
799
                $this->FontFiles[$info['file']] = array('length1'=>$info['originalsize']);
800
            } else {
801
                $this->FontFiles[$info['file']] = array('length1'=>$info['size1'], 'length2'=>$info['size2']);
802
            }
803
        }
804
        $this->fonts[$fontkey] = $info;
805
    }
806
807
    /**
808
     * Sets the font used to print character strings.
809
     * It is mandatory to call this method at least once before printing text or the
810
     * resulting document would not be valid.
811
     * The font can be either a standard one or a font added via the AddFont() method.
812
     * Standard fonts use the Windows encoding cp1252 (Western Europe).
813
     * The method can be called before the first page is created and the font is kept from page to page.
814
     * If you just wish to change the current font size, it is simpler to call SetFontSize().<br/>
815
     *
816
     * <b>Note:</b><br/>
817
     * the font definition files must be accessible.
818
     * They are searched successively in: <ul>
819
     * <li> The directory defined by the FPDF_FONTPATH constant (if this constant is defined) </li>
820
     * <li> The 'font' directory located in the same directory as fpdf.php (if it exists) </li>
821
     * <li> The directories accessible through include() </li></ul>
822
     * @param string $family    Family font. <br/>
823
     *                          It can be either a name defined by AddFont() or one of the standard families (case insensitive): <ul>
824
     *                          <li> 'Courier' (fixed-width) </li>
825
     *                          <li> 'Helvetica' or 'Arial' (synonymous; sans serif) </li>
826
     *                          <li> 'Times' (serif) </li>
827
     *                          <li> 'Symbol' (symbolic) </li>
828
     *                          <li> 'ZapfDingbats' (symbolic)</li></ul>
829
     *                          It is also possible to pass an empty string. In that case, the current family is kept.<br/>
830
     * @param string $style     Font style. <br>
831
     *                          ossible values are (case insensitive): <ul>
832
     *                          <li> empty string: regular </li>
833
     *                          <li> 'B': bold </li>
834
     *                          <li> 'I': italic </li>
835
     *                          <li> 'U': underline </li>
836
     *                          <li> or any combination. </li></ul>
837
     *                          The default value is regular. Bold and italic styles do not apply to Symbol and ZapfDingbats.<br/>
838
     * @param float $size       Font size in points. <br/>
839
     *                          The default value is the current size. <br/>
840
     *                          If no size has been specified since the beginning of the document, the value taken is 12.
841
     */
842
    public function setFont(string $family, string $style = '', float $size = 0) : void
843
    {
844
        // Select a font; size given in points
845
        if ($family == '') {
846
            $family = $this->FontFamily;
847
        } else {
848
            $family = strtolower($family);
849
        }
850
        $style = strtoupper($style);
851
        if (strpos($style, 'U') !== false) {
852
            $this->underline = true;
853
            $style = str_replace('U', '', $style);
854
        } else {
855
            $this->underline = false;
856
        }
857
        if ($style == 'IB') {
858
            $style = 'BI';
859
        }
860
        if ($size == 0) {
861
            $size = $this->FontSizePt;
862
        }
863
        // Test if font is already selected
864
        if ($this->FontFamily == $family && $this->FontStyle == $style && $this->FontSizePt == $size) {
865
            return;
866
        }
867
        // Test if font is already loaded
868
        $fontkey = $family . $style;
869
        if (!isset($this->fonts[$fontkey])) {
870
            // Test if one of the core fonts
871
            if ($this->isCoreFont($family)) {
872
                if ($family == 'symbol' || $family == 'zapfdingbats') {
873
                    $style = '';
874
                }
875
                $fontkey = $family . $style;
876
                if (!isset($this->fonts[$fontkey])) {
877
                    $this->addFont($family, $style);
878
                }
879
            } else {
880
                $this->error('Undefined font: ' . $family . ' ' . $style);
881
            }
882
        }
883
        // Select it
884
        $this->FontFamily = $family;
885
        $this->FontStyle = $style;
886
        $this->FontSizePt = $size;
887
        $this->FontSize = $size / $this->k;
888
        $this->CurrentFont = &$this->fonts[$fontkey];
889
        if ($this->page > 0) {
890
            $this->out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt));
891
        }
892
    }
893
894
    /**
895
     * Defines the size of the current font.
896
     * @param float $size   The size (in points).
897
     */
898
    public function setFontSize(float $size) : void
899
    {
900
        // Set font size in points
901
        if ($this->FontSizePt == $size) {
902
            return;
903
        }
904
        $this->FontSizePt = $size;
905
        $this->FontSize = $size / $this->k;
906
        if ($this->page > 0) {
907
            $this->out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt));
908
        }
909
    }
910
911
    /**
912
     * Check for core font
913
     * @param string $strFamily
914
     * @return bool
915
     */
916
    protected function isCoreFont(string &$strFamily) : bool
917
    {
918
        $strFamily = strtolower($strFamily);
919
        if ($strFamily == 'arial') {
920
            $strFamily = 'helvetica';
921
        }
922
        // Core fonts
923
        $aCoreFonts = array('courier', 'helvetica', 'times', 'symbol', 'zapfdingbats');
924
        return in_array($strFamily, $aCoreFonts);
925
    }
926
927
    /**
928
     * Creates a new internal link and returns its identifier.
929
     * An internal link is a clickable area which directs to another place within the document.
930
     * The identifier can then be passed to Cell(), Write(), Image() or Link().
931
     * The destination is defined with SetLink().
932
     * @return int
933
     */
934
    public function addLink() : int
935
    {
936
        // Create a new internal link
937
        $n = count($this->links) + 1;
938
        $this->links[$n] = array(0, 0);
939
        return $n;
940
    }
941
942
    /**
943
     * Defines the page and position a link points to.
944
     * @param int $link The link identifier created by AddLink().
945
     * @param float $y  Y-position of target position; -1 indicates the current position. The default value is 0 (top of page).
946
     * @param int $page Number of target page; -1 indicates the current page. This is the default value.
947
     */
948
    public function setLink(int $link, float $y = 0, int $page = -1) : void
949
    {
950
        // Set destination of internal link
951
        if ($y == -1) {
952
            $y = $this->y;
953
        }
954
        if ($page == -1) {
955
            $page = $this->page;
956
        }
957
        $this->links[$link] = array($page, $y);
958
    }
959
960
    /**
961
     * Puts a link on a rectangular area of the page.
962
     * Text or image links are generally put via Cell(), Write() or Image(), but this
963
     * method can be useful for instance to define a clickable area inside an image.
964
     * Target can be an external URL or an internal link ID created and specified by AddLink()/SetLink()
965
     * @param float $x          X-position
966
     * @param float $y          Y-position
967
     * @param float $w          Width
968
     * @param float $h          Height
969
     * @param string|int $link  URL or link-ID
970
     */
971
    public function link(float $x, float $y, float $w, float $h, $link) : void
972
    {
973
        // Put a link on the page
974
        $this->PageLinks[$this->page][] = array($x * $this->k, $this->hPt - $y * $this->k, $w * $this->k, $h * $this->k, $link);
975
    }
976
977
    /**
978
     * Prints a character string.
979
     * The origin is on the left of the first character, on the baseline.
980
     * This method allows to place a string precisely on the page, but it is usually
981
     * easier to use Cell(), MultiCell() or Write() which are the standard methods
982
     * to print text.
983
     * @param float $x      X-position
984
     * @param float $y      Y-position
985
     * @param string $txt   String to print.
986
     */
987
    public function text(float $x, float $y, string $txt) : void
988
    {
989
        // Output a string
990
        if (!isset($this->CurrentFont)) {
991
            $this->error('No font has been set');
992
        }
993
        $txt = $this->conv($txt);
994
        $s = sprintf('BT %.2F %.2F Td (%s) Tj ET', $x * $this->k, ($this->h - $y) * $this->k, $this->escape($txt));
995
        if ($this->underline && $txt != '') {
996
            $s .= ' ' . $this->doUnderline($x, $y, $txt);
997
        }
998
        if ($this->ColorFlag) {
999
            $s = 'q ' . $this->TextColor . ' ' . $s . ' Q';
1000
        }
1001
        $this->out($s);
1002
    }
1003
1004
    /**
1005
     * Whenever a page break condition is met, the method is called, and the break is
1006
     * issued or not depending on the returned value.
1007
     * The default implementation returns a value according to the mode selected by
1008
     * SetAutoPageBreak().
1009
     * This method is called automatically and should not be called directly by the application.<br/>
1010
     * <br/>
1011
     * For usage in derived classes see example at http://www.fpdf.org/en/doc/acceptpagebreak.htm.
1012
     * @link http://www.fpdf.org/en/doc/acceptpagebreak.htm
1013
     * @return bool
1014
     */
1015
    public function acceptPageBreak() : bool
1016
    {
1017
        // Accept automatic page break or not
1018
        return $this->AutoPageBreak;
1019
    }
1020
1021
    /**
1022
     * Prints a cell (rectangular area) with optional borders, background color and character string.
1023
     * The upper-left corner of the cell corresponds to the current position. The text can be
1024
     * aligned or centered. After the call, the current position moves to the right or to the next line.
1025
     * It is possible to put a link on the text.
1026
     * If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done
1027
     * before outputting.
1028
     * @param float $w          Cell width. If 0, the cell extends up to the right margin.
1029
     * @param float $h          Cell height. Default value: 0.
1030
     * @param string $txt       String to print. Default value: empty string.
1031
     * @param int|string $border    Indicates if borders must be drawn around the cell. <br/>
1032
     *                          The value can be either a number: <ul>
1033
     *                          <li>0: no border </li>
1034
     *                          <li>1: frame </li></ul>
1035
     *                          or a string containing some or all of the following characters (in any order): <ul>
1036
     *                          <li> 'L': left </li>
1037
     *                          <li> 'T': top </li>
1038
     *                          <li> 'R': right </li>
1039
     *                          <li> 'B': bottom </li></ul>
1040
     *                          Default value: 0. <br/>
1041
     * @param float $ln         Indicates where the current position should go after the call. <br/>
1042
     *                          Possible values are: <ul>
1043
     *                          <li> 0: to the right </li>
1044
     *                          <li> 1: to the beginning of the next line </li>
1045
     *                          <li> 2: below </li></ul>
1046
     *                          Putting 1 is equivalent to putting 0 and calling Ln() just after. <br/>
1047
     *                          Default value: 0. <br/>
1048
     * @param string $align     Allows to center or align the text. <br/>
1049
     *                          Possible values are: <ul>
1050
     *                          <li> 'L' or empty string: left align (default value) </li>
1051
     *                          <li> 'C': center </li>
1052
     *                          <li> 'R': right align </li></ul>
1053
     * @param boolean $fill     Indicates if the cell background must be painted (true) or transparent (false). <br/>
1054
     *                          If set to true, current FillColor is used for the background. <br/>
1055
     *                          Default value: false. <br/>
1056
     * @param string|int $link  URL or identifier for internal link created by AddLink().
1057
     */
1058
    public function cell(float $w, float $h = 0, string $txt = '', $border = 0, float $ln = 0, $align = '', $fill = false, $link = '') : void
1059
    {
1060
        $txt = $this->conv($txt);
1061
        $this->internalCell($w, $h, $txt, $border, $ln, $align, $fill, $link);
1062
    }
1063
1064
1065
    /**
1066
     * The internal function is necessaray for calls from multiCell() and write() where the
1067
     * text is already converted from UTF-8 for some width calculations. Multiple conversion
1068
     * leads to undetermined characters.
1069
     * @see FPDF::cell()
1070
     * @param float $w
1071
     * @param float $h
1072
     * @param string $txt
1073
     * @param int|string $border
1074
     * @param float $ln
1075
     * @param string $align
1076
     * @param boolean $fill
1077
     * @param string|int $link
1078
     */
1079
    private function internalCell(float $w, float $h = 0, string $txt = '', $border = 0, float $ln = 0, $align = '', $fill = false, $link = '') : void
1080
    {
1081
        // Output a cell
1082
        $k = $this->k;
1083
        if ($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->acceptPageBreak()) {
1084
            // Automatic page break
1085
            $x = $this->x;
1086
            $ws = $this->ws;
1087
            if ($ws > 0) {
1088
                $this->ws = 0;
1089
                $this->out('0 Tw');
1090
            }
1091
            $this->addPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation);
1092
            $this->x = $x;
1093
            if ($ws > 0) {
1094
                $this->ws = $ws;
1095
                $this->out(sprintf('%.3F Tw', $ws * $k));
1096
            }
1097
        }
1098
        if ($w == 0) {
1099
            $w = $this->w - $this->rMargin - $this->x;
1100
        }
1101
        $s = '';
1102
        if ($fill || $border == 1) {
1103
            if ($fill) {
1104
                $op = ($border == 1) ? 'B' : 'f';
1105
            } else {
1106
                $op = 'S';
1107
            }
1108
            $s = sprintf('%.2F %.2F %.2F %.2F re %s ', $this->x * $k, ($this->h - $this->y) * $k, $w * $k, -$h * $k, $op);
1109
        }
1110
        if (is_string($border)) {
1111
            $x = $this->x;
1112
            $y = $this->y;
1113
            if (strpos($border, 'L') !== false) {
1114
                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, $x * $k, ($this->h - ($y + $h)) * $k);
1115
            }
1116
            if (strpos($border, 'T') !== false) {
1117
                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - $y) * $k);
1118
            }
1119
            if (strpos($border, 'R') !== false) {
1120
                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', ($x + $w) * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k);
1121
            }
1122
            if (strpos($border, 'B') !== false) {
1123
                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - ($y + $h)) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k);
1124
            }
1125
        }
1126
        if ($txt !== '') {
1127
            if (!isset($this->CurrentFont)) {
1128
                $this->error('No font has been set');
1129
            }
1130
            if ($align == 'R') {
1131
                $dx = $w - $this->cMargin - $this->getStringWidth($txt);
1132
            } elseif ($align == 'C') {
1133
                $dx = ($w - $this->getStringWidth($txt)) / 2;
1134
            } else {
1135
                $dx = $this->cMargin;
1136
            }
1137
            if ($this->ColorFlag) {
1138
                $s .= 'q ' . $this->TextColor . ' ';
1139
            }
1140
            $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));
1141
            if ($this->underline) {
1142
                $s .= ' ' . $this->doUnderline($this->x + $dx, $this->y + 0.5 * $h + 0.3 * $this->FontSize, $txt);
1143
            }
1144
            if ($this->ColorFlag) {
1145
                $s .= ' Q';
1146
            }
1147
            if ($link) {
1148
                $this->link($this->x + $dx, $this->y + 0.5 * $h - 0.5 * $this->FontSize, $this->getStringWidth($txt), $this->FontSize, $link);
1149
            }
1150
        }
1151
        if ($s) {
1152
            $this->out($s);
1153
        }
1154
        $this->lasth = $h;
1155
        if ($ln > 0) {
1156
            // Go to next line
1157
            $this->y += $h;
1158
            if ($ln == 1) {
1159
                $this->x = $this->lMargin;
1160
            }
1161
        } else {
1162
            $this->x += $w;
1163
        }
1164
    }
1165
1166
    /**
1167
     * This method allows printing text with line breaks.
1168
     * They can be automatic (as soon as the text reaches the right border of the cell) or
1169
     * explicit (via the \n character).
1170
     * As many cells as necessary are output, one below the other.
1171
     * Text can be aligned, centered or justified. The cell block can be framed and the background painted.
1172
     * @param float $w          Cell width. If 0, the cell extends up to the right margin.
1173
     * @param float $h          Cell height. Default value: 0.
1174
     * @param string $txt       String to print. Default value: empty string.
1175
     * @param int|string $border    Indicates if borders must be drawn around the cell. <br/>
1176
     *                          The value can be either a number: <ul>
1177
     *                          <li>0: no border </li>
1178
     *                          <li>1: frame </li></ul>
1179
     *                          or a string containing some or all of the following characters (in any order): <ul>
1180
     *                          <li> 'L': left </li>
1181
     *                          <li> 'T': top </li>
1182
     *                          <li> 'R': right </li>
1183
     *                          <li> 'B': bottom </li></ul>
1184
     *                          Default value: 0. <br/>
1185
     * @param string $align     Allows to center or align the text. <br/>
1186
     *                          Possible values are: <ul>
1187
     *                          <li> 'L' or empty string: left align (default value) </li>
1188
     *                          <li> 'C': center </li>
1189
     *                          <li> 'R': right align </li></ul>
1190
     * @param boolean $fill     Indicates if the cell background must be painted (true) or transparent (false). <br/>
1191
     *                          If set to true, current FillColor is used for the background. <br/>
1192
     *                          Default value: false.
1193
     */
1194
    public function multiCell(float $w, float $h, string $txt, $border = 0, string $align = 'J', bool $fill = false) : void
1195
    {
1196
        // Output text with automatic or explicit line breaks
1197
        if (!isset($this->CurrentFont)) {
1198
            $this->error('No font has been set');
1199
        }
1200
        $cw = &$this->CurrentFont['cw'];
1201
        if ($w == 0) {
1202
            $w = $this->w - $this->rMargin - $this->x;
1203
        }
1204
        $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1205
        $s = $this->conv(str_replace("\r", '', $txt));
1206
        $nb = strlen($s);
1207
        if ($nb > 0 && $s[$nb - 1] == "\n") {
1208
            $nb--;
1209
        }
1210
        $b = 0;
1211
        $b2 = '';
1212
        if ($border) {
1213
            if ($border == 1) {
1214
                $border = 'LTRB';
1215
                $b = 'LRT';
1216
                $b2 = 'LR';
1217
            } else {
1218
                $b2 = '';
1219
                if (strpos($border, 'L') !== false) {
1220
                    $b2 .= 'L';
1221
                }
1222
                if (strpos($border, 'R') !== false) {
1223
                    $b2 .= 'R';
1224
                }
1225
                $b = (strpos($border, 'T') !== false) ? $b2 . 'T' : $b2;
1226
            }
1227
        }
1228
        $sep = -1;
1229
        $i = 0;
1230
        $j = 0;
1231
        $l = 0;
1232
        $ns = 0;
1233
        $nl = 1;
1234
        $ls = 0;
1235
        while ($i < $nb) {
1236
            // Get next character
1237
            $c = $s[$i];
1238
            if ($c == "\n") {
1239
                // Explicit line break
1240
                if ($this->ws > 0) {
1241
                    // reset wordspacing
1242
                    $this->ws = 0;
1243
                    $this->out('0 Tw');
1244
                }
1245
                $strLine = substr($s, $j, $i - $j);
1246
                $this->internalCell($w, $h, $strLine, $b, 2, $align, $fill);
1247
                $i++;
1248
                $sep = -1;
1249
                $j = $i;
1250
                $l = 0;
1251
                $ns = 0;
1252
                $nl++;
1253
                if ($border && $nl == 2) {
1254
                    $b = $b2;
1255
                }
1256
                continue;
1257
            }
1258
            if ($c == ' ') {
1259
                $sep = $i;  // last space
1260
                $ls = $l;   // current line width until last space
1261
                $ns++;      // number of spaces
1262
            }
1263
            $l += $cw[$c];
1264
            if ($l > $wmax) {
1265
                // Automatic line break
1266
                if ($sep == -1) {
1267
                    // no space in this line... hard break
1268
                    if ($i == $j) {
1269
                        $i++;
1270
                    }
1271
                    if ($this->ws > 0) {
1272
                        // reset wordspacing
1273
                        $this->ws = 0;
1274
                        $this->out('0 Tw');
1275
                    }
1276
                    $strLine = substr($s, $j, $i - $j);
1277
                    $this->internalCell($w, $h, $strLine, $b, 2, $align, $fill);
1278
                } else {
1279
                    if ($align == 'J') {
1280
                        // justified print - calc wordspacing
1281
                        $this->ws = ($ns > 1) ? ($wmax - $ls) / 1000 * $this->FontSize / ($ns - 1) : 0;
1282
                        $this->out(sprintf('%.3F Tw', $this->ws * $this->k));
1283
                    }
1284
                    $strLine = substr($s, $j, $sep - $j);
1285
                    $this->internalCell($w, $h, $strLine, $b, 2, $align, $fill);
1286
                    $i = $sep + 1;
1287
                }
1288
                $sep = -1;
1289
                $j = $i;
1290
                $l = 0;
1291
                $ns = 0;
1292
                $nl++;
1293
                if ($border && $nl == 2) {
1294
                    $b = $b2;
1295
                }
1296
            } else {
1297
                $i++;
1298
            }
1299
        }
1300
        // Last chunk
1301
        if ($this->ws > 0) {
1302
            $this->ws = 0;
1303
            $this->out('0 Tw');
1304
        }
1305
        if ($border && strpos($border, 'B') !== false) {
1306
            $b .= 'B';
1307
        }
1308
        $this->cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill);
1309
        $this->x = $this->lMargin;
1310
    }
1311
1312
    /**
1313
     * This method prints text from the current position.
1314
     * When the right margin is reached (or the \n character is met) a line break occurs
1315
     * and text continues from the left margin. Upon method exit, the current position
1316
     * is left just at the end of the text.
1317
     * It is possible to put a link on the text.
1318
     * @param float $h          Line height.
1319
     * @param string $txt       String to print.
1320
     * @param string|int $link  URL or identifier for internal link created by AddLink().
1321
     */
1322
    public function write(float $h, string $txt, $link = '') : void
1323
    {
1324
        // Output text in flowing mode
1325
        if (!isset($this->CurrentFont)) {
1326
            $this->error('No font has been set');
1327
        }
1328
        $cw = &$this->CurrentFont['cw'];
1329
        $w = $this->w - $this->rMargin - $this->x;
1330
        $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1331
        $s = $this->conv(str_replace("\r", '', $txt));
1332
        $nb = strlen($s);
1333
        $sep = -1;
1334
        $i = 0;
1335
        $j = 0;
1336
        $l = 0;
1337
        $nl = 1;
1338
        while ($i < $nb) {
1339
            // Get next character
1340
            $c = $s[$i];
1341
            if ($c == "\n") {
1342
                // Explicit line break
1343
                $this->internalCell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link);
1344
                $i++;
1345
                $sep = -1;
1346
                $j = $i;
1347
                $l = 0;
1348
                if ($nl == 1) {
1349
                    $this->x = $this->lMargin;
1350
                    $w = $this->w - $this->rMargin - $this->x;
1351
                    $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1352
                }
1353
                $nl++;
1354
                continue;
1355
            }
1356
            if ($c == ' ') {
1357
                $sep = $i;
1358
            }
1359
            $l += $cw[$c];
1360
            if ($l > $wmax) {
1361
                // Automatic line break
1362
                if ($sep == -1) {
1363
                    if ($this->x > $this->lMargin) {
1364
                        // Move to next line
1365
                        $this->x = $this->lMargin;
1366
                        $this->y += $h;
1367
                        $w = $this->w - $this->rMargin - $this->x;
1368
                        $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1369
                        $i++;
1370
                        $nl++;
1371
                        continue;
1372
                    }
1373
                    if ($i == $j) {
1374
                        $i++;
1375
                    }
1376
                    $this->internalCell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link);
1377
                } else {
1378
                    $this->internalCell($w, $h, substr($s, $j, $sep - $j), 0, 2, '', false, $link);
1379
                    $i = $sep + 1;
1380
                }
1381
                $sep = -1;
1382
                $j = $i;
1383
                $l = 0;
1384
                if ($nl == 1) {
1385
                    $this->x = $this->lMargin;
1386
                    $w = $this->w - $this->rMargin - $this->x;
1387
                    $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
1388
                }
1389
                $nl++;
1390
            } else {
1391
                $i++;
1392
            }
1393
        }
1394
        // Last chunk
1395
        if ($i != $j) {
1396
            $this->internalCell($l / 1000 * $this->FontSize, $h, substr($s, $j), 0, 0, '', false, $link);
1397
        }
1398
    }
1399
1400
    /**
1401
     * Performs a line break.
1402
     * The current X-position goes back to the left margin and the Y-position increases by
1403
     * the amount passed in parameter.
1404
     * @param float $h  The height of the break. <br/>
1405
     *                  By default, the value equals the height of the last printed cell.
1406
     */
1407
    public function ln(float $h = null) : void
1408
    {
1409
        // Line feed; default value is the last cell height
1410
        $this->x = $this->lMargin;
1411
        if ($h === null) {
1412
            $this->y += $this->lasth;
1413
        } else {
1414
            $this->y += $h;
1415
        }
1416
    }
1417
1418
    /**
1419
     * Puts an image.
1420
     * The size it will take on the page can be specified in different ways: <ul>
1421
     * <li> explicit width and height (expressed in user unit or dpi) </li>
1422
     * <li> one explicit dimension, the other being calculated automatically in order to keep the original proportions </li>
1423
     * <li> no explicit dimension, in which case the image is put at 96 dpi </li></ul>
1424
     * Supported formats are JPEG, PNG and GIF. <b>The GD extension is required for GIF.</b><br/>
1425
     * For JPEGs, all flavors are allowed: <ul>
1426
     * <li> gray scales </li>
1427
     * <li> true colors (24 bits) </li>
1428
     * <li> CMYK (32 bits) </li></ul>
1429
     * For PNGs, are allowed: <ul>
1430
     * <li> gray scales on at most 8 bits (256 levels) </li>
1431
     * <li> indexed colors </li>
1432
     * <li> true colors (24 bits) </li></ul>
1433
     * For GIFs: in case of an animated GIF, only the first frame is displayed. <br/><br/>
1434
     * Transparency is supported. <br/><br/>
1435
     * The format can be specified explicitly or inferred from the file extension. <br/><br/>
1436
     * It is possible to put a link on the image. <br/><br/>
1437
     * <b>Remark:</b> if an image is used several times, only one copy is embedded in the file.
1438
     * @param string $file  Path or URL of the image.
1439
     * @param float $x      X-position of the upper-left corner. <br/> <br/>
1440
     *                      If not specified or equal to null, the current X-position is used. <br/>
1441
     * @param float $y      Y-position of the upper-left corner. <br/>
1442
     *                      If not specified or equal to null, the current Y-position is used; <br/>
1443
     *                      moreover, a page break is triggered first if necessary (in case automatic page breaking is enabled) and, <br/>
1444
     *                      after the call, the current Y-position is moved to the bottom of the image. <br/>
1445
     * @param float $w      Width of the image in the page. <br/>
1446
     *                      There are three cases: <ul>
1447
     *                      <li> If the value is positive, it represents the width in user unit </li>
1448
     *                      <li> If the value is negative, the absolute value represents the horizontal resolution in dpi </li>
1449
     *                      <li> If the value is not specified or equal to zero, it is automatically calculated </li><ul>
1450
     * @param float $h      Height of the image in the page. <br/>
1451
     *                      There are three cases: <ul>
1452
     *                      <li> If the value is positive, it represents the height in user unit </li>
1453
     *                      <li> If the value is negative, the absolute value represents the vertical resolution in dpi </li>
1454
     *                      <li> If the value is not specified or equal to zero, it is automatically calculated </li><ul>
1455
     * @param string $type  Image format. <br/>
1456
     *                      Possible values are (case insensitive): <ul>
1457
     *                      <li> JPG </li>
1458
     *                      <li> JPEG </li>
1459
     *                      <li> PNG </li>
1460
     *                      <li> GIF </li></ul>
1461
     *                      If not specified, the type is inferred from the file extension. <br/>
1462
     * @param string|int $link  URL or identifier for internal link created by AddLink().
1463
     */
1464
    public function image(string $file, ?float $x = null, ?float $y = null, float $w = 0, float $h = 0, string $type = '', $link = '') : void
1465
    {
1466
        // Put an image on the page
1467
        if ($file == '') {
1468
            $this->error('Image file name is empty');
1469
            return;
1470
        }
1471
        if (!isset($this->images[$file])) {
1472
            // First use of this image, get info
1473
            if ($type == '') {
1474
                $pos = strrpos($file, '.');
1475
                if (!$pos) {
1476
                    $this->error('Image file has no extension and no type was specified: ' . $file);
1477
                    return;
1478
                }
1479
                $type = substr($file, $pos + 1);
1480
            }
1481
            $type = strtolower($type);
1482
            if ($type == 'jpeg') {
1483
                $type = 'jpg';
1484
            }
1485
            $mtd = 'parse' . ucfirst($type);
1486
            if (!method_exists($this, $mtd)) {
1487
                $this->error('Unsupported image type: ' . $type);
1488
            }
1489
            $info = $this->$mtd($file);
1490
            $info['i'] = count($this->images) + 1;
1491
            $this->images[$file] = $info;
1492
        } else {
1493
            $info = $this->images[$file];
1494
        }
1495
1496
        // Automatic width and height calculation if needed
1497
        if ($w == 0 && $h == 0) {
1498
            // Put image at 96 dpi
1499
            $w = -96;
1500
            $h = -96;
1501
        }
1502
        if ($w < 0) {
1503
            $w = -$info['w'] * 72 / $w / $this->k;
1504
        }
1505
        if ($h < 0) {
1506
            $h = -$info['h'] * 72 / $h / $this->k;
1507
        }
1508
        if ($w == 0) {
1509
            $w = $h * $info['w'] / $info['h'];
1510
        }
1511
        if ($h == 0) {
1512
            $h = $w * $info['h'] / $info['w'];
1513
        }
1514
1515
        // Flowing mode
1516
        if ($y === null) {
1517
            if ($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->acceptPageBreak()) {
1518
                // Automatic page break
1519
                $x2 = $this->x;
1520
                $this->addPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation);
1521
                $this->x = $x2;
1522
            }
1523
            $y = $this->y;
1524
            $this->y += $h;
1525
        }
1526
1527
        if ($x === null) {
1528
            $x = $this->x;
1529
        }
1530
        $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']));
1531
        if ($link) {
1532
            $this->link($x, $y, $w, $h, $link);
1533
        }
1534
    }
1535
1536
    /**
1537
     * Set bookmark at current position.
1538
     * <b>from FPDF.org extension to create Bookmarks</b>
1539
     * @param string $txt
1540
     * @param bool $isUTF8
1541
     * @param int $level
1542
     * @param int $y
1543
     */
1544
    public function bookmark(string $txt, bool $isUTF8 = true, int $level = 0, int $y = 0) : void
1545
    {
1546
        if (!$isUTF8) {
1547
            $txt = utf8_encode($txt);
1548
        }
1549
        if ($y == -1) {
1550
            $y = $this->getY();
1551
        }
1552
        $this->outlines[] = array('t' => $txt, 'l' => $level, 'y' => ($this->h - $y) * $this->k, 'p' => $this->pageNo());
1553
    }
1554
1555
    /**
1556
     * Get current page width.
1557
     * @return float
1558
     */
1559
    public function getPageWidth() : float
1560
    {
1561
        // Get current page width
1562
        return $this->w;
1563
    }
1564
1565
    /**
1566
     * Get current page height.
1567
     * @return float
1568
     */
1569
    public function getPageHeight() : float
1570
    {
1571
        // Get current page height
1572
        return $this->h;
1573
    }
1574
1575
    /**
1576
     * Get the trigger for autopagebreak
1577
     * @return float
1578
     */
1579
    public function getPageBreakTrigger() : float
1580
    {
1581
        // Get current page height
1582
        return $this->PageBreakTrigger;
1583
    }
1584
1585
    /**
1586
     * Get current x position.
1587
     * @return float
1588
     */
1589
    public function getX() : float
1590
    {
1591
        // GetX position
1592
        return $this->x;
1593
    }
1594
1595
    /**
1596
     * Set new X position.
1597
     * If the passed value is negative, it is relative to the right of the page.
1598
     * @param float $x
1599
     */
1600
    public function setX(float $x) : void
1601
    {
1602
        // Set x position
1603
        if ($x >= 0) {
1604
            $this->x = $x;
1605
        } else {
1606
            $this->x = $this->w + $x;
1607
        }
1608
    }
1609
1610
    /**
1611
     * Get current Y position.
1612
     * @return float
1613
     */
1614
    public function getY() : float
1615
    {
1616
        // Get y position
1617
        return $this->y;
1618
    }
1619
1620
    /**
1621
     * Set new Y position and optionally moves the current X-position back to the left margin.
1622
     * If the passed value is negative, it is relative to the bottom of the page.
1623
     * @param float $y
1624
     * @param bool $resetX
1625
     */
1626
    public function setY(float $y, bool $resetX = true) : void
1627
    {
1628
        // Set y position and optionally reset x
1629
        if ($y >= 0) {
1630
            $this->y = $y;
1631
        } else {
1632
            $this->y = $this->h + $y;
1633
        }
1634
        if ($resetX) {
1635
            $this->x = $this->lMargin;
1636
        }
1637
    }
1638
1639
    /**
1640
     * Set new X and Y position.
1641
     * If the passed values are negative, they are relative respectively to the right and bottom of the page.
1642
     * @param float $x
1643
     * @param float $y
1644
     */
1645
    public function setXY(float $x, float $y) : void
1646
    {
1647
        // Set x and y positions
1648
        $this->setX($x);
1649
        $this->setY($y, false);
1650
    }
1651
1652
    /**
1653
     * Send the document to a given destination: browser, file or string.
1654
     * In the case of a browser, the PDF viewer may be used or a download may be forced.
1655
     * The method first calls Close() if necessary to terminate the document.
1656
     * @param string $dest  Destination where to send the document. <br/>
1657
     *                      It can be one of the following: <ul>
1658
     *                      <li> 'I': send the file inline to the browser. The PDF viewer is used if available. </li>
1659
     *                      <li> 'D': send to the browser and force a file download with the name given by name. </li>
1660
     *                      <li> 'F': save to a local file with the name given by name (may include a path). </li>
1661
     *                      <li> 'S': return the document as a string. </li></ul>
1662
     *                      The default value is I. <br/>
1663
     * @param string $name  The name of the file. It is ignored in case of destination 'S'. <br/>
1664
     *                      The default value is doc.pdf. <br/>
1665
     * @param bool $isUTF8  Indicates if name is encoded in ISO-8859-1 (false) or UTF-8 (true). <br/>
1666
     *                      Only used for destinations I and D. <br/>
1667
     *                      The default value is true. <br/>
1668
     * @return string
1669
     */
1670
    public function output(string $dest = '', string $name = '', bool $isUTF8 = true) : string
1671
    {
1672
        // Output PDF to some destination
1673
        $this->close();
1674
        if (strlen($name) == 1 && strlen($dest) != 1) {
1675
            // Fix parameter order
1676
            $tmp = $dest;
1677
            $dest = $name;
1678
            $name = $tmp;
1679
        }
1680
        if ($dest == '') {
1681
            $dest = 'I';
1682
        }
1683
        if ($name == '') {
1684
            $name = 'doc.pdf';
1685
        }
1686
        switch (strtoupper($dest)) {
1687
            case 'I':
1688
                // Send to standard output
1689
                $this->checkOutput();
1690
                if (PHP_SAPI != 'cli') {
1691
                    // We send to a browser
1692
                    header('Content-Type: application/pdf; charset=UTF-8');
1693
                    header('Content-Disposition: inline; ' . $this->httpEncode('filename', $name, $isUTF8));
1694
                    header('Cache-Control: private, max-age=0, must-revalidate');
1695
                    header('Pragma: public');
1696
                }
1697
                echo $this->buffer;
1698
                break;
1699
            case 'D':
1700
                // Download file
1701
                $this->checkOutput();
1702
                header('Content-Type: application/x-download');
1703
                header('Content-Disposition: attachment; ' . $this->httpEncode('filename', $name, $isUTF8));
1704
                header('Cache-Control: private, max-age=0, must-revalidate');
1705
                header('Pragma: public');
1706
                echo $this->buffer;
1707
                break;
1708
            case 'F':
1709
                // Save to local file
1710
                if (!file_put_contents($name, $this->buffer)) {
1711
                    $this->error('Unable to create output file: ' . $name);
1712
                }
1713
                break;
1714
            case 'S':
1715
                // Return as a string
1716
                return $this->buffer;
1717
            default:
1718
                $this->error('Incorrect output destination: ' . $dest);
1719
        }
1720
        return '';
1721
    }
1722
1723
    /**
1724
     * Some internal checks before starting.
1725
     */
1726
    protected function doChecks() : void
1727
    {
1728
        // Check mbstring overloading
1729
        if ((ini_get('mbstring.func_overload') & 2) !== 0) {
1730
            $this->error('mbstring overloading must be disabled');
1731
        }
1732
    }
1733
1734
    /**
1735
     * get the path for the font definitions
1736
     */
1737
    protected function getFontPath() : void
1738
    {
1739
        // Font path
1740
        $this->fontpath = '';
1741
        if (defined('FPDF_FONTPATH')) {
1742
            $this->fontpath = FPDF_FONTPATH;
1743
            if (substr($this->fontpath, -1) != '/' && substr($this->fontpath, -1) != '\\') {
1744
                $this->fontpath .= '/';
1745
            }
1746
        } elseif (is_dir(dirname(__FILE__) . '/font')) {
1747
            $this->fontpath = dirname(__FILE__) . '/font/';
1748
        }
1749
    }
1750
1751
    /**
1752
     * Some internal checks before output.
1753
     */
1754
    protected function checkOutput() : void
1755
    {
1756
        if (PHP_SAPI != 'cli') {
1757
            $file = '';
1758
            $line = 0;
1759
            if (headers_sent($file, $line)) {
1760
                $this->error("Some data has already been output, can't send PDF file (output started at $file:$line)");
1761
            }
1762
        }
1763
        if (ob_get_length() !== false) {
1764
            // The output buffer is not empty
1765
            if (preg_match('/^(\xEF\xBB\xBF)?\s*$/', ob_get_contents())) {
1766
                // It contains only a UTF-8 BOM and/or whitespace, let's clean it
1767
                ob_clean();
1768
            } else {
1769
                $this->error("Some data has already been output, can't send PDF file");
1770
            }
1771
        }
1772
    }
1773
1774
    /**
1775
     * Get dimensions of selected pagesize.
1776
     * @param string|array $size
1777
     * @return array
1778
     */
1779
    protected function getPageSize($size) : array
1780
    {
1781
        if (is_string($size)) {
1782
            $aStdPageSizes = array(
1783
                'a3'=>array(841.89, 1190.55),
1784
                'a4'=>array(595.28, 841.89),
1785
                'a5'=>array(420.94, 595.28),
1786
                'letter'=>array(612, 792),
1787
                'legal'=>array(612, 1008)
1788
            );
1789
1790
            $size = strtolower($size);
1791
            if (!isset($aStdPageSizes[$size])) {
1792
                $this->error('Unknown page size: ' . $size);
1793
                $a = $aStdPageSizes['a4'];
1794
            } else {
1795
                $a = $aStdPageSizes[$size];
1796
            }
1797
            return array($a[0] / $this->k, $a[1] / $this->k);
1798
        } else {
1799
            if ($size[0] > $size[1]) {
1800
                return array($size[1], $size[0]);
1801
            } else {
1802
                return $size;
1803
            }
1804
        }
1805
    }
1806
1807
    /**
1808
     * Start new page.
1809
     * @param string $orientation
1810
     * @param string|array $size
1811
     * @param int $rotation
1812
     */
1813
    protected function beginPage(string $orientation, $size, int $rotation) : void
1814
    {
1815
        $this->page++;
1816
        $this->pages[$this->page] = '';
1817
        $this->state = 2;
1818
        $this->x = $this->lMargin;
1819
        $this->y = $this->tMargin;
1820
        $this->FontFamily = '';
1821
        // Check page size and orientation
1822
        if ($orientation == '') {
1823
            $orientation = $this->DefOrientation;
1824
        } else {
1825
            $orientation = strtoupper($orientation[0]);
1826
        }
1827
        if ($size == '') {
1828
            $size = $this->DefPageSize;
1829
        } else {
1830
            $size = $this->getPageSize($size);
1831
        }
1832
        if ($orientation != $this->CurOrientation || $size[0] != $this->CurPageSize[0] || $size[1] != $this->CurPageSize[1]) {
1833
            // New size or orientation
1834
            if ($orientation == 'P') {
1835
                $this->w = $size[0];
1836
                $this->h = $size[1];
1837
            } else {
1838
                $this->w = $size[1];
1839
                $this->h = $size[0];
1840
            }
1841
            $this->wPt = $this->w * $this->k;
1842
            $this->hPt = $this->h * $this->k;
1843
            $this->PageBreakTrigger = $this->h - $this->bMargin;
1844
            $this->CurOrientation = $orientation;
1845
            $this->CurPageSize = $size;
1846
        }
1847
        if ($orientation != $this->DefOrientation || $size[0] != $this->DefPageSize[0] || $size[1] != $this->DefPageSize[1]) {
1848
            $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt);
1849
        }
1850
        if ($rotation != 0) {
1851
            if ($rotation % 90 != 0) {
1852
                $this->error('Incorrect rotation value: ' . $rotation);
1853
            }
1854
            $this->CurRotation = $rotation;
1855
            $this->PageInfo[$this->page]['rotation'] = $rotation;
1856
        }
1857
    }
1858
1859
    /**
1860
     * End of current page.
1861
     */
1862
    protected function endPage() : void
1863
    {
1864
        $this->state = 1;
1865
    }
1866
1867
    /**
1868
     * Load a font definition file from the font directory.
1869
     * @param string $font
1870
     * @return array
1871
     */
1872
    protected function loadFont(string $font) : array
1873
    {
1874
        // Load a font definition file from the font directory
1875
        if (strpos($font, '/') !== false || strpos($font, "\\") !== false) {
1876
            $this->error('Incorrect font definition file name: ' . $font);
1877
            return [];
1878
        }
1879
        // following vars must be initialized in the font definition file beeing included
1880
        $name = null;
1881
        $enc = null;
1882
        $subsetted = null;
1883
        include($this->fontpath . $font);
1884
1885
        // phpstan can't see the code dynamicly included before so assuming $name, $enc, $subsetted always set to null!
1886
        if (!isset($name)) {            /* @phpstan-ignore-line */
1887
            $this->error('Could not include font definition file');
1888
        }
1889
        if (isset($enc)) {              /* @phpstan-ignore-line */
1890
            $enc = strtolower($enc);
1891
        }
1892
        if (!isset($subsetted)) {       /* @phpstan-ignore-line */
1893
            $subsetted = false;
1894
        }
1895
        return get_defined_vars();
1896
    }
1897
1898
    /**
1899
     * Check if string only contains ascii chars (0...127).
1900
     * @param string $s
1901
     * @return bool
1902
     */
1903
    protected function isAscii(string $s) : bool
1904
    {
1905
        // Test if string is ASCII
1906
        $nb = strlen($s);
1907
        for ($i = 0; $i < $nb; $i++) {
1908
            if (ord($s[$i]) > 127) {
1909
                return false;
1910
            }
1911
        }
1912
        return true;
1913
    }
1914
1915
    /**
1916
     * @param string $param
1917
     * @param string $value
1918
     * @param bool $isUTF8
1919
     * @return string
1920
     */
1921
    protected function httpEncode(string $param, string $value, bool $isUTF8) : string
1922
    {
1923
        // Encode HTTP header field parameter
1924
        if ($this->isAscii($value)) {
1925
            return $param . '="' . $value . '"';
1926
        }
1927
        if (!$isUTF8) {
1928
            $value = utf8_encode($value);
1929
        }
1930
        if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false) {
1931
            return $param . '="' . rawurlencode($value) . '"';
1932
        } else {
1933
            return $param . "*=UTF-8''" . rawurlencode($value);
1934
        }
1935
    }
1936
1937
    /**
1938
     * Convert UTF8 to UTF16.
1939
     * @param string $s
1940
     * @return string
1941
     */
1942
    protected function convUTF8toUTF16(string $s) : string
1943
    {
1944
        // Convert UTF-8 to UTF-16BE with BOM
1945
        $res = "\xFE\xFF";
1946
        $nb = strlen($s);
1947
        $i = 0;
1948
        while ($i < $nb) {
1949
            $c1 = ord($s[$i++]);
1950
            if ($c1 >= 224) {
1951
                // 3-byte character
1952
                $c2 = ord($s[$i++]);
1953
                $c3 = ord($s[$i++]);
1954
                $res .= chr((($c1 & 0x0F) << 4) + (($c2 & 0x3C) >> 2));
1955
                $res .= chr((($c2 & 0x03) << 6) + ($c3 & 0x3F));
1956
            } elseif ($c1 >= 192) {
1957
                // 2-byte character
1958
                $c2 = ord($s[$i++]);
1959
                $res .= chr(($c1 & 0x1C) >> 2);
1960
                $res .= chr((($c1 & 0x03) << 6) + ($c2 & 0x3F));
1961
            } else {
1962
                // Single-byte character
1963
                $res .= "\0" . chr($c1);
1964
            }
1965
        }
1966
        return $res;
1967
    }
1968
1969
    /**
1970
     * Escape special characters.
1971
     * @param string $s
1972
     * @return string
1973
     */
1974
    protected function escape(string $s) : string
1975
    {
1976
        // Escape special characters
1977
        if (strpos($s, '(') !== false ||
1978
            strpos($s, ')') !== false ||
1979
            strpos($s, '\\') !== false ||
1980
            strpos($s, "\r") !== false) {
1981
            $s = str_replace(array('\\', '(', ')', "\r"), array('\\\\', '\\(', '\\)', '\\r'), $s);
1982
        }
1983
        return $s;
1984
    }
1985
1986
    /**
1987
     * @param string $s
1988
     * @return string
1989
     */
1990
    protected function conv(string $s) : string
1991
    {
1992
        if ($this->charset != 'ISO-8859-15') {
1993
            $s = iconv($this->charset, 'ISO-8859-15//IGNORE', $s);
1994
        }
1995
        $s= str_replace('{eur}', chr(128) . '  ', $s);
1996
        return $s;
1997
    }
1998
1999
2000
    /**
2001
     * Format a text string.
2002
     * @param string $s
2003
     * @return string
2004
     */
2005
    protected function textString(string $s) : string
2006
    {
2007
        // Format a text string
2008
        if (!$this->isAscii($s)) {
2009
            $s = $this->convUTF8toUTF16($s);
2010
        }
2011
        return '(' . $this->escape($s) . ')';
2012
    }
2013
2014
    /**
2015
     * Underline text with 'simple' line.
2016
     * @param float $x
2017
     * @param float $y
2018
     * @param string $txt
2019
     * @return string
2020
     */
2021
    protected function doUnderline(float $x, float $y, string $txt) : string
2022
    {
2023
        // Underline text
2024
        $up = $this->CurrentFont['up'];
2025
        $ut = $this->CurrentFont['ut'];
2026
        $w = $this->getStringWidth($txt) + $this->ws * substr_count($txt, ' ');
2027
        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);
2028
    }
2029
2030
    /**
2031
     * Extract info from a JPEG file.
2032
     * @param string $file
2033
     * @return array
2034
     */
2035
    protected function parseJpg(string $file) : array
2036
    {
2037
        // Extract info from a JPEG file
2038
        $a = getimagesize($file);
2039
        if (!$a) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $a of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2040
            $this->error('Missing or incorrect image file: ' . $file);
2041
            return [];
2042
        }
2043
        if ($a[2] != 2) {
2044
            $this->error('Not a JPEG file: ' . $file);
2045
            return [];
2046
        }
2047
        if (!isset($a['channels']) || $a['channels'] == 3) {
2048
            $colspace = 'DeviceRGB';
2049
        } elseif ($a['channels'] == 4) {
2050
            $colspace = 'DeviceCMYK';
2051
        } else {
2052
            $colspace = 'DeviceGray';
2053
        }
2054
        $bpc = isset($a['bits']) ? $a['bits'] : 8;
2055
        $data = file_get_contents($file);
2056
        return array('w' => $a[0], 'h' => $a[1], 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'DCTDecode', 'data' => $data);
2057
    }
2058
2059
    /**
2060
     * Extract info from a PNG file.
2061
     * @param string $file
2062
     * @return array
2063
     */
2064
    protected function parsePng(string $file) : array
2065
    {
2066
        // Extract info from a PNG file
2067
        $f = fopen($file, 'rb');
2068
        if ($f === false) {
2069
            $this->error('Can\'t open image file: ' . $file);
2070
            return [];
2071
        }
2072
        $info = $this->parsePngStream($f, $file);
2073
        fclose($f);
2074
        return $info;
2075
    }
2076
2077
    /**
2078
     * Extract info from a PNG stream
2079
     * @param resource $f
2080
     * @param string $file
2081
     * @return array
2082
     */
2083
    protected function parsePngStream($f, string $file) : array
2084
    {
2085
        // Check signature
2086
        if ($this->readStream($f, 8) != chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) {
2087
            $this->error('Not a PNG file: ' . $file);
2088
            return [];
2089
        }
2090
2091
        // Read header chunk
2092
        $this->readStream($f, 4);
2093
        if ($this->readStream($f, 4) != 'IHDR') {
2094
            $this->error('Incorrect PNG file: ' . $file);
2095
            return [];
2096
        }
2097
        $w = $this->readInt($f);
2098
        $h = $this->readInt($f);
2099
        $bpc = ord($this->readStream($f, 1));
2100
        if ($bpc > 8) {
2101
            $this->error('16-bit depth not supported: ' . $file);
2102
            return [];
2103
        }
2104
        $ct = ord($this->readStream($f, 1));
2105
        $colspace = '';
2106
        if ($ct == 0 || $ct == 4) {
2107
            $colspace = 'DeviceGray';
2108
        } elseif ($ct == 2 || $ct == 6) {
2109
            $colspace = 'DeviceRGB';
2110
        } elseif ($ct == 3) {
2111
            $colspace = 'Indexed';
2112
        } else {
2113
            $this->error('Unknown color type: ' . $file);
2114
            return [];
2115
        }
2116
        if (ord($this->readStream($f, 1)) != 0) {
2117
            $this->error('Unknown compression method: ' . $file);
2118
            return [];
2119
        }
2120
        if (ord($this->readStream($f, 1)) != 0) {
2121
            $this->error('Unknown filter method: ' . $file);
2122
            return [];
2123
        }
2124
        if (ord($this->readStream($f, 1)) != 0) {
2125
            $this->error('Interlacing not supported: ' . $file);
2126
            return [];
2127
        }
2128
        $this->readStream($f, 4);
2129
        $dp = '/Predictor 15 /Colors ' . ($colspace == 'DeviceRGB' ? 3 : 1) . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $w;
2130
2131
        // Scan chunks looking for palette, transparency and image data
2132
        $pal = '';
2133
        $trns = '';
2134
        $data = '';
2135
        do {
2136
            $n = $this->readInt($f);
2137
            $type = $this->readStream($f, 4);
2138
            if ($type == 'PLTE') {
2139
                // Read palette
2140
                $pal = $this->readStream($f, $n);
2141
                $this->readStream($f, 4);
2142
            } elseif ($type == 'tRNS') {
2143
                // Read transparency info
2144
                $t = $this->readStream($f, $n);
2145
                if ($ct == 0) {
2146
                    $trns = array(ord(substr($t, 1, 1)));
2147
                } elseif ($ct == 2) {
2148
                    $trns = array(ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1)));
2149
                } else {
2150
                    $pos = strpos($t, chr(0));
2151
                    if ($pos !== false) {
2152
                        $trns = array($pos);
2153
                    }
2154
                }
2155
                $this->readStream($f, 4);
2156
            } elseif ($type == 'IDAT') {
2157
                // Read image data block
2158
                $data .= $this->readStream($f, $n);
2159
                $this->readStream($f, 4);
2160
            } elseif ($type == 'IEND') {
2161
                break;
2162
            } else {
2163
                $this->readStream($f, $n + 4);
2164
            }
2165
        } while ($n);
2166
2167
        if ($colspace == 'Indexed' && empty($pal)) {
2168
            $this->error('Missing palette in ' . $file);
2169
            return [];
2170
        }
2171
        $info = array(
2172
            'w' => $w,
2173
            'h' => $h,
2174
            'cs' => $colspace,
2175
            'bpc' => $bpc,
2176
            'f' => 'FlateDecode',
2177
            'dp' => $dp,
2178
            'pal' => $pal,
2179
            'trns' => $trns
2180
        );
2181
2182
        if ($ct >= 4) {
2183
            // Extract alpha channel
2184
            if (!function_exists('gzuncompress')) {
2185
                $this->error('Zlib not available, can\'t handle alpha channel: ' . $file);
2186
                return [];
2187
            }
2188
            $data = gzuncompress($data);
2189
            $color = '';
2190
            $alpha = '';
2191
            if ($ct == 4) {
2192
                // Gray image
2193
                $len = 2 * $w;
2194
                for ($i = 0; $i < $h; $i++) {
2195
                    $pos = (1 + $len) * $i;
2196
                    $color .= $data[$pos];
2197
                    $alpha .= $data[$pos];
2198
                    $line = substr($data, $pos + 1, $len);
2199
                    $color .= preg_replace('/(.)./s', '$1', $line);
2200
                    $alpha .= preg_replace('/.(.)/s', '$1', $line);
2201
                }
2202
            } else {
2203
                // RGB image
2204
                $len = 4 * $w;
2205
                for ($i = 0; $i < $h; $i++) {
2206
                    $pos = (1 + $len) * $i;
2207
                    $color .= $data[$pos];
2208
                    $alpha .= $data[$pos];
2209
                    $line = substr($data, $pos + 1, $len);
2210
                    $color .= preg_replace('/(.{3})./s', '$1', $line);
2211
                    $alpha .= preg_replace('/.{3}(.)/s', '$1', $line);
2212
                }
2213
            }
2214
            unset($data);
2215
            $data = gzcompress($color);
2216
            $info['smask'] = gzcompress($alpha);
2217
            $this->WithAlpha = true;
2218
            if ($this->PDFVersion < '1.4') {
2219
                $this->PDFVersion = '1.4';
2220
            }
2221
        }
2222
        $info['data'] = $data;
2223
        return $info;
2224
    }
2225
2226
    /**
2227
     * Read n bytes from stream.
2228
     * @param resource $f
2229
     * @param int $n
2230
     * @return string
2231
     */
2232
    protected function readStream($f, int $n) : string
2233
    {
2234
        // Read n bytes from stream
2235
        $res = '';
2236
        while ($n > 0 && !feof($f)) {
2237
            $s = fread($f, $n);
2238
            if ($s === false) {
2239
                $this->error('Error while reading stream');
2240
                return '';
2241
            }
2242
            $n -= strlen($s);
2243
            $res .= $s;
2244
        }
2245
        if ($n > 0) {
2246
            $this->error('Unexpected end of stream');
2247
        }
2248
        return $res;
2249
    }
2250
2251
    /**
2252
     * Read a 4-byte integer from stream.
2253
     * @param resource $f
2254
     * @return int
2255
     */
2256
    protected function readInt($f) : int
2257
    {
2258
        // Read a 4-byte integer from stream
2259
        $a = unpack('Ni', $this->readStream($f, 4));
2260
        return $a['i'];
2261
    }
2262
2263
    /**
2264
     * Extract info from a GIF file.
2265
     * 1. GIF is converted to PNG using GD extension since PDF don't support
2266
     * GIF image format. <br/>
2267
     * 2. The converted image is written to memory stream <br/>
2268
     * 3. The PNG-memory stream is parsed using internal function for PNG
2269
     * @param string $file
2270
     * @return array
2271
     */
2272
    protected function parseGif(string $file) : array
2273
    {
2274
        // Extract info from a GIF file (via PNG conversion)
2275
        if (!function_exists('imagepng')) {
2276
            $this->error('GD extension is required for GIF support');
2277
            return [];
2278
        }
2279
        if (!function_exists('imagecreatefromgif')) {
2280
            $this->error('GD has no GIF read support');
2281
            return [];
2282
        }
2283
        $im = imagecreatefromgif($file);
2284
        if ($im === false) {
2285
            $this->error('Missing or incorrect image file: ' . $file);
2286
            return [];
2287
        }
2288
        imageinterlace($im, 0);
2289
        ob_start();
2290
        imagepng($im);
2291
        $data = ob_get_clean();
2292
        imagedestroy($im);
2293
        $f = fopen('php://temp', 'rb+');
2294
        if ($f === false) {
2295
            $this->error('Unable to create memory stream');
2296
            return [];
2297
        }
2298
        fwrite($f, $data);
2299
        rewind($f);
2300
        $info = $this->parsePngStream($f, $file);
2301
        fclose($f);
2302
        return $info;
2303
    }
2304
2305
    /**
2306
     * Add a line to the document.
2307
     * @param string $s
2308
     */
2309
    protected function out(string $s) : void
2310
    {
2311
        // Add a line to the document
2312
        switch ($this->state) {
2313
            case 0: // no page added so far
2314
                $this->error('No page has been added yet');
2315
                break;
2316
            case 1: // all output in the endDoc() method goes direct to the buffer
2317
                $this->put($s);
2318
                break;
2319
            case 2: // page output is stored in internal array
2320
                $this->pages[$this->page] .= $s . "\n";
2321
                break;
2322
            case 3: // document is closed so far
2323
                $this->error('The document is closed');
2324
                break;
2325
        }
2326
    }
2327
2328
    /**
2329
     * Add a command to the document.
2330
     * @param string $s
2331
     */
2332
    protected function put(string $s) : void
2333
    {
2334
        $this->buffer .= $s . "\n";
2335
    }
2336
2337
    /**
2338
     * Get current length of the output buffer.
2339
     * @return int
2340
     */
2341
    protected function getOffset() : int
2342
    {
2343
        return strlen($this->buffer);
2344
    }
2345
2346
    /**
2347
     * Begin a new object.
2348
     * @param int $n
2349
     */
2350
    protected function newObject(?int $n = null) : void
2351
    {
2352
        // Begin a new object
2353
        if ($n === null) {
2354
            $n = ++$this->n;
2355
        }
2356
        $this->offsets[$n] = $this->getOffset();
2357
        $this->put($n . ' 0 obj');
2358
    }
2359
2360
    /**
2361
     * Add data from stream to the document.
2362
     * @param string $data
2363
     */
2364
    protected function putStream(string $data) : void
2365
    {
2366
        $this->put('stream');
2367
        $this->put($data);
2368
        $this->put('endstream');
2369
    }
2370
2371
    /**
2372
     * Add Stream-object to the document.
2373
     * @param string $data
2374
     */
2375
    protected function putStreamObject(string $data) : void
2376
    {
2377
        $entries = '';
2378
        if ($this->compress) {
2379
            $entries = '/Filter /FlateDecode ';
2380
            $data = gzcompress($data);
2381
        }
2382
        $entries .= '/Length ' . strlen($data);
2383
        $this->newObject();
2384
        $this->put('<<' . $entries . '>>');
2385
        $this->putStream($data);
2386
        $this->put('endobj');
2387
    }
2388
2389
    /**
2390
     * Add all pages to the document.
2391
     */
2392
    protected function putPages() : void
2393
    {
2394
        $nb = $this->page;
2395
        for ($n = 1; $n <= $nb; $n++) {
2396
            $this->PageInfo[$n]['n'] = $this->n + 1 + 2 * ($n - 1);
2397
        }
2398
        for ($n = 1; $n <= $nb; $n++) {
2399
            $this->putPage($n);
2400
        }
2401
        // Pages root
2402
        $this->newObject(1);
2403
        $this->put('<</Type /Pages');
2404
        $kids = '/Kids [';
2405
        for ($n = 1; $n <= $nb; $n++) {
2406
            $kids .= $this->PageInfo[$n]['n'] . ' 0 R ';
2407
        }
2408
        $this->put($kids . ']');
2409
        $this->put('/Count ' . $nb);
2410
        if ($this->DefOrientation == 'P') {
2411
            $w = $this->DefPageSize[0];
2412
            $h = $this->DefPageSize[1];
2413
        } else {
2414
            $w = $this->DefPageSize[1];
2415
            $h = $this->DefPageSize[0];
2416
        }
2417
        $this->put(sprintf('/MediaBox [0 0 %.2F %.2F]', $w * $this->k, $h * $this->k));
2418
        $this->put('>>');
2419
        $this->put('endobj');
2420
    }
2421
2422
    /**
2423
     * Add Pageinfos to the document.
2424
     * @param int $n
2425
     */
2426
    protected function putPage(int $n) : void
2427
    {
2428
        $this->newObject();
2429
        $this->put('<</Type /Page');
2430
        $this->put('/Parent 1 0 R');
2431
        if (isset($this->PageInfo[$n]['size'])) {
2432
            $this->put(sprintf('/MediaBox [0 0 %.2F %.2F]', $this->PageInfo[$n]['size'][0], $this->PageInfo[$n]['size'][1]));
2433
        }
2434
        if (isset($this->PageInfo[$n]['rotation'])) {
2435
            $this->put('/Rotate ' . $this->PageInfo[$n]['rotation']);
2436
        }
2437
        $this->put('/Resources 2 0 R');
2438
        if (isset($this->PageLinks[$n])) {
2439
            // Links
2440
            $annots = '/Annots [';
2441
            foreach ($this->PageLinks[$n] as $pl) {
2442
                $rect = sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]);
2443
                $annots .= '<</Type /Annot /Subtype /Link /Rect [' . $rect . '] /Border [0 0 0] ';
2444
                if (is_string($pl[4])) {
2445
                    $annots .= '/A <</S /URI /URI ' . $this->textString($pl[4]) . '>>>>';
2446
                } else {
2447
                    $l = $this->links[$pl[4]];
2448
                    if (isset($this->PageInfo[$l[0]]['size'])) {
2449
                        $h = $this->PageInfo[$l[0]]['size'][1];
2450
                    } else {
2451
                        $h = ($this->DefOrientation == 'P') ? $this->DefPageSize[1] * $this->k : $this->DefPageSize[0] * $this->k;
2452
                    }
2453
                    $annots .= sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>', $this->PageInfo[$l[0]]['n'], $h - $l[1] * $this->k);
2454
                }
2455
            }
2456
            $this->put($annots . ']');
2457
        }
2458
        if ($this->WithAlpha) {
2459
            $this->put('/Group <</Type /Group /S /Transparency /CS /DeviceRGB>>');
2460
        }
2461
        $this->put('/Contents ' . ($this->n + 1) . ' 0 R>>');
2462
        $this->put('endobj');
2463
        // Page content
2464
        if (!empty($this->AliasNbPages)) {
2465
            $this->pages[$n] = str_replace($this->AliasNbPages, strval($this->page), $this->pages[$n]);
2466
        }
2467
        $this->putStreamObject($this->pages[$n]);
2468
    }
2469
2470
    /**
2471
     * Add fonts to the document.
2472
     */
2473
    protected function putFonts() : void
2474
    {
2475
        foreach ($this->FontFiles as $file => $info) {
2476
            // Font file embedding
2477
            $this->newObject();
2478
            $this->FontFiles[$file]['n'] = $this->n;
2479
            $font = file_get_contents($this->fontpath . $file, true);
2480
            if (!$font) {
2481
                $this->error('Font file not found: ' . $file);
2482
            }
2483
            $compressed = (substr($file, -2) == '.z');
2484
            if (!$compressed && isset($info['length2'])) {
2485
                $font = substr($font, 6, $info['length1']) . substr($font, 6 + $info['length1'] + 6, $info['length2']);
2486
            }
2487
            $this->put('<</Length ' . strlen($font));
2488
            if ($compressed) {
2489
                $this->put('/Filter /FlateDecode');
2490
            }
2491
            $this->put('/Length1 ' . $info['length1']);
2492
            if (isset($info['length2'])) {
2493
                $this->put('/Length2 ' . $info['length2'] . ' /Length3 0');
2494
            }
2495
            $this->put('>>');
2496
            $this->putStream($font);
2497
            $this->put('endobj');
2498
        }
2499
        foreach ($this->fonts as $k => $font) {
2500
            // Encoding
2501
            if (isset($font['diff'])) {
2502
                if (!isset($this->encodings[$font['enc']])) {
2503
                    $this->newObject();
2504
                    $this->put('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [' . $font['diff'] . ']>>');
2505
                    $this->put('endobj');
2506
                    $this->encodings[$font['enc']] = $this->n;
2507
                }
2508
            }
2509
            // ToUnicode CMap
2510
            $cmapkey = '';
2511
            if (isset($font['uv'])) {
2512
                if (isset($font['enc'])) {
2513
                    $cmapkey = $font['enc'];
2514
                } else {
2515
                    $cmapkey = $font['name'];
2516
                }
2517
                if (!isset($this->cmaps[$cmapkey])) {
2518
                    $cmap = $this->toUnicodeCMap($font['uv']);
2519
                    $this->putStreamObject($cmap);
2520
                    $this->cmaps[$cmapkey] = $this->n;
2521
                }
2522
            }
2523
            // Font object
2524
            $this->fonts[$k]['n'] = $this->n + 1;
2525
            $type = $font['type'];
2526
            $name = $font['name'];
2527
            if ($font['subsetted']) {
2528
                $name = 'AAAAAA+' . $name;
2529
            }
2530
            if ($type == 'Core') {
2531
                // Core font
2532
                $this->newObject();
2533
                $this->put('<</Type /Font');
2534
                $this->put('/BaseFont /' . $name);
2535
                $this->put('/Subtype /Type1');
2536
                if ($name != 'Symbol' && $name != 'ZapfDingbats') {
2537
                    $this->put('/Encoding /WinAnsiEncoding');
2538
                }
2539
                if (isset($font['uv'])) {
2540
                    $this->put('/ToUnicode ' . $this->cmaps[$cmapkey] . ' 0 R');
2541
                }
2542
                $this->put('>>');
2543
                $this->put('endobj');
2544
            } elseif ($type == 'Type1' || $type == 'TrueType') {
2545
                // Additional Type1 or TrueType/OpenType font
2546
                $this->newObject();
2547
                $this->put('<</Type /Font');
2548
                $this->put('/BaseFont /' . $name);
2549
                $this->put('/Subtype /' . $type);
2550
                $this->put('/FirstChar 32 /LastChar 255');
2551
                $this->put('/Widths ' . ($this->n + 1) . ' 0 R');
2552
                $this->put('/FontDescriptor ' . ($this->n + 2) . ' 0 R');
2553
                if (isset($font['diff'])) {
2554
                    $this->put('/Encoding ' . $this->encodings[$font['enc']] . ' 0 R');
2555
                } else {
2556
                    $this->put('/Encoding /WinAnsiEncoding');
2557
                }
2558
                if (isset($font['uv'])) {
2559
                    $this->put('/ToUnicode ' . $this->cmaps[$cmapkey] . ' 0 R');
2560
                }
2561
                $this->put('>>');
2562
                $this->put('endobj');
2563
                // Widths
2564
                $this->newObject();
2565
                $cw = &$font['cw'];
2566
                $s = '[';
2567
                for ($i = 32; $i <= 255; $i++) {
2568
                    $s .= $cw[chr($i)] . ' ';
2569
                }
2570
                $this->put($s . ']');
2571
                $this->put('endobj');
2572
                // Descriptor
2573
                $this->newObject();
2574
                $s = '<</Type /FontDescriptor /FontName /' . $name;
2575
                foreach ($font['desc'] as $k2 => $v) {
2576
                    $s .= ' /' . $k2 . ' ' . $v;
2577
                }
2578
                if (!empty($font['file'])) {
2579
                    $s .= ' /FontFile' . ($type == 'Type1' ? '' : '2') . ' ' . $this->FontFiles[$font['file']]['n'] . ' 0 R';
2580
                }
2581
                $this->put($s . '>>');
2582
                $this->put('endobj');
2583
            } else {
2584
                // Allow for additional types
2585
                $mtd = '_put' . strtolower($type);
2586
                if (!method_exists($this, $mtd)) {
2587
                    $this->error('Unsupported font type: ' . $type);
2588
                }
2589
                $this->$mtd($font);
2590
            }
2591
        }
2592
    }
2593
2594
    /**
2595
     * @param array $uv
2596
     * @return string
2597
     */
2598
    protected function toUnicodeCMap(array $uv) : string
2599
    {
2600
        $ranges = '';
2601
        $nbr = 0;
2602
        $chars = '';
2603
        $nbc = 0;
2604
        foreach ($uv as $c => $v) {
2605
            if (is_array($v)) {
2606
                $ranges .= sprintf("<%02X> <%02X> <%04X>\n", $c, $c + $v[1] - 1, $v[0]);
2607
                $nbr++;
2608
            } else {
2609
                $chars .= sprintf("<%02X> <%04X>\n", $c, $v);
2610
                $nbc++;
2611
            }
2612
        }
2613
        $s = "/CIDInit /ProcSet findresource begin\n";
2614
        $s .= "12 dict begin\n";
2615
        $s .= "begincmap\n";
2616
        $s .= "/CIDSystemInfo\n";
2617
        $s .= "<</Registry (Adobe)\n";
2618
        $s .= "/Ordering (UCS)\n";
2619
        $s .= "/Supplement 0\n";
2620
        $s .= ">> def\n";
2621
        $s .= "/CMapName /Adobe-Identity-UCS def\n";
2622
        $s .= "/CMapType 2 def\n";
2623
        $s .= "1 begincodespacerange\n";
2624
        $s .= "<00> <FF>\n";
2625
        $s .= "endcodespacerange\n";
2626
        if ($nbr > 0) {
2627
            $s .= "$nbr beginbfrange\n";
2628
            $s .= $ranges;
2629
            $s .= "endbfrange\n";
2630
        }
2631
        if ($nbc > 0) {
2632
            $s .= "$nbc beginbfchar\n";
2633
            $s .= $chars;
2634
            $s .= "endbfchar\n";
2635
        }
2636
        $s .= "endcmap\n";
2637
        $s .= "CMapName currentdict /CMap defineresource pop\n";
2638
        $s .= "end\n";
2639
        $s .= "end";
2640
        return $s;
2641
    }
2642
2643
    /**
2644
     * Add all containing images to the document.
2645
     */
2646
    protected function putImages() : void
2647
    {
2648
        foreach (array_keys($this->images) as $file) {
2649
            $this->putImage($this->images[$file]);
2650
            unset($this->images[$file]['data']);
2651
            unset($this->images[$file]['smask']);
2652
        }
2653
    }
2654
2655
    /**
2656
     * Add image to the document.
2657
     * @param array $info
2658
     */
2659
    protected function putImage(array &$info) : void
2660
    {
2661
        $this->newObject();
2662
        $info['n'] = $this->n;
2663
        $this->put('<</Type /XObject');
2664
        $this->put('/Subtype /Image');
2665
        $this->put('/Width ' . $info['w']);
2666
        $this->put('/Height ' . $info['h']);
2667
        if ($info['cs'] == 'Indexed') {
2668
            $this->put('/ColorSpace [/Indexed /DeviceRGB ' . (strlen($info['pal']) / 3 - 1) . ' ' . ($this->n + 1) . ' 0 R]');
2669
        } else {
2670
            $this->put('/ColorSpace /' . $info['cs']);
2671
            if ($info['cs'] == 'DeviceCMYK') {
2672
                $this->put('/Decode [1 0 1 0 1 0 1 0]');
2673
            }
2674
        }
2675
        $this->put('/BitsPerComponent ' . $info['bpc']);
2676
        if (isset($info['f'])) {
2677
            $this->put('/Filter /' . $info['f']);
2678
        }
2679
        if (isset($info['dp'])) {
2680
            $this->put('/DecodeParms <<' . $info['dp'] . '>>');
2681
        }
2682
        if (isset($info['trns']) && is_array($info['trns'])) {
2683
            $trns = '';
2684
            $cnt = count($info['trns']);
2685
            for ($i = 0; $i < $cnt; $i++) {
2686
                $trns .= $info['trns'][$i] . ' ' . $info['trns'][$i] . ' ';
2687
            }
2688
            $this->put('/Mask [' . $trns . ']');
2689
        }
2690
        if (isset($info['smask'])) {
2691
            $this->put('/SMask ' . ($this->n + 1) . ' 0 R');
2692
        }
2693
        $this->put('/Length ' . strlen($info['data']) . '>>');
2694
        $this->putStream($info['data']);
2695
        $this->put('endobj');
2696
        // Soft mask
2697
        if (isset($info['smask'])) {
2698
            $dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns ' . $info['w'];
2699
            $smask = array(
2700
                'w'=>$info['w'],
2701
                'h'=>$info['h'],
2702
                'cs'=>'DeviceGray',
2703
                'bpc'=>8,
2704
                'f'=>$info['f'],
2705
                'dp'=>$dp,
2706
                'data'=>$info['smask']
2707
            );
2708
            $this->putImage($smask);
2709
        }
2710
        // Palette
2711
        if ($info['cs'] == 'Indexed') {
2712
            $this->putStreamObject($info['pal']);
2713
        }
2714
    }
2715
2716
    /**
2717
     *
2718
     */
2719
    protected function putXObjectDict() : void
2720
    {
2721
        $this->put('/XObject <<');
2722
        foreach ($this->images as $image) {
2723
            $this->put('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R');
2724
        }
2725
        $this->put('>>');
2726
    }
2727
2728
    /**
2729
     * Insert the fonts dictionary.
2730
     */
2731
    protected function putFontsDict() : void
2732
    {
2733
        $this->put('/Font <<');
2734
        foreach ($this->fonts as $font) {
2735
            $this->put('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R');
2736
        }
2737
        $this->put('>>');
2738
    }
2739
2740
    /**
2741
     * Insert the resource dictionary.
2742
     */
2743
    protected function putResourceDict() : void
2744
    {
2745
        $this->newObject(2);
2746
        $this->put('<<');
2747
        $this->put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
2748
        $this->putFontsDict();
2749
        $this->putXObjectDict();
2750
        $this->put('>>');
2751
        $this->put('endobj');
2752
    }
2753
2754
    /**
2755
     * Insert all resources.
2756
     * - fonts
2757
     * - images
2758
     * - resource dictonary
2759
     */
2760
    protected function putResources() : void
2761
    {
2762
        $this->putFonts();
2763
        $this->putImages();
2764
    }
2765
2766
    /**
2767
     *
2768
     */
2769
    protected function putBookmarks() : void
2770
    {
2771
        $nb = count($this->outlines);
2772
        if ($nb == 0) {
2773
            return;
2774
        }
2775
        $lru = array();
2776
        $level = 0;
2777
        foreach ($this->outlines as $i => $o) {
2778
            if ($o['l'] > 0) {
2779
                $parent = $lru[$o['l'] - 1];
2780
                // Set parent and last pointers
2781
                $this->outlines[$i]['parent'] = $parent;
2782
                $this->outlines[$parent]['last'] = $i;
2783
                if ($o['l'] > $level) {
2784
                    // Level increasing: set first pointer
2785
                    $this->outlines[$parent]['first'] = $i;
2786
                }
2787
            } else {
2788
                $this->outlines[$i]['parent'] = $nb;
2789
            }
2790
            if ($o['l'] <= $level && $i > 0) {
2791
                // Set prev and next pointers
2792
                $prev = $lru[$o['l']];
2793
                $this->outlines[$prev]['next'] = $i;
2794
                $this->outlines[$i]['prev'] = $prev;
2795
            }
2796
            $lru[$o['l']] = $i;
2797
            $level = $o['l'];
2798
        }
2799
        // Outline items
2800
        $n = $this->n + 1;
2801
        foreach ($this->outlines as $i => $o) {
2802
            $this->newObject();
2803
            $this->put('<</Title ' . $this->textString($o['t']));
2804
            $this->put('/Parent ' . ($n + $o['parent']) . ' 0 R');
2805
            if (isset($o['prev'])) {
2806
                $this->put('/Prev ' . ($n + $o['prev']) . ' 0 R');
2807
            }
2808
            if (isset($o['next'])) {
2809
                $this->put('/Next ' . ($n + $o['next']) . ' 0 R');
2810
            }
2811
            if (isset($o['first'])) {
2812
                $this->put('/First ' . ($n + $o['first']) . ' 0 R');
2813
            }
2814
            if (isset($o['last'])) {
2815
                $this->put('/Last ' . ($n + $o['last']) . ' 0 R');
2816
            }
2817
            $this->put(sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]', $this->PageInfo[$o['p']]['n'], $o['y']));
2818
            $this->put('/Count 0>>');
2819
            $this->put('endobj');
2820
        }
2821
        // Outline root
2822
        $this->newObject();
2823
        $this->outlineRoot = $this->n;
2824
        $this->put('<</Type /Outlines /First ' . $n . ' 0 R');
2825
        $this->put('/Last ' . ($n + $lru[0]) . ' 0 R>>');
2826
        $this->put('endobj');
2827
    }
2828
2829
    /**
2830
     * create the file info.
2831
     */
2832
    protected function putInfo() : void
2833
    {
2834
        $this->newObject();
2835
        $this->put('<<');
2836
        $this->metadata['Producer'] = 'FPDF ' . FPDF_VERSION;
2837
        $this->metadata['CreationDate'] = 'D:' . @date('YmdHis');
2838
        foreach ($this->metadata as $key => $value) {
2839
            $this->put('/' . $key . ' ' . $this->textString($value));
2840
        }
2841
        $this->put('>>');
2842
        $this->put('endobj');
2843
    }
2844
2845
    /**
2846
     * Create catalog.
2847
     * - zoom mode
2848
     * - page layout
2849
     * - outlines
2850
     */
2851
    protected function putCatalog() : void
2852
    {
2853
        $this->newObject();
2854
        $this->put('<<');
2855
2856
        $n = $this->PageInfo[1]['n'];
2857
        $this->put('/Type /Catalog');
2858
        $this->put('/Pages 1 0 R');
2859
        if ($this->ZoomMode == 'fullpage') {
2860
            $this->put('/OpenAction [' . $n . ' 0 R /Fit]');
2861
        } elseif ($this->ZoomMode == 'fullwidth') {
2862
            $this->put('/OpenAction [' . $n . ' 0 R /FitH null]');
2863
        } elseif ($this->ZoomMode == 'real') {
2864
            $this->put('/OpenAction [' . $n . ' 0 R /XYZ null null 1]');
2865
        } elseif (!is_string($this->ZoomMode)) {
2866
            $this->put('/OpenAction [' . $n . ' 0 R /XYZ null null ' . sprintf('%.2F', $this->ZoomMode / 100) . ']');
2867
        }
2868
        if ($this->LayoutMode == 'single') {
2869
            $this->put('/PageLayout /SinglePage');
2870
        } elseif ($this->LayoutMode == 'continuous') {
2871
            $this->put('/PageLayout /OneColumn');
2872
        } elseif ($this->LayoutMode == 'two') {
2873
            $this->put('/PageLayout /TwoColumnLeft');
2874
        }
2875
        if (count($this->outlines) > 0) {
2876
            $this->put('/Outlines ' . $this->outlineRoot . ' 0 R');
2877
            $this->put('/PageMode /UseOutlines');
2878
        }
2879
2880
        $this->put('>>');
2881
        $this->put('endobj');
2882
    }
2883
2884
    /**
2885
     * Create Header.
2886
     * Header only contains the PDF Version
2887
     */
2888
    protected function putHeader() : void
2889
    {
2890
        $this->put('%PDF-' . $this->PDFVersion);
2891
    }
2892
2893
    /**
2894
     * Create Trailer.
2895
     *
2896
     */
2897
    protected function putTrailer() : void
2898
    {
2899
        $this->put('trailer');
2900
        $this->put('<<');
2901
        $this->put('/Size ' . ($this->n + 1));
2902
        $this->put('/Root ' . $this->n . ' 0 R');
2903
        $this->put('/Info ' . ($this->n - 1) . ' 0 R');
2904
        $this->put('>>');
2905
    }
2906
2907
    /**
2908
     *  End of document.
2909
     *  Generate the whole document into internal buffer: <ul>
2910
     *  <li> header (PDF Version)</li>
2911
     *  <li> pages </li>
2912
     *  <li> ressources (fonts, images) </li>
2913
     *  <li> ressources dictionary </li>
2914
     *  <li> bookmarks </li>
2915
     *  <li> fileinfo </li>
2916
     *  <li> zoommode, layout </li>
2917
     *  <li> page - ref's </li>
2918
     *  <li> trailer </li></ul>
2919
     */
2920
    protected function endDoc() : void
2921
    {
2922
        $this->putHeader();
2923
        $this->putPages();
2924
        $this->putResources();
2925
        $this->putResourceDict();
2926
        $this->putBookmarks();
2927
2928
        // Info
2929
        $this->putInfo();
2930
2931
        // Catalog (zoommode, page layout)
2932
        $this->putCatalog();
2933
2934
        // Cross-ref
2935
        $offset = $this->getOffset();
2936
        $this->put('xref');
2937
        $this->put('0 ' . ($this->n + 1));
2938
        $this->put('0000000000 65535 f ');
2939
        for ($i = 1; $i <= $this->n; $i++) {
2940
            $this->put(sprintf('%010d 00000 n ', $this->offsets[$i]));
2941
        }
2942
        // Trailer
2943
        $this->putTrailer();
2944
2945
        // EOF
2946
        $this->put('startxref');
2947
        $this->put(strval($offset));
2948
        $this->put('%%EOF');
2949
        $this->state = 3;
2950
    }
2951
}
2952