Passed
Push — main ( 83c25f...829487 )
by Stefan
01:56
created

FPDF::image()   F

Complexity

Conditions 20
Paths 3458

Size

Total Lines 69
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 44
nc 3458
nop 7
dl 0
loc 69
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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