FPDF::internalCell()   F
last analyzed

Complexity

Conditions 28
Paths > 20000

Size

Total Lines 84
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
namespace OPlathey\FPDF;
3
4
5
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