Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
36.45% covered (warning)
36.45%
218 / 598
63.83% covered (warning)
63.83%
30 / 47
CRAP
0.00% covered (danger)
0.00%
0 / 1
Parser
36.45% covered (warning)
36.45%
218 / 598
63.83% covered (warning)
63.83%
30 / 47
14163.26
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 parseFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 parseUri
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setDocument
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parseHtml
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 parseHtmlFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseHtmlUri
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseCss
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 parseCssFile
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 parseCssUri
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getDocument
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 document
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPageSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getPageSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPageMargins
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 setPageTopMargin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPageRightMargin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPageBottomMargin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPageLeftMargin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setDefaultStyle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setX
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setY
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPageMargins
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageTopMargin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageRightMargin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageBottomMargin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageLeftMargin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getX
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getY
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultStyles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCss
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 prepare
31.25% covered (danger)
31.25%
5 / 16
0.00% covered (danger)
0.00%
0 / 1
58.79
 process
71.43% covered (success)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 addNodeToDocument
17.68% covered (danger)
17.68%
29 / 164
0.00% covered (danger)
0.00%
0 / 1
1444.47
 addNodeStreamToDocument
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
90
 createDefaultStyles
98.11% covered (success)
98.11%
52 / 53
0.00% covered (danger)
0.00%
0 / 1
9
 prepareNodeStyles
27.40% covered (danger)
27.40%
57 / 208
0.00% covered (danger)
0.00%
0 / 1
3119.55
 getCurrentX
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 resetX
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCurrentY
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
5.93
 resetY
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 newPage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 goToNextLine
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStringLines
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Pdf\Build\Html;
15
16use Pop\Css;
17use Pop\Color\Color;
18use Pop\Dom\Child;
19use Pop\Pdf\Document;
20
21/**
22 * Pdf HTML parser class
23 *
24 * @category   Pop
25 * @package    Pop\Pdf
26 * @author     Nick Sagona, III <dev@nolainteractive.com>
27 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
28 * @license    http://www.popphp.org/license     New BSD License
29 * @version    5.0.0
30 */
31class Parser
32{
33
34    /**
35     * PDF document
36     * @var ?Document
37     */
38    protected ?Document $document = null;
39
40    /**
41     * HTML object or array of HTML objects
42     * @var Child|array|null
43     */
44    protected Child|array|null $html = null;
45
46    /**
47     * CSS object
48     * @var ?Css\Css
49     */
50    protected ?Css\Css $css = null;
51
52    /**
53     * Page size
54     * @var string
55     */
56    protected string $pageSize = 'LETTER';
57
58    /**
59     * Page margins
60     * @var array
61     */
62    protected array $pageMargins = [
63        'top'    => 80,
64        'right'  => 60,
65        'bottom' => 60,
66        'left'   => 60
67    ];
68
69    /**
70     * Default styles
71     * @var array
72     */
73    protected array $defaultStyles = [
74        'font-family' => 'Arial',
75        'font-size'   => 10,
76        'font-weight' => 'normal',
77        'color'       => [0, 0, 0],
78        'line-height' => 14
79    ];
80
81    /**
82     * Current x-position
83     * @var int
84     */
85    protected int $x = 0;
86
87    /**
88     * Current y-position
89     * @var int
90     */
91    protected int $y = 0;
92
93    /**
94     * Current page object
95     * @var ?Document\Page
96     */
97    protected ?Document\Page $page = null;
98
99    /**
100     * HTML file directory
101     * @var ?string
102     */
103    protected ?string $fileDir = null;
104
105    /**
106     * Text wrap object
107     * @var ?Document\Page\Text\Wrap
108     */
109    protected ?Document\Page\Text\Wrap $textWrap = null;
110
111    /**
112     * Y-override
113     * @var ?int
114     */
115    protected ?int $yOverride = null;
116
117    /**
118     * Constructor
119     *
120     * Instantiate the HTML parser object
121     *
122     * @param ?Document $document
123     */
124    public function __construct(?Document $document = null)
125    {
126        if ($document !== null) {
127            $this->setDocument($document);
128        }
129        $this->createDefaultStyles();
130    }
131
132    /**
133     * Parse HTML string
134     *
135     * @param  string    $htmlString
136     * @param  ?Document $document
137     * @return Parser
138     */
139    public static function parseString(string $htmlString, ?Document $document = null): Parser
140    {
141        $html = new self($document);
142        $html->parseHtml($htmlString);
143
144        return $html;
145    }
146
147    /**
148     * Parse $html from file
149     *
150     * @param  string    $htmlFile
151     * @param  ?Document $document
152     * @throws Exception
153     * @return Parser
154     */
155    public static function parseFile(string $htmlFile, ?Document $document = null): Parser
156    {
157        $html = new self($document);
158        $html->parseHtmlFile($htmlFile);
159
160        return $html;
161    }
162
163    /**
164     * Parse $html from URI
165     *
166     * @param string $htmlUri
167     * @param  ?Document $document
168     * @return Parser
169     */
170    public static function parseUri(string $htmlUri, ?Document $document = null): Parser
171    {
172        $html = new self($document);
173        $html->parseHtmlUri($htmlUri);
174
175        return $html;
176    }
177
178    /**
179     * Set document
180     *
181     * @param  Document $document
182     * @return Parser
183     */
184    public function setDocument(Document $document): Parser
185    {
186        $this->document = $document;
187        return $this;
188    }
189
190    /**
191     * Parse HTML string
192     *
193     * @param  string  $htmlString
194     * @param  ?string $basePath
195     * @return Parser
196     */
197    public function parseHtml(string $htmlString, ?string $basePath = null): Parser
198    {
199        if ($basePath !== null) {
200            $this->fileDir = $basePath;
201        }
202        $this->html = Child::parseString($htmlString);
203        return $this;
204    }
205
206    /**
207     * Parse HTML string from file
208     *
209     * @param  string $htmlFile
210     * @throws Exception
211     * @return Parser
212     */
213    public function parseHtmlFile(string $htmlFile): Parser
214    {
215        if (!file_exists($htmlFile)) {
216            throw new Exception('Error: That file does not exist.');
217        }
218        return $this->parseHtml(file_get_contents($htmlFile), dirname(realpath($htmlFile)));
219    }
220
221    /**
222     * Parse HTML string from URI
223     *
224     * @param  string $htmlUri
225     * @return Parser
226     */
227    public function parseHtmlUri(string $htmlUri): Parser
228    {
229        return $this->parseHtml(file_get_contents($htmlUri));
230    }
231
232    /**
233     * Parse CSS string
234     *
235     * @param  string $cssString
236     * @return Parser
237     */
238    public function parseCss(string $cssString): Parser
239    {
240        if ($this->css === null) {
241            $this->css = Css\Css::parseString($cssString);
242        } else {
243            $this->css->parseCss($cssString);
244        }
245        return $this;
246    }
247
248    /**
249     * Parse CSS file
250     *
251     * @param  string $cssFile
252     * @throws \Pop\Css\Exception
253     * @return Parser
254     */
255    public function parseCssFile(string $cssFile): Parser
256    {
257        if ($this->css === null) {
258            $this->css = Css\Css::parseFile($cssFile);
259        } else {
260            $this->css->parseCssFile($cssFile);
261        }
262        return $this;
263    }
264
265    /**
266     * Parse CSS URI
267     *
268     * @param  string $cssUri
269     * @return Parser
270     */
271    public function parseCssUri(string $cssUri): Parser
272    {
273        if ($this->css === null) {
274            $this->css = Css\Css::parseUri($cssUri);
275        } else {
276            $this->css->parseCssUri($cssUri);
277        }
278        return $this;
279    }
280
281    /**
282     * Get document
283     *
284     * @return ?Document
285     */
286    public function getDocument(): ?Document
287    {
288        return $this->document;
289    }
290
291    /**
292     * Get document (alias)
293     *
294     * @return ?Document
295     */
296    public function document(): ?Document
297    {
298        return $this->document;
299    }
300
301    /**
302     * Set page size
303     *
304     * @param  mixed $size
305     * @param  mixed $height
306     * @return Parser
307     */
308    public function setPageSize(mixed $size, mixed $height = null): Parser
309    {
310        $this->pageSize = ($height !== null) ? ['width' => $size, 'height' => $height] : $size;
311
312        return $this;
313    }
314
315    /**
316     * Get page size
317     *
318     * @return string|array
319     */
320    public function getPageSize(): string|array
321    {
322        return $this->pageSize;
323    }
324
325    /**
326     * Set page margins
327     *
328     * @param  int $top
329     * @param  int $right
330     * @param  int $bottom
331     * @param  int $left
332     * @return Parser
333     */
334    public function setPageMargins(int $top, int $right, int $bottom, int $left): Parser
335    {
336        $this->pageMargins['top']    = $top;
337        $this->pageMargins['right']  = $right;
338        $this->pageMargins['bottom'] = $bottom;
339        $this->pageMargins['left']   = $left;
340        return $this;
341    }
342
343    /**
344     * Set page top margin
345     *
346     * @param  int $margin
347     * @return Parser
348     */
349    public function setPageTopMargin(int $margin): Parser
350    {
351        $this->pageMargins['top'] = $margin;
352        return $this;
353    }
354
355    /**
356     * Set page right margin
357     *
358     * @param  int $margin
359     * @return Parser
360     */
361    public function setPageRightMargin(int $margin): Parser
362    {
363        $this->pageMargins['right'] = $margin;
364        return $this;
365    }
366
367    /**
368     * Set page bottom margin
369     *
370     * @param  int $margin
371     * @return Parser
372     */
373    public function setPageBottomMargin(int $margin): Parser
374    {
375        $this->pageMargins['bottom'] = $margin;
376        return $this;
377    }
378
379    /**
380     * Set page left margin
381     *
382     * @param  int $margin
383     * @return Parser
384     */
385    public function setPageLeftMargin(int $margin): Parser
386    {
387        $this->pageMargins['left'] = $margin;
388        return $this;
389    }
390
391    /**
392     * Set a default style
393     *
394     * @param  string $property
395     * @param  string $value
396     * @return Parser
397     */
398    public function setDefaultStyle(string $property, string $value): Parser
399    {
400        $this->defaultStyles[$property] = $value;
401        return $this;
402    }
403
404    /**
405     * Set x-position
406     *
407     * @param  int $x
408     * @return Parser
409     */
410    public function setX(int $x): Parser
411    {
412        $this->x = $x;
413        return $this;
414    }
415
416    /**
417     * Set y-position
418     *
419     * @param  int $y
420     * @return Parser
421     */
422    public function setY(int $y): Parser
423    {
424        $this->y = $y;
425        return $this;
426    }
427
428    /**
429     * Get page margins
430     *
431     * @return array
432     */
433    public function getPageMargins(): array
434    {
435        return $this->pageMargins;
436    }
437
438    /**
439     * Get page top margin
440     *
441     * @return int
442     */
443    public function getPageTopMargin(): int
444    {
445        return $this->pageMargins['top'];
446    }
447
448    /**
449     * Get page right margin
450     *
451     * @return int
452     */
453    public function getPageRightMargin(): int
454    {
455        return $this->pageMargins['right'];
456    }
457
458    /**
459     * Get page bottom margin
460     *
461     * @return int
462     */
463    public function getPageBottomMargin(): int
464    {
465        return $this->pageMargins['bottom'];
466    }
467
468    /**
469     * Get page left margin
470     *
471     * @return int
472     */
473    public function getPageLeftMargin(): int
474    {
475        return $this->pageMargins['left'];
476    }
477
478    /**
479     * Get x-position
480     *
481     * @return int
482     */
483    public function getX(): int
484    {
485        return $this->x;
486    }
487
488    /**
489     * Get y-position
490     *
491     * @return int
492     */
493    public function getY(): int
494    {
495        return $this->y;
496    }
497
498    /**
499     * Get a default style
500     *
501     * @param  string $property
502     * @return ?string
503     */
504    public function getDefaultStyle(string $property): ?string
505    {
506        return $this->defaultStyles[$property] ?? null;
507    }
508
509    /**
510     * Get default styles
511     *
512     * @return array
513     */
514    public function getDefaultStyles(): array
515    {
516        return $this->defaultStyles;
517    }
518
519    /**
520     * Get styles
521     *
522     * @return ?Css\Css
523     */
524    public function getCss(): ?Css\Css
525    {
526        return $this->css;
527    }
528
529    /**
530     * Get HTML nodes
531     *
532     * @return array|Child|null
533     */
534    public function getHtml(): array|Child|null
535    {
536        return $this->html;
537    }
538
539    /**
540     * Prepare for conversion of HTML into PDF objects
541     *
542     * @return array|Child|null
543     */
544    public function prepare(): array|Child|null
545    {
546        $htmlNodes = null;
547        if ($this->html instanceof Child) {
548            foreach ($this->html->getChildNodes() as $child) {
549                if ($child->getNodeName() == 'head') {
550                    foreach ($child->getChildNodes() as $c) {
551                        if (($c->getNodeName() == 'link') && ($c->hasAttribute('href')) &&
552                            ($c->hasAttribute('type')) && ($c->getAttribute('type') == 'text/css')) {
553                            $href = $c->getAttribute('href');
554                            if ($this->css === null) {
555                                $this->css = Css\Css::parseFile($this->fileDir . '/' . $href);
556                            } else {
557                                $this->css->parseCssFile($this->fileDir . '/' . $href);
558                            }
559                        }
560                    }
561                } else if ($child->getNodeName() == 'body') {
562                    $htmlNodes = $child;
563                }
564            }
565        }
566
567        if ($htmlNodes === null) {
568            $htmlNodes = $this->html;
569        }
570
571        return $htmlNodes;
572    }
573
574    /**
575     * Process conversion of HTML into PDF objects
576     *
577     * @throws Exception
578     * @return ?Document
579     */
580    public function process(): ?Document
581    {
582        $htmlNodes = $this->prepare();
583
584        if ($htmlNodes instanceof Child) {
585            foreach ($htmlNodes->getChildNodes() as $child) {
586                $this->addNodeToDocument($child);
587            }
588        } else {
589            foreach ($htmlNodes as $child) {
590                $this->addNodeToDocument($child);
591            }
592        }
593
594        return $this->document;
595    }
596
597    /**
598     * Add node to document
599     *
600     * @param Child $child
601     * @throws Exception|\Pop\Pdf\Exception
602     * @return void
603     */
604    protected function addNodeToDocument(Child $child): void
605    {
606        $styles   = $this->prepareNodeStyles($child->getNodeName(), $child->getAttributes());
607        $currentX = $this->getCurrentX();
608
609        if ($this->yOverride !== null) {
610            $currentY        = $this->yOverride;
611            $this->yOverride = null;
612        } else {
613            $currentY = $this->getCurrentY();
614        }
615
616        $wrapLength = ($this->x > $this->pageMargins['left']) ?
617            $this->page->getWidth() - $this->pageMargins['right'] - $this->x :
618            $this->page->getWidth() - $this->pageMargins['right'] - $this->pageMargins['left'];
619
620        // Image node
621        if ($child->getNodeName() == 'img') {
622            $image = Document\Page\Image::createImageFromFile($this->fileDir . '/' . $child->getAttribute('src'));
623            $width = null;
624            $height = null;
625            $align = null;
626
627            if ($child->hasAttribute('width')) {
628                $width = (strpos($child->getAttribute('width'), '%')) ?
629                    $this->page->getWidth() * ((int)$child->getAttribute('width') / 100) : (int)$child->getAttribute('width');
630            } else if ($child->hasAttribute('height')) {
631                $height = (strpos($child->getAttribute('height'), '%')) ?
632                    $this->page->getHeight() * ((int)$child->getAttribute('height') / 100) : (int)$child->getAttribute('height');
633            } else if ($styles['width'] !== null) {
634                $width = (strpos($styles['width'], '%')) ?
635                    $this->page->getWidth() * ((int)$styles['width'] / 100) : (int)$styles['width'];
636            } else if ($styles['height'] !== null) {
637                $height = (strpos($styles['height'], '%')) ?
638                    $this->page->getHeight() * ((int)$styles['height'] / 100) : (int)$styles['height'];
639            }
640
641            if ($width !== null) {
642                $image->resizeToWidth($width);
643            } else if ($height !== null) {
644                $image->resizeToHeight($height);
645            }
646
647            if ($height === null) {
648                $height = ($image->getResizedHeight() !== null) ? $image->getResizedHeight() : $image->getHeight();
649            }
650
651            if ($child->hasAttribute('align')) {
652                $align = strtoupper($child->getAttribute('align'));
653            } else if (isset($styles['float'])) {
654                $align = strtoupper($styles['float']);
655            }
656
657            if ($align == 'LEFT') {
658                $box = [
659                    'left' => $currentX,
660                    'right' => $currentX + $width + ($styles['marginRight'] ?? 0),
661                    'top' => $currentY,
662                    'bottom' => $currentY - $height - ($styles['marginBottom'] ?? 0)
663                ];
664                $this->textWrap = new Document\Page\Text\Wrap('RIGHT', $this->pageMargins['left'], $this->page->getWidth() - $this->pageMargins['right'], $box);
665            } else if ($align == 'RIGHT') {
666                $box = [
667                    'left' => $this->page->getWidth() - $this->pageMargins['right'] - $width - ($styles['marginLeft'] ?? 0),
668                    'right' => $this->page->getWidth() - $this->pageMargins['right'],
669                    'top' => $currentY,
670                    'bottom' => $currentY - $height - ($styles['marginBottom'] ?? 0)
671                ];
672                $this->textWrap = new Document\Page\Text\Wrap('LEFT', $this->pageMargins['left'], $this->page->getWidth() - $this->pageMargins['right'], $box);
673            }
674
675            if ($this->textWrap !== null) {
676                $newY = $currentY - (($image->getResizedHeight() !== null) ? $image->getResizedHeight() : $image->getHeight());
677                if ($align == 'RIGHT') {
678                    $this->page->addImage($image, ($this->page->getWidth() - $this->pageMargins['right'] - $width), $newY);
679                } else {
680                    $this->page->addImage($image, $currentX, $newY);
681                }
682                $currentY -= $styles['lineHeight'];
683                $this->y += $styles['lineHeight'];
684            } else {
685                $currentY -= ($image->getResizedHeight() !== null) ? $image->getResizedHeight() : $image->getHeight();
686                $this->y += ($image->getResizedHeight() !== null) ? $image->getResizedHeight() : $image->getHeight();
687                $this->page->addImage($image, $currentX, $currentY);
688                $currentY -= $styles['lineHeight'];
689                $this->y += $styles['lineHeight'];
690            }
691        // Table node
692        } else if ($child->getNodeName() == 'table') {
693            $tableWidth  = $this->page->getWidth() - $this->pageMargins['left'] - $this->pageMargins['right'];
694            $columnCount = count($child->getChild(0)->getChildNodes());;
695            $rowCount    = 0;
696            $columnWidth = floor($tableWidth / $columnCount);
697            $fontObject  = $this->document->getFont($styles['currentFont']);
698            $currentRow  = 0;
699            $currentX    = $this->pageMargins['left'] + 10;
700            $thHeight    = 0;
701            $offset      = 0;
702            $startY      = $currentY;
703
704            foreach ($child->getChildNodes() as $childNode) {
705                if (($childNode->getNodeName() == 'tr') && ($childNode->hasChildNodes())) {
706                    foreach ($childNode->getChildNodes() as $grandChild) {
707                        if ($grandChild->getNodeName() == 'th') {
708                            $thString = $grandChild->getNodeValue();
709
710                            $thStringWidth = $fontObject->getStringWidth($thString, $styles['fontSize']);
711                            if ($thStringWidth > ($columnWidth - 20)) {
712                                $strings = $this->getStringLines($thString, $styles['fontSize'], $columnWidth - 20, $fontObject);
713                                foreach ($strings as $i => $string) {
714                                    $text = new Document\Page\Text($string, $styles['fontSize']);
715                                    $this->page->addText($text, $styles['currentFont'], $currentX, ($currentY - ($styles['fontSize'] * $i)));
716                                }
717                                $newThHeight = (count($strings) * $styles['fontSize']) + 20;
718                                if ($newThHeight > $thHeight) {
719                                    $thHeight = $newThHeight;
720                                    $offset   = $thHeight - 30;
721                                }
722                            } else {
723                                $text = new Document\Page\Text($thString, $styles['fontSize']);
724                                $this->page->addText($text, $styles['currentFont'], $currentX, $currentY);
725                            }
726                            $currentX += $columnWidth;
727                        }
728                    }
729                }
730            }
731
732            foreach ($child->getChildNodes() as $childNode) {
733                if (($childNode->getNodeName() == 'tr') && ($childNode->hasChildNodes())) {
734                    $rowCount++;
735                    $currentX  = $this->pageMargins['left'] + 10;
736                    foreach ($childNode->getChildNodes() as $grandChild) {
737                        if ($grandChild->getNodeName() == 'td') {
738                            $text = new Document\Page\Text($grandChild->getNodeValue(), $styles['fontSize']);
739                            $this->page->addText($text, $styles['currentFont'], $currentX, $currentY - $offset);
740                            $currentX += $columnWidth;
741                        }
742                    }
743                    $currentY -= 25;
744                }
745            }
746
747            $currentY += 15;
748            $x1 = $this->pageMargins['left'];
749/**
750            $finalHeight = (25 * $rowCount);
751            $path    = new Document\Page\Path();
752            $path->drawRectangle($x1, $currentY, $tableWidth, $finalHeight);
753            $this->page->addPath($path);
754
755            for ($i = 1; $i < $columnCount; $i++) {
756                $path = new Document\Page\Path();
757                $path->drawLine($x1 + ($i * $columnWidth), $currentY, $x1 + ($i * $columnWidth), $this->page->getHeight() - $finalHeight + 35);
758                $this->page->addPath($path);
759            }
760*/
761
762            for ($i = 0; $i < $rowCount; $i++) {
763                if (($i == 0) && ($thHeight > 0)) {
764                    $lineY = $startY - $thHeight + 20;
765                } else {
766                    $lineY = ($startY - ($i * 25) - 10) - $offset;
767                }
768                $path  = new Document\Page\Path();
769                $path->drawLine($x1, $lineY, $tableWidth + $this->pageMargins['left'], $lineY);
770                $this->page->addPath($path);
771/**
772                if (($i == 1) && ($thHeight > 0)) {
773                    $lineY = ($currentY + ($i * 25));
774                } else {
775                    $lineY = ($currentY + ($i * 25));
776                }
777                $path  = new Document\Page\Path();
778                $path->drawLine($x1, $lineY, $tableWidth + $this->pageMargins['left'], $lineY);
779                $this->page->addPath($path);
780*/
781            }
782
783
784        // Text node
785        } else {
786            if ($this->textWrap !== null) {
787                $box = $this->textWrap->getBox();
788                if ($this->textWrap->isRight()) {
789                    $startX = $box['right'];
790                    $startY = $box['top'] - $styles['fontSize'];
791                    $edgeX  = $wrapLength;
792                    $edgeY  = $box['bottom'];
793                } else {
794                    $startX = $currentX;
795                    $startY = $box['top'] - $styles['fontSize'];
796                    $edgeX  = $box['left'] - 40;
797                    $edgeY  = $box['bottom'];
798                }
799            } else {
800                $startX = $currentX;
801                $startY = $currentY;
802                $edgeX  = $wrapLength;
803                $edgeY  = $this->pageMargins['bottom'];
804            }
805
806            $textStream = new Document\Page\Text\Stream($startX, $startY, $edgeX, $edgeY);
807            $textStream->setCurrentStyle(
808                $styles['currentFont'],
809                $styles['fontSize'],
810                new Color\Rgb($styles['color'][0], $styles['color'][1], $styles['color'][2])
811            );
812            $streamY = $styles['lineHeight'] ?? null;
813            $textStream->addText($child->getNodeValue(), $streamY);
814
815            if ($child->hasChildNodes()) {
816                foreach ($child->getChildNodes() as $grandChild) {
817                    $gcStyles = $this->prepareNodeStyles($grandChild->getNodeName(), $grandChild->getAttributes(), $styles);
818                    $textStream->setCurrentStyle(
819                        $gcStyles['currentFont'],
820                        $gcStyles['fontSize'],
821                        new Color\Rgb($gcStyles['color'][0], $gcStyles['color'][1], $gcStyles['color'][2])
822                    );
823                    $streamY = $gcStyles['lineHeight'] ?? null;
824                    $textStream->addText($grandChild->getNodeValue(), $streamY);
825                }
826            }
827
828            $this->page->addTextStream($textStream);
829
830            $orphanStream = clone $textStream;
831            $hasOrphans   = false;
832
833            while ($orphanStream->hasOrphans($this->document->getFonts())) {
834                $orphanStream = $orphanStream->getOrphanStream();
835                if ($orphanStream->getCurrentY() <= $this->pageMargins['bottom']) {
836                    $currentY = $this->newPage();
837                    $orphanStream->setCurrentY($currentY);
838                } else {
839                    $orphanStream->setStartX($this->pageMargins['left']);
840                    $orphanStream->setEdgeX($wrapLength);
841                    $orphanStream->setEdgeY($this->pageMargins['bottom']);
842                }
843
844                $orphanStream->setCurrentX($currentX);
845                $this->page->addTextStream($orphanStream);
846
847                $orphanStream = clone $orphanStream;
848                $hasOrphans = true;
849            }
850
851            if ($hasOrphans) {
852                $this->yOverride = $orphanStream->getCurrentY();
853            } else {
854                $this->yOverride = null;
855                $this->y  += (!empty($styles['marginBottom'])) ? $styles['marginBottom'] : 25;
856            }
857        }
858    }
859
860    /**
861     * Add node stream to document
862     *
863     * @param  Child $child
864     * @throws Exception
865     * @return void
866     */
867    protected function addNodeStreamToDocument(Child $child): void
868    {
869        $styles     = $this->prepareNodeStyles($child->getNodeName(), $child->getAttributes());
870        $currentX   = $this->getCurrentX();
871        $currentY   = $this->getCurrentY();
872        $fontObject = $this->document->getFont($styles['currentFont']);
873        $wrapLength = ($this->x > $this->pageMargins['left']) ?
874            $this->page->getWidth() - $this->pageMargins['right'] - $this->x :
875            $this->page->getWidth() - $this->pageMargins['right'] - $this->pageMargins['left'];
876
877        $string      = $child->getNodeValue();
878        $stringWidth = $fontObject->getStringWidth($string, $styles['fontSize']);
879
880        if ($stringWidth > $wrapLength) {
881            $strings = $this->getStringLines($string, $styles['fontSize'], $wrapLength, $fontObject);
882            if ($this->x > $this->pageMargins['left']) {
883                $text = new Document\Page\Text($strings[0], $styles['fontSize']);
884                $text->setFillColor(new Color\Rgb($styles['color'][0], $styles['color'][1], $styles['color'][2]));
885                $this->page->addText($text, $styles['currentFont'], $currentX, $currentY);
886                if ($currentY <= $this->pageMargins['bottom']) {
887                    $currentY = $this->newPage();
888                } else {
889                    $currentY -= $styles['lineHeight'];
890                    $this->y  += $styles['lineHeight'];
891                }
892                $currentX = $this->resetX();
893                $wrapLength = $this->page->getWidth() - $this->pageMargins['right'] - $this->pageMargins['left'];
894                unset($strings[0]);
895                $strings = $this->getStringLines(implode(' ', $strings), $styles['fontSize'], $wrapLength, $fontObject);
896            }
897
898            foreach ($strings as $i => $string) {
899                $text = new Document\Page\Text($string, $styles['fontSize']);
900                $text->setFillColor(new Color\Rgb($styles['color'][0], $styles['color'][1], $styles['color'][2]));
901                $this->page->addText($text, $styles['currentFont'], $currentX, $currentY);
902                if ($i < (count($strings) - 1)) {
903                    if ($currentY <= $this->pageMargins['bottom']) {
904                        $currentY = $this->newPage();
905                    } else {
906                        $currentY -= $styles['lineHeight'];
907                        $this->y  += $styles['lineHeight'];
908                    }
909                }
910            }
911            $this->x += $fontObject->getStringWidth($string, $styles['fontSize']);
912        } else {
913            $text = new Document\Page\Text($string, $styles['fontSize']);
914            $text->setFillColor(new Color\Rgb($styles['color'][0], $styles['color'][1], $styles['color'][2]));
915            $this->page->addText($text, $styles['currentFont'], $currentX, $currentY);
916            $this->x += $fontObject->getStringWidth($string, $styles['fontSize']);
917        }
918
919        foreach ($child->getChildNodes() as $grandChild) {
920            $this->addNodeStreamToDocument($grandChild);
921        }
922    }
923
924    /**
925     * Create default styles
926     *
927     * @return void
928     */
929    protected function createDefaultStyles(): void
930    {
931        $h1 = new Css\Selector('h1');
932        $h1['margin-bottom'] = '18px';
933        $h1['font-size']     = '32px';
934        $h1['font-weight']   = 'bold';
935
936        $h2 = new Css\Selector('h2');
937        $h2['margin-bottom'] = '18px';
938        $h2['font-size']     = '28px';
939        $h2['font-weight']   = 'bold';
940
941        $h3 = new Css\Selector('h3');
942        $h3['margin-bottom'] = '16px';
943        $h3['font-size']     = '24px';
944        $h3['font-weight']   = 'bold';
945
946        $h4 = new Css\Selector('h4');
947        $h4['margin-bottom'] = '14px';
948        $h4['font-size']     = '20px';
949        $h4['font-weight']   = 'bold';
950
951        $h5 = new Css\Selector('h5');
952        $h5['margin-bottom'] = '12px';
953        $h5['font-size']     = '16px';
954        $h5['font-weight']   = 'bold';
955
956        $h6 = new Css\Selector('h6');
957        $h6['margin-bottom'] = '10px';
958        $h6['font-size']     = '14px';
959        $h6['font-weight']   = 'bold';
960
961        $p = new Css\Selector('p');
962        $p['margin-bottom'] = '24px';
963        $p['font-size']     = '12px';
964
965        $a = new Css\Selector('a');
966        $a['color'] = new Color\Rgb(0, 0, 255);
967
968        $strong = new Css\Selector('strong');
969        $strong['font-size']   = '10px';
970        $strong['font-weight'] = 'bold';
971
972        $em = new Css\Selector('em');
973        $em['font-size']  = '10px';
974        $em['font-style'] = 'italic';
975
976        $this->css = new Css\Css();
977        $this->css->addSelectors([$h1, $h2, $h3, $h4, $h5, $h6, $p, $a, $strong, $em]);
978
979        if (!($this->document->hasFont('Arial'))) {
980            $this->document->addFont(new Document\Font('Arial'));
981        }
982        if (!($this->document->hasFont('Arial,Bold'))) {
983            $this->document->addFont(new Document\Font('Arial,Bold'));
984        }
985        if (!($this->document->hasFont('Arial,Italic'))) {
986            $this->document->addFont(new Document\Font('Arial,Italic'));
987        }
988        if (!($this->document->hasFont('Arial,BoldItalic'))) {
989            $this->document->addFont(new Document\Font('Arial,BoldItalic'));
990        }
991        if (!($this->document->hasFont('TimesNewRoman'))) {
992            $this->document->addFont(new Document\Font('TimesNewRoman'));
993        }
994        if (!($this->document->hasFont('TimesNewRoman,Bold'))) {
995            $this->document->addFont(new Document\Font('TimesNewRoman,Bold'));
996        }
997        if (!($this->document->hasFont('TimesNewRoman,Italic'))) {
998            $this->document->addFont(new Document\Font('TimesNewRoman,Italic'));
999        }
1000        if (!($this->document->hasFont('TimesNewRoman,BoldItalic'))) {
1001            $this->document->addFont(new Document\Font('TimesNewRoman,BoldItalic'));
1002        }
1003    }
1004
1005    /**
1006     * Prepare node styles
1007     *
1008     * @param  string $name
1009     * @param  array  $attribs
1010     * @param  array  $currentStyles
1011     * @throws Exception
1012     * @return array
1013     */
1014    protected function prepareNodeStyles(string $name, array $attribs = [], array $currentStyles = []): array
1015    {
1016        $styles = [
1017            'currentFont'   => null,
1018            'fontFamily'    => $currentStyles['fontFamily'] ?? $this->defaultStyles['font-family'],
1019            'fontSize'      => $currentStyles['fontSize'] ?? $this->defaultStyles['font-size'],
1020            'fontWeight'    => $currentStyles['fontWeight'] ?? $this->defaultStyles['font-weight'],
1021            'float'         => null,
1022            'width'         => null,
1023            'height'        => null,
1024            'color'         => $currentStyles['color'] ?? $this->defaultStyles['color'],
1025            'lineHeight'    => $currentStyles['lineHeight'] ?? $this->defaultStyles['line-height'],
1026            'marginTop'     => 0,
1027            'paddingTop'    => 0,
1028            'marginRight'   => 0,
1029            'paddingRight'  => 0,
1030            'marginBottom'  => 0,
1031            'paddingBottom' => 0,
1032            'marginLeft'    => 0,
1033            'paddingLeft'   => 0
1034        ];
1035
1036        if (in_array($name, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])) {
1037            switch ($name) {
1038                case 'h1':
1039                    $styles['fontSize']   = round($styles['fontSize'] * 2.67); // 32
1040                    $styles['fontWeight'] = 'bold';
1041                    break;
1042                case 'h2':
1043                    $styles['fontSize']   = round($styles['fontSize'] * 2.33);  // 28
1044                    $styles['fontWeight'] = 'bold';
1045                    break;
1046                case 'h3':
1047                    $styles['fontSize']   = $styles['fontSize'] * 2;  // 24
1048                    $styles['fontWeight'] = 'bold';
1049                    break;
1050                case 'h4':
1051                    $styles['fontSize']   = round($styles['fontSize'] * 1.67); // 20
1052                    $styles['fontWeight'] = 'bold';
1053                    break;
1054                case 'h5':
1055                    $styles['fontSize']   = round($styles['fontSize'] * 1.33);  // 16
1056                    $styles['fontWeight'] = 'bold';
1057                    break;
1058                case 'h6':
1059                    $styles['fontWeight'] = 'bold';
1060                    break;
1061            }
1062        }
1063
1064        if ($this->css->hasSelector($name)) {
1065            if ($this->css[$name]->hasProperty('font-family')) {
1066                $styles['fontFamily'] = str_replace('"', '', $this->css[$name]['font-family']);
1067            }
1068            if ($this->css[$name]->hasProperty('font-size')) {
1069                //$styles['fontSize'] = (int)$this->css[$name]['font-size'];
1070            }
1071            if ($this->css[$name]->hasProperty('font-weight')) {
1072                $styles['fontWeight'] = $this->css[$name]['font-weight'];
1073            }
1074            if ($this->css[$name]->hasProperty('color')) {
1075                $styles['color'] = $this->css[$name]['color'];
1076                if (is_string($styles['color'])) {
1077                    $cssColor = Color::parse($styles['color']);
1078                    $styles['color'] = $cssColor->toRgb()->toArray(false);
1079                }
1080            }
1081            if ($this->css[$name]->hasProperty('float')) {
1082                $styles['float'] = $this->css[$name]['float'];
1083            }
1084            if ($this->css[$name]->hasProperty('width')) {
1085                $styles['width'] = $this->css[$name]['width'];
1086            }
1087            if ($this->css[$name]->hasProperty('height')) {
1088                $styles['height'] = $this->css[$name]['height'];
1089            }
1090            if ($this->css[$name]->hasProperty('line-height')) {
1091                $styles['lineHeight'] = (int)$this->css[$name]['line-height'];
1092            }
1093            if ((int)$this->css[$name]['margin-top'] > 0) {
1094                $styles['marginTop'] = (int)$this->css[$name]['margin-top'];
1095            }
1096            if ((int)$this->css[$name]['padding-top'] > 0) {
1097                $styles['paddingTop'] = (int)$this->css[$name]['padding-top'];
1098            }
1099            if ((int)$this->css[$name]['margin-right'] > 0) {
1100                $styles['marginRight'] = (int)$this->css[$name]['margin-right'];
1101            }
1102            if ((int)$this->css[$name]['padding-right'] > 0) {
1103                $styles['paddingRight'] = (int)$this->css[$name]['padding-right'];
1104            }
1105            if ((int)$this->css[$name]['margin-bottom'] > 0) {
1106                $styles['marginBottom'] = (int)$this->css[$name]['margin-bottom'];
1107            }
1108            if ((int)$this->css[$name]['padding-bottom'] > 0) {
1109                $styles['paddingBottom'] = (int)$this->css[$name]['padding-bottom'];
1110            }
1111            if ((int)$this->css[$name]['margin-left'] > 0) {
1112                $styles['marginLeft'] = (int)$this->css[$name]['margin-left'];
1113            }
1114            if ((int)$this->css[$name]['padding-left'] > 0) {
1115                $styles['paddingLeft'] = (int)$this->css[$name]['padding-left'];
1116            }
1117        }
1118
1119        if (isset($attribs['id']) && $this->css->hasSelector('#' . $attribs['id'])) {
1120            if ($this->css['#' . $attribs['id']]->hasProperty('font-family')) {
1121                $styles['fontFamily'] = str_replace('"', '', $this->css['#' . $attribs['id']]['font-family']);
1122            }
1123            if ($this->css['#' . $attribs['id']]->hasProperty('font-size')) {
1124                $styles['fontSize'] = (int)$this->css['#' . $attribs['id']]['font-size'];
1125            }
1126            if ($this->css['#' . $attribs['id']]->hasProperty('font-weight')) {
1127                $styles['fontWeight'] = $this->css['#' . $attribs['id']]['font-weight'];
1128            }
1129            if ($this->css['#' . $attribs['id']]->hasProperty('color')) {
1130                $styles['color'] = $this->css['#' . $attribs['id']]['color'];
1131                if (is_string($styles['color'])) {
1132                    $cssColor = Color::parse($styles['color']);
1133                    $styles['color'] = $cssColor->toRgb()->toArray(false);
1134                }
1135            }
1136            if ($this->css['#' . $attribs['id']]->hasProperty('float')) {
1137                $styles['float'] = $this->css['#' . $attribs['id']]['float'];
1138            }
1139            if ($this->css['#' . $attribs['id']]->hasProperty('width')) {
1140                $styles['width'] = $this->css['#' . $attribs['id']]['width'];
1141            }
1142            if ($this->css['#' . $attribs['id']]->hasProperty('height')) {
1143                $styles['height'] = $this->css['#' . $attribs['id']]['height'];
1144            }
1145            if ($this->css['#' . $attribs['id']]->hasProperty('line-height')) {
1146                $styles['lineHeight'] = (int)$this->css['#' . $attribs['id']]['line-height'];
1147            }
1148            if ((int)$this->css['#' . $attribs['id']]['margin-top'] > 0) {
1149                $styles['marginTop'] = (int)$this->css['#' . $attribs['id']]['margin-top'];
1150            }
1151            if ((int)$this->css['#' . $attribs['id']]['padding-top'] > 0) {
1152                $styles['paddingTop'] = (int)$this->css['#' . $attribs['id']]['padding-top'];
1153            }
1154            if ((int)$this->css['#' . $attribs['id']]['margin-right'] > 0) {
1155                $styles['marginRight'] = (int)$this->css['#' . $attribs['id']]['margin-right'];
1156            }
1157            if ((int)$this->css['#' . $attribs['id']]['padding-right'] > 0) {
1158                $styles['paddingRight'] = (int)$this->css['#' . $attribs['id']]['padding-right'];
1159            }
1160            if ((int)$this->css['#' . $attribs['id']]['margin-bottom'] > 0) {
1161                $styles['marginBottom'] = (int)$this->css['#' . $attribs['id']]['margin-bottom'];
1162            }
1163            if ((int)$this->css['#' . $attribs['id']]['padding-bottom'] > 0) {
1164                $styles['paddingBottom'] = (int)$this->css['#' . $attribs['id']]['padding-bottom'];
1165            }
1166            if ((int)$this->css['#' . $attribs['id']]['margin-left'] > 0) {
1167                $styles['marginLeft'] = (int)$this->css['#' . $attribs['id']]['margin-left'];
1168            }
1169            if ((int)$this->css['#' . $attribs['id']]['padding-left'] > 0) {
1170                $styles['paddingLeft'] = (int)$this->css['#' . $attribs['id']]['padding-left'];
1171            }
1172        }
1173
1174        if (isset($attribs['class']) && $this->css->hasSelector('.' . $attribs['class'])) {
1175            if ($this->css['.' . $attribs['class']]->hasProperty('font-family')) {
1176                $styles['fontFamily'] = str_replace('"', '', $this->css['.' . $attribs['class']]['font-family']);
1177            }
1178            if ($this->css['.' . $attribs['class']]->hasProperty('font-size')) {
1179                $styles['fontSize'] = (int)$this->css['.' . $attribs['class']]['font-size'];
1180            }
1181            if ($this->css['.' . $attribs['class']]->hasProperty('font-weight')) {
1182                $styles['fontWeight'] = $this->css['.' . $attribs['class']]['font-weight'];
1183            }
1184            if ($this->css['.' . $attribs['class']]->hasProperty('color')) {
1185                $styles['color'] = $this->css['.' . $attribs['class']]['color'];
1186                if (is_string($styles['color'])) {
1187                    $cssColor = Color::parse($styles['color']);
1188                    $styles['color'] = $cssColor->toRgb()->toArray(false);
1189                }
1190            }
1191            if ($this->css['.' . $attribs['class']]->hasProperty('float')) {
1192                $styles['float'] = $this->css['.' . $attribs['class']]['float'];
1193            }
1194            if ($this->css['.' . $attribs['class']]->hasProperty('width')) {
1195                $styles['width'] = $this->css['.' . $attribs['class']]['width'];
1196            }
1197            if ($this->css['.' . $attribs['class']]->hasProperty('height')) {
1198                $styles['height'] = $this->css['.' . $attribs['class']]['height'];
1199            }
1200            if ($this->css['.' . $attribs['class']]->hasProperty('line-height')) {
1201                $styles['lineHeight'] = (int)$this->css['.' . $attribs['class']]['line-height'];
1202            }
1203            if ((int)$this->css['.' . $attribs['class']]['margin-top'] > 0) {
1204                $styles['marginTop'] = (int)$this->css['.' . $attribs['class']]['margin-top'];
1205            }
1206            if ((int)$this->css['.' . $attribs['class']]['padding-top'] > 0) {
1207                $styles['paddingTop'] = (int)$this->css['.' . $attribs['class']]['padding-top'];
1208            }
1209            if ((int)$this->css['.' . $attribs['class']]['margin-right'] > 0) {
1210                $styles['marginRight'] = (int)$this->css['.' . $attribs['class']]['margin-right'];
1211            }
1212            if ((int)$this->css['.' . $attribs['class']]['padding-right'] > 0) {
1213                $styles['paddingRight'] = (int)$this->css['.' . $attribs['class']]['padding-right'];
1214            }
1215            if ((int)$this->css['.' . $attribs['class']]['margin-bottom'] > 0) {
1216                $styles['marginBottom'] = (int)$this->css['.' . $attribs['class']]['margin-bottom'];
1217            }
1218            if ((int)$this->css['.' . $attribs['class']]['padding-bottom'] > 0) {
1219                $styles['paddingBottom'] = (int)$this->css['.' . $attribs['class']]['padding-bottom'];
1220            }
1221            if ((int)$this->css['.' . $attribs['class']]['margin-left'] > 0) {
1222                $styles['marginLeft'] = (int)$this->css['.' . $attribs['class']]['margin-left'];
1223            }
1224            if ((int)$this->css['.' . $attribs['class']]['padding-left'] > 0) {
1225                $styles['paddingLeft'] = (int)$this->css['.' . $attribs['class']]['padding-left'];
1226            }
1227        }
1228
1229        if (str_contains($styles['fontFamily'], ',')) {
1230            $fonts = explode(',', $styles['fontFamily']);
1231            foreach ($fonts as $font) {
1232                $font = trim($font);
1233                if ($this->document->hasFont($font)) {
1234                    $styles['currentFont'] = $font;
1235                    break;
1236                } else if ($this->document->hasFont(str_replace(' ', '-', $font))) {
1237                    $styles['currentFont'] = str_replace(' ', '-', $font);
1238                } else if ($this->document->hasFont(str_replace(' ', ',', $font))) {
1239                    $styles['currentFont'] = str_replace(' ', ',', $font);
1240                } else if ($this->document->hasFont(str_replace(' ', '', $font))) {
1241                    $styles['currentFont'] = str_replace(' ', '', $font);
1242                }
1243            }
1244        } else {
1245            $styles['currentFont'] = $styles['fontFamily'];
1246        }
1247
1248        if ($styles['currentFont'] === null) {
1249            throw new Exception('Error: No available font has been detected.');
1250        } else if ($styles['currentFont'] == 'sans-serif') {
1251            $styles['currentFont'] = 'Arial';
1252        } else if ($styles['currentFont'] == 'serif') {
1253            $styles['currentFont'] = 'TimesNewRoman';
1254        }
1255
1256        if ($styles['fontWeight'] == 'bold') {
1257            if ($this->document->hasFont($styles['currentFont'] . 'Bold')) {
1258                $styles['currentFont'] .= 'Bold';
1259            } else if ($this->document->hasFont($styles['currentFont'] . '-Bold')) {
1260                $styles['currentFont'] .= '-Bold';
1261            } else if ($this->document->hasFont($styles['currentFont'] . ',Bold')) {
1262                $styles['currentFont'] .= ',Bold';
1263            }
1264        }
1265
1266        if (!($this->document->hasFont($styles['currentFont']))) {
1267            $standardFonts = Document\Font::standardFonts();
1268            if (in_array($styles['currentFont'], $standardFonts)) {
1269                $this->document->addFont(new Document\Font($styles['currentFont']));
1270            } else if (in_array(str_replace(' ', '-', $styles['currentFont']), $standardFonts)) {
1271                $styles['currentFont'] = str_replace(' ', '-', $styles['currentFont']);
1272                $this->document->addFont(new Document\Font($styles['currentFont']));
1273            } else if (in_array(str_replace(' ', ',', $styles['currentFont']), $standardFonts)) {
1274                $styles['currentFont'] = str_replace(' ', ',', $styles['currentFont']);
1275                $this->document->addFont(new Document\Font($styles['currentFont']));
1276            } else if (in_array(str_replace(' ', '', $styles['currentFont']), $standardFonts)) {
1277                $styles['currentFont'] = str_replace(' ', '', $styles['currentFont']);
1278                $this->document->addFont(new Document\Font($styles['currentFont']));
1279            } else {
1280                throw new Exception('Error: The current font has not been added to the document.');
1281            }
1282
1283            if ($styles['fontWeight'] == 'bold') {
1284                if ($this->document->hasFont($styles['currentFont'] . 'Bold')) {
1285                    $styles['currentFont'] .= 'Bold';
1286                } else if ($this->document->hasFont($styles['currentFont'] . '-Bold')) {
1287                    $styles['currentFont'] .= '-Bold';
1288                } else if ($this->document->hasFont($styles['currentFont'] . ',Bold')) {
1289                    $styles['currentFont'] .= ',Bold';
1290                } else if (in_array($styles['currentFont'] . 'Bold', $standardFonts)) {
1291                    $styles['currentFont'] .= 'Bold';
1292                    $this->document->addFont(new Document\Font($styles['currentFont']));
1293                } else if (in_array($styles['currentFont'] . '-Bold', $standardFonts)) {
1294                    $styles['currentFont'] .= '-Bold';
1295                    $this->document->addFont(new Document\Font($styles['currentFont']));
1296                } else if (in_array($styles['currentFont'] . ',Bold', $standardFonts)) {
1297                    $styles['currentFont'] .= ',Bold';
1298                    $this->document->addFont(new Document\Font($styles['currentFont']));
1299                }
1300            }
1301        }
1302
1303        return $styles;
1304    }
1305
1306    /**
1307     * Get current X-position
1308     *
1309     * @return int
1310     */
1311    protected function getCurrentX(): int
1312    {
1313        if ($this->x < $this->pageMargins['left']) {
1314            $this->x = $this->pageMargins['left'];
1315        }
1316        return $this->x;
1317    }
1318
1319    /**
1320     * Reset X-position
1321     *
1322     * @return int
1323     */
1324    protected function resetX(): int
1325    {
1326        $this->x = $this->pageMargins['left'];
1327        return $this->x;
1328    }
1329
1330    /**
1331     * Get current Y-position
1332     *
1333     * @return int
1334     */
1335    protected function getCurrentY(): int
1336    {
1337        if (!($this->document->hasPages())) {
1338            $this->page = (is_array($this->pageSize)) ?
1339                new Document\Page($this->pageSize['width'], $this->pageSize['height']) : new Document\Page($this->pageSize);
1340            $this->document->addPage($this->page);
1341        } else {
1342            $this->page = $this->document->getPage($this->document->getCurrentPage());
1343        }
1344
1345        $currentY = $this->page->getHeight() - $this->pageMargins['top'] - $this->y;
1346
1347        if ($currentY <= $this->pageMargins['bottom']) {
1348            $this->page = (is_array($this->pageSize)) ?
1349                new Document\Page($this->pageSize['width'], $this->pageSize['height']) : new Document\Page($this->pageSize);
1350            $this->document->addPage($this->page);
1351            $currentY = $this->resetY();
1352        }
1353
1354        return $currentY;
1355    }
1356
1357    /**
1358     * Reset Y-position
1359     *
1360     * @return int
1361     */
1362    protected function resetY(): int
1363    {
1364        if (!($this->document->hasPages())) {
1365            $this->page = (is_array($this->pageSize)) ?
1366                new Document\Page($this->pageSize['width'], $this->pageSize['height']) : new Document\Page($this->pageSize);
1367            $this->document->addPage($this->page);
1368        } else {
1369            $this->page = $this->document->getPage($this->document->getCurrentPage());
1370        }
1371
1372        $this->y  = 0;
1373
1374        return $this->page->getHeight() - $this->pageMargins['top'];
1375    }
1376
1377    /**
1378     * Create new page
1379     *
1380     * @return int
1381     */
1382    protected function newPage(): int
1383    {
1384        $this->page = (is_array($this->pageSize)) ?
1385            new Document\Page($this->pageSize['width'], $this->pageSize['height']) : new Document\Page($this->pageSize);
1386        $this->document->addPage($this->page);
1387        $this->y = 0;
1388        return $this->page->getHeight() - $this->pageMargins['top'] - $this->y;
1389    }
1390
1391    /**
1392     * Go to next line
1393     *
1394     * @param  array $styles
1395     * @return void
1396     */
1397    protected function goToNextLine(array $styles): void
1398    {
1399        $this->y += $styles['marginBottom'] + $styles['paddingBottom'] + $styles['lineHeight'];
1400    }
1401
1402    /**
1403     * Get string lines
1404     *
1405     * @param  string        $string
1406     * @param  int           $fontSize
1407     * @param  int           $wrapLength
1408     * @param  Document\Font $fontObject
1409     * @return array
1410     */
1411    protected function getStringLines(string $string, int $fontSize, int $wrapLength, Document\Font $fontObject): array
1412    {
1413        $strings   = [];
1414        $curString = '';
1415        $words     = explode(' ', $string);
1416
1417        foreach ($words as $word) {
1418            $newString = ($curString != '') ? $curString . ' ' . $word : $word;
1419            if ($fontObject->getStringWidth($newString, $fontSize) <= $wrapLength) {
1420                $curString = $newString;
1421            } else {
1422                $strings[] = $curString;
1423                $curString = $word;
1424            }
1425        }
1426        if (!empty($curString)) {
1427            $strings[] = $curString;
1428        }
1429
1430        return $strings;
1431    }
1432
1433}