Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
36.60% covered (warning)
36.60%
224 / 612
63.83% covered (warning)
63.83%
30 / 47
CRAP
0.00% covered (danger)
0.00%
0 / 1
Parser
36.60% covered (warning)
36.60%
224 / 612
63.83% covered (warning)
63.83%
30 / 47
15041.45
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
19.30% covered (danger)
19.30%
33 / 171
0.00% covered (danger)
0.00%
0 / 1
1644.92
 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.44% covered (danger)
27.44%
59 / 215
0.00% covered (danger)
0.00%
0 / 1
3325.21
 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 (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
8 * @license    https://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@noladev.com>
27 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    5.2.7
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 $i => $child) {
586                $this->addNodeToDocument($child, $i);
587            }
588        } else {
589            foreach ($htmlNodes as $i => $child) {
590                $this->addNodeToDocument($child, $i);
591            }
592        }
593
594        return $this->document;
595    }
596
597    /**
598     * Add node to document
599     *
600     * @param  Child $child
601     * @param  int   $i
602     * @throws Exception|\Pop\Pdf\Exception
603     * @return void
604     */
605    protected function addNodeToDocument(Child $child, int $i = 0): void
606    {
607        $styles   = $this->prepareNodeStyles($child->getNodeName(), $child->getAttributes());
608        $currentX = $this->getCurrentX();
609
610        if ($this->yOverride !== null) {
611            $currentY        = $this->yOverride;
612            $this->yOverride = null;
613        } else {
614            $currentY = $this->getCurrentY(($i != 0) ? $styles['marginBottom'] ?? 0 : 0);
615        }
616
617        $wrapLength = ($this->x > $this->pageMargins['left']) ?
618            $this->page->getWidth() - $this->pageMargins['right'] - $this->x :
619            $this->page->getWidth() - $this->pageMargins['right'] - $this->pageMargins['left'];
620
621        // Image node
622        if ($child->getNodeName() == 'img') {
623            $image = Document\Page\Image::createImageFromFile($this->fileDir . '/' . $child->getAttribute('src'));
624            $width = null;
625            $height = null;
626            $align = null;
627
628            if ($child->hasAttribute('width')) {
629                $width = (strpos($child->getAttribute('width'), '%')) ?
630                    $this->page->getWidth() * ((int)$child->getAttribute('width') / 100) : (int)$child->getAttribute('width');
631            } else if ($child->hasAttribute('height')) {
632                $height = (strpos($child->getAttribute('height'), '%')) ?
633                    $this->page->getHeight() * ((int)$child->getAttribute('height') / 100) : (int)$child->getAttribute('height');
634            } else if ($styles['width'] !== null) {
635                $width = (strpos($styles['width'], '%')) ?
636                    $this->page->getWidth() * ((int)$styles['width'] / 100) : (int)$styles['width'];
637            } else if ($styles['height'] !== null) {
638                $height = (strpos($styles['height'], '%')) ?
639                    $this->page->getHeight() * ((int)$styles['height'] / 100) : (int)$styles['height'];
640            }
641
642            if ($width !== null) {
643                $image->resizeToWidth($width);
644            } else if ($height !== null) {
645                $image->resizeToHeight($height);
646            }
647
648            if ($height === null) {
649                $height = ($image->getResizedHeight() !== null) ? $image->getResizedHeight() : $image->getHeight();
650            }
651
652            if ($child->hasAttribute('align')) {
653                $align = strtoupper($child->getAttribute('align'));
654            } else if (isset($styles['float'])) {
655                $align = strtoupper($styles['float']);
656            }
657
658            if ($align == 'LEFT') {
659                $box = [
660                    'left' => $currentX,
661                    'right' => $currentX + $width + ($styles['marginRight'] ?? 0),
662                    'top' => $currentY,
663                    'bottom' => $currentY - $height - ($styles['marginBottom'] ?? 0)
664                ];
665                $this->textWrap = new Document\Page\Text\Wrap('RIGHT', $this->pageMargins['left'], $this->page->getWidth() - $this->pageMargins['right'], $box);
666            } else if ($align == 'RIGHT') {
667                $box = [
668                    'left' => $this->page->getWidth() - $this->pageMargins['right'] - $width - ($styles['marginLeft'] ?? 0),
669                    'right' => $this->page->getWidth() - $this->pageMargins['right'],
670                    'top' => $currentY,
671                    'bottom' => $currentY - $height - ($styles['marginBottom'] ?? 0)
672                ];
673                $this->textWrap = new Document\Page\Text\Wrap('LEFT', $this->pageMargins['left'], $this->page->getWidth() - $this->pageMargins['right'], $box);
674            }
675
676            if ($this->textWrap !== null) {
677                $newY = $currentY - (($image->getResizedHeight() !== null) ? $image->getResizedHeight() : $image->getHeight());
678                if ($align == 'RIGHT') {
679                    $this->page->addImage($image, ($this->page->getWidth() - $this->pageMargins['right'] - $width), $newY);
680                } else {
681                    $this->page->addImage($image, $currentX, $newY);
682                }
683                $currentY -= $styles['lineHeight'];
684                $this->y += $styles['lineHeight'];
685            } else {
686                $currentY -= ($image->getResizedHeight() !== null) ? $image->getResizedHeight() : $image->getHeight();
687                $this->y += ($image->getResizedHeight() !== null) ? $image->getResizedHeight() : $image->getHeight();
688                $this->page->addImage($image, $currentX, $currentY);
689                $currentY -= $styles['lineHeight'];
690                $this->y += $styles['lineHeight'];
691            }
692        // Table node
693        } else if ($child->getNodeName() == 'table') {
694            $tableWidth  = $this->page->getWidth() - $this->pageMargins['left'] - $this->pageMargins['right'];
695            $columnCount = count($child->getChild(0)->getChildNodes());;
696            $rowCount    = 0;
697            $columnWidth = floor($tableWidth / $columnCount);
698            $fontObject  = $this->document->getFont($styles['currentFont']);
699            $currentRow  = 0;
700            $currentX    = $this->pageMargins['left'] + 10;
701            $thHeight    = 0;
702            $offset      = 0;
703            $startY      = $currentY;
704
705            foreach ($child->getChildNodes() as $childNode) {
706                if (($childNode->getNodeName() == 'tr') && ($childNode->hasChildNodes())) {
707                    foreach ($childNode->getChildNodes() as $grandChild) {
708                        if ($grandChild->getNodeName() == 'th') {
709                            $thString = $grandChild->getNodeValue();
710
711                            $thStringWidth = $fontObject->getStringWidth($thString, $styles['fontSize']);
712                            if ($thStringWidth > ($columnWidth - 20)) {
713                                $strings = $this->getStringLines($thString, $styles['fontSize'], $columnWidth - 20, $fontObject);
714                                foreach ($strings as $i => $string) {
715                                    $text = new Document\Page\Text($string, $styles['fontSize']);
716                                    $this->page->addText($text, $styles['currentFont'], $currentX, ($currentY - ($styles['fontSize'] * $i)));
717                                }
718                                $newThHeight = (count($strings) * $styles['fontSize']) + 20;
719                                if ($newThHeight > $thHeight) {
720                                    $thHeight = $newThHeight;
721                                    $offset   = $thHeight - 30;
722                                }
723                            } else {
724                                $text = new Document\Page\Text($thString, $styles['fontSize']);
725                                $this->page->addText($text, $styles['currentFont'], $currentX, $currentY);
726                            }
727                            $currentX += $columnWidth;
728                        }
729                    }
730                }
731            }
732
733            foreach ($child->getChildNodes() as $childNode) {
734                if (($childNode->getNodeName() == 'tr') && ($childNode->hasChildNodes())) {
735                    $rowCount++;
736                    $currentX  = $this->pageMargins['left'] + 10;
737                    foreach ($childNode->getChildNodes() as $grandChild) {
738                        if ($grandChild->getNodeName() == 'td') {
739                            $text = new Document\Page\Text($grandChild->getNodeValue(), $styles['fontSize']);
740                            $this->page->addText($text, $styles['currentFont'], $currentX, $currentY - $offset);
741                            $currentX += $columnWidth;
742                        }
743                    }
744                    $currentY -= 25;
745                }
746            }
747
748            $currentY += 15;
749            $x1 = $this->pageMargins['left'];
750/**
751            $finalHeight = (25 * $rowCount);
752            $path    = new Document\Page\Path();
753            $path->drawRectangle($x1, $currentY, $tableWidth, $finalHeight);
754            $this->page->addPath($path);
755
756            for ($i = 1; $i < $columnCount; $i++) {
757                $path = new Document\Page\Path();
758                $path->drawLine($x1 + ($i * $columnWidth), $currentY, $x1 + ($i * $columnWidth), $this->page->getHeight() - $finalHeight + 35);
759                $this->page->addPath($path);
760            }
761*/
762
763            for ($i = 0; $i < $rowCount; $i++) {
764                if (($i == 0) && ($thHeight > 0)) {
765                    $lineY = $startY - $thHeight + 20;
766                } else {
767                    $lineY = ($startY - ($i * 25) - 10) - $offset;
768                }
769                $path  = new Document\Page\Path();
770                $path->drawLine($x1, $lineY, $tableWidth + $this->pageMargins['left'], $lineY);
771                $this->page->addPath($path);
772/**
773                if (($i == 1) && ($thHeight > 0)) {
774                    $lineY = ($currentY + ($i * 25));
775                } else {
776                    $lineY = ($currentY + ($i * 25));
777                }
778                $path  = new Document\Page\Path();
779                $path->drawLine($x1, $lineY, $tableWidth + $this->pageMargins['left'], $lineY);
780                $this->page->addPath($path);
781*/
782            }
783
784
785        // Text node
786        } else {
787            if ($this->textWrap !== null) {
788                $box = $this->textWrap->getBox();
789                if ($this->textWrap->isRight()) {
790                    $startX = $box['right'];
791                    $startY = $box['top'] - $styles['fontSize'];
792                    $edgeX  = $wrapLength;
793                    $edgeY  = $box['bottom'];
794                } else {
795                    $startX = $currentX;
796                    $startY = $box['top'] - $styles['fontSize'];
797                    $edgeX  = $box['left'] - 40;
798                    $edgeY  = $box['bottom'];
799                }
800            } else {
801                $startX = $currentX;
802                $startY = $currentY;
803                $edgeX  = $wrapLength;
804                $edgeY  = $this->pageMargins['bottom'];
805            }
806
807            $textStream = new Document\Page\Text\Stream($startX, $startY, $edgeX, $edgeY);
808            $textStream->setCurrentStyle(
809                $styles['currentFont'],
810                $styles['fontSize'],
811                new Color\Rgb($styles['color'][0], $styles['color'][1], $styles['color'][2]),
812                $styles['textAlign']
813            );
814            $streamY = $styles['lineHeight'] ?? null;
815            if (!empty($child->getNodeValue())) {
816                $textStream->addText($child->getNodeValue(), $streamY);
817            }
818
819            $childTextStreams = [];
820            if ($child->hasChildNodes()) {
821                foreach ($child->getChildNodes() as $grandChild) {
822                    $gcStyles = $this->prepareNodeStyles($grandChild->getNodeName(), $grandChild->getAttributes(), $styles);
823                    $textStream->setCurrentStyle(
824                        $gcStyles['currentFont'],
825                        $gcStyles['fontSize'],
826                        new Color\Rgb($gcStyles['color'][0], $gcStyles['color'][1], $gcStyles['color'][2])
827                    );
828                    $streamY = $gcStyles['lineHeight'] ?? null;
829                    if (!empty($grandChild->getNodeValue())) {
830                        $textStream->addText($grandChild->getNodeValue(), $streamY, ($grandChild->getNodeName() == 'br'));
831                    }
832                }
833            }
834
835            $this->page->addTextStream($textStream);
836            if (!empty($childTextStreams)) {
837                foreach ($childTextStreams as $childTextStream) {
838                    $this->page->addTextStream($childTextStream);
839                }
840            }
841
842            $orphanStream = clone $textStream;
843            $hasOrphans   = false;
844
845            while ($orphanStream->hasOrphans($this->document->getFonts())) {
846                $orphanStream = $orphanStream->getOrphanStream();
847                if ($orphanStream->getCurrentY() <= $this->pageMargins['bottom']) {
848                    $currentY = $this->newPage();
849                    $orphanStream->setCurrentY($currentY);
850                } else {
851                    $orphanStream->setStartX($this->pageMargins['left']);
852                    $orphanStream->setEdgeX($wrapLength);
853                    $orphanStream->setEdgeY($this->pageMargins['bottom']);
854                }
855
856                $orphanStream->setCurrentX($currentX);
857                $this->page->addTextStream($orphanStream);
858
859                $orphanStream = clone $orphanStream;
860                $hasOrphans = true;
861            }
862
863            if ($hasOrphans) {
864                $this->yOverride = $orphanStream->getCurrentY();
865            } else {
866                $this->yOverride = null;
867                $this->y  += (!empty($styles['marginBottom'])) ? $styles['marginBottom'] : 25;
868            }
869        }
870    }
871
872    /**
873     * Add node stream to document
874     *
875     * @param  Child $child
876     * @throws Exception
877     * @return void
878     */
879    protected function addNodeStreamToDocument(Child $child): void
880    {
881        $styles     = $this->prepareNodeStyles($child->getNodeName(), $child->getAttributes());
882        $currentX   = $this->getCurrentX();
883        $currentY   = $this->getCurrentY();
884        $fontObject = $this->document->getFont($styles['currentFont']);
885        $wrapLength = ($this->x > $this->pageMargins['left']) ?
886            $this->page->getWidth() - $this->pageMargins['right'] - $this->x :
887            $this->page->getWidth() - $this->pageMargins['right'] - $this->pageMargins['left'];
888
889        $string      = $child->getNodeValue();
890        $stringWidth = $fontObject->getStringWidth($string, $styles['fontSize']);
891
892        if ($stringWidth > $wrapLength) {
893            $strings = $this->getStringLines($string, $styles['fontSize'], $wrapLength, $fontObject);
894            if ($this->x > $this->pageMargins['left']) {
895                $text = new Document\Page\Text($strings[0], $styles['fontSize']);
896                $text->setFillColor(new Color\Rgb($styles['color'][0], $styles['color'][1], $styles['color'][2]));
897                $this->page->addText($text, $styles['currentFont'], $currentX, $currentY);
898                if ($currentY <= $this->pageMargins['bottom']) {
899                    $currentY = $this->newPage();
900                } else {
901                    $currentY -= $styles['lineHeight'];
902                    $this->y  += $styles['lineHeight'];
903                }
904                $currentX = $this->resetX();
905                $wrapLength = $this->page->getWidth() - $this->pageMargins['right'] - $this->pageMargins['left'];
906                unset($strings[0]);
907                $strings = $this->getStringLines(implode(' ', $strings), $styles['fontSize'], $wrapLength, $fontObject);
908            }
909
910            foreach ($strings as $i => $string) {
911                $text = new Document\Page\Text($string, $styles['fontSize']);
912                $text->setFillColor(new Color\Rgb($styles['color'][0], $styles['color'][1], $styles['color'][2]));
913                $this->page->addText($text, $styles['currentFont'], $currentX, $currentY);
914                if ($i < (count($strings) - 1)) {
915                    if ($currentY <= $this->pageMargins['bottom']) {
916                        $currentY = $this->newPage();
917                    } else {
918                        $currentY -= $styles['lineHeight'];
919                        $this->y  += $styles['lineHeight'];
920                    }
921                }
922            }
923            $this->x += $fontObject->getStringWidth($string, $styles['fontSize']);
924        } else {
925            $text = new Document\Page\Text($string, $styles['fontSize']);
926            $text->setFillColor(new Color\Rgb($styles['color'][0], $styles['color'][1], $styles['color'][2]));
927            $this->page->addText($text, $styles['currentFont'], $currentX, $currentY);
928            $this->x += $fontObject->getStringWidth($string, $styles['fontSize']);
929        }
930
931        foreach ($child->getChildNodes() as $grandChild) {
932            $this->addNodeStreamToDocument($grandChild);
933        }
934    }
935
936    /**
937     * Create default styles
938     *
939     * @return void
940     */
941    protected function createDefaultStyles(): void
942    {
943        $h1 = new Css\Selector('h1');
944        $h1['margin-bottom'] = '18px';
945        $h1['font-size']     = '32px';
946        $h1['font-weight']   = 'bold';
947
948        $h2 = new Css\Selector('h2');
949        $h2['margin-bottom'] = '18px';
950        $h2['font-size']     = '28px';
951        $h2['font-weight']   = 'bold';
952
953        $h3 = new Css\Selector('h3');
954        $h3['margin-bottom'] = '16px';
955        $h3['font-size']     = '24px';
956        $h3['font-weight']   = 'bold';
957
958        $h4 = new Css\Selector('h4');
959        $h4['margin-bottom'] = '14px';
960        $h4['font-size']     = '20px';
961        $h4['font-weight']   = 'bold';
962
963        $h5 = new Css\Selector('h5');
964        $h5['margin-bottom'] = '12px';
965        $h5['font-size']     = '16px';
966        $h5['font-weight']   = 'bold';
967
968        $h6 = new Css\Selector('h6');
969        $h6['margin-bottom'] = '10px';
970        $h6['font-size']     = '14px';
971        $h6['font-weight']   = 'bold';
972
973        $p = new Css\Selector('p');
974        $p['margin-bottom'] = '24px';
975        $p['font-size']     = '12px';
976
977        $a = new Css\Selector('a');
978        $a['color'] = new Color\Rgb(0, 0, 255);
979
980        $strong = new Css\Selector('strong');
981        $strong['font-size']   = '10px';
982        $strong['font-weight'] = 'bold';
983
984        $em = new Css\Selector('em');
985        $em['font-size']  = '10px';
986        $em['font-style'] = 'italic';
987
988        $this->css = new Css\Css();
989        $this->css->addSelectors([$h1, $h2, $h3, $h4, $h5, $h6, $p, $a, $strong, $em]);
990
991        if (!($this->document->hasFont('Arial'))) {
992            $this->document->addFont(new Document\Font('Arial'));
993        }
994        if (!($this->document->hasFont('Arial,Bold'))) {
995            $this->document->addFont(new Document\Font('Arial,Bold'));
996        }
997        if (!($this->document->hasFont('Arial,Italic'))) {
998            $this->document->addFont(new Document\Font('Arial,Italic'));
999        }
1000        if (!($this->document->hasFont('Arial,BoldItalic'))) {
1001            $this->document->addFont(new Document\Font('Arial,BoldItalic'));
1002        }
1003        if (!($this->document->hasFont('TimesNewRoman'))) {
1004            $this->document->addFont(new Document\Font('TimesNewRoman'));
1005        }
1006        if (!($this->document->hasFont('TimesNewRoman,Bold'))) {
1007            $this->document->addFont(new Document\Font('TimesNewRoman,Bold'));
1008        }
1009        if (!($this->document->hasFont('TimesNewRoman,Italic'))) {
1010            $this->document->addFont(new Document\Font('TimesNewRoman,Italic'));
1011        }
1012        if (!($this->document->hasFont('TimesNewRoman,BoldItalic'))) {
1013            $this->document->addFont(new Document\Font('TimesNewRoman,BoldItalic'));
1014        }
1015    }
1016
1017    /**
1018     * Prepare node styles
1019     *
1020     * @param  string $name
1021     * @param  array  $attribs
1022     * @param  array  $currentStyles
1023     * @throws Exception
1024     * @return array
1025     */
1026    protected function prepareNodeStyles(string $name, array $attribs = [], array $currentStyles = []): array
1027    {
1028        $styles = [
1029            'currentFont'   => null,
1030            'fontFamily'    => $currentStyles['fontFamily'] ?? $this->defaultStyles['font-family'],
1031            'fontSize'      => $currentStyles['fontSize'] ?? $this->defaultStyles['font-size'],
1032            'fontWeight'    => $currentStyles['fontWeight'] ?? $this->defaultStyles['font-weight'],
1033            'float'         => null,
1034            'width'         => null,
1035            'height'        => null,
1036            'color'         => $currentStyles['color'] ?? $this->defaultStyles['color'],
1037            'lineHeight'    => $currentStyles['lineHeight'] ?? $this->defaultStyles['line-height'],
1038            'marginTop'     => 0,
1039            'paddingTop'    => 0,
1040            'marginRight'   => 0,
1041            'paddingRight'  => 0,
1042            'marginBottom'  => 0,
1043            'paddingBottom' => 0,
1044            'marginLeft'    => 0,
1045            'paddingLeft'   => 0,
1046            'textAlign'     => null
1047        ];
1048
1049        if (in_array($name, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])) {
1050            switch ($name) {
1051                case 'h1':
1052                    $styles['fontSize']   = round($styles['fontSize'] * 2.67); // 32
1053                    $styles['fontWeight'] = 'bold';
1054                    break;
1055                case 'h2':
1056                    $styles['fontSize']   = round($styles['fontSize'] * 2.33);  // 28
1057                    $styles['fontWeight'] = 'bold';
1058                    break;
1059                case 'h3':
1060                    $styles['fontSize']   = $styles['fontSize'] * 2;  // 24
1061                    $styles['fontWeight'] = 'bold';
1062                    break;
1063                case 'h4':
1064                    $styles['fontSize']   = round($styles['fontSize'] * 1.67); // 20
1065                    $styles['fontWeight'] = 'bold';
1066                    break;
1067                case 'h5':
1068                    $styles['fontSize']   = round($styles['fontSize'] * 1.33);  // 16
1069                    $styles['fontWeight'] = 'bold';
1070                    break;
1071                case 'h6':
1072                    $styles['fontWeight'] = 'bold';
1073                    break;
1074            }
1075        }
1076
1077        if ($this->css->hasSelector($name)) {
1078            if ($this->css[$name]->hasProperty('font-family')) {
1079                $styles['fontFamily'] = str_replace('"', '', $this->css[$name]['font-family']);
1080            }
1081            if ($this->css[$name]->hasProperty('font-size')) {
1082                //$styles['fontSize'] = (int)$this->css[$name]['font-size'];
1083            }
1084            if ($this->css[$name]->hasProperty('font-weight')) {
1085                $styles['fontWeight'] = $this->css[$name]['font-weight'];
1086            }
1087            if ($this->css[$name]->hasProperty('color')) {
1088                $styles['color'] = $this->css[$name]['color'];
1089                if (is_string($styles['color'])) {
1090                    $cssColor = Color::parse($styles['color']);
1091                    $styles['color'] = $cssColor->toRgb()->toArray(false);
1092                }
1093            }
1094            if ($this->css[$name]->hasProperty('float')) {
1095                $styles['float'] = $this->css[$name]['float'];
1096            }
1097            if ($this->css[$name]->hasProperty('width')) {
1098                $styles['width'] = $this->css[$name]['width'];
1099            }
1100            if ($this->css[$name]->hasProperty('height')) {
1101                $styles['height'] = $this->css[$name]['height'];
1102            }
1103            if ($this->css[$name]->hasProperty('line-height')) {
1104                $styles['lineHeight'] = (int)$this->css[$name]['line-height'];
1105            }
1106            if ((int)$this->css[$name]['margin-top'] > 0) {
1107                $styles['marginTop'] = (int)$this->css[$name]['margin-top'];
1108            }
1109            if ((int)$this->css[$name]['padding-top'] > 0) {
1110                $styles['paddingTop'] = (int)$this->css[$name]['padding-top'];
1111            }
1112            if ((int)$this->css[$name]['margin-right'] > 0) {
1113                $styles['marginRight'] = (int)$this->css[$name]['margin-right'];
1114            }
1115            if ((int)$this->css[$name]['padding-right'] > 0) {
1116                $styles['paddingRight'] = (int)$this->css[$name]['padding-right'];
1117            }
1118            if ((int)$this->css[$name]['margin-bottom'] > 0) {
1119                $styles['marginBottom'] = (int)$this->css[$name]['margin-bottom'];
1120            }
1121            if ((int)$this->css[$name]['padding-bottom'] > 0) {
1122                $styles['paddingBottom'] = (int)$this->css[$name]['padding-bottom'];
1123            }
1124            if ((int)$this->css[$name]['margin-left'] > 0) {
1125                $styles['marginLeft'] = (int)$this->css[$name]['margin-left'];
1126            }
1127            if ((int)$this->css[$name]['padding-left'] > 0) {
1128                $styles['paddingLeft'] = (int)$this->css[$name]['padding-left'];
1129            }
1130            if ($this->css[$name]->hasProperty('text-align')) {
1131                $styles['textAlign'] = $this->css[$name]['text-align'];
1132            }
1133        }
1134
1135        if (isset($attribs['id']) && $this->css->hasSelector('#' . $attribs['id'])) {
1136            if ($this->css['#' . $attribs['id']]->hasProperty('font-family')) {
1137                $styles['fontFamily'] = str_replace('"', '', $this->css['#' . $attribs['id']]['font-family']);
1138            }
1139            if ($this->css['#' . $attribs['id']]->hasProperty('font-size')) {
1140                $styles['fontSize'] = (int)$this->css['#' . $attribs['id']]['font-size'];
1141            }
1142            if ($this->css['#' . $attribs['id']]->hasProperty('font-weight')) {
1143                $styles['fontWeight'] = $this->css['#' . $attribs['id']]['font-weight'];
1144            }
1145            if ($this->css['#' . $attribs['id']]->hasProperty('color')) {
1146                $styles['color'] = $this->css['#' . $attribs['id']]['color'];
1147                if (is_string($styles['color'])) {
1148                    $cssColor = Color::parse($styles['color']);
1149                    $styles['color'] = $cssColor->toRgb()->toArray(false);
1150                }
1151            }
1152            if ($this->css['#' . $attribs['id']]->hasProperty('float')) {
1153                $styles['float'] = $this->css['#' . $attribs['id']]['float'];
1154            }
1155            if ($this->css['#' . $attribs['id']]->hasProperty('width')) {
1156                $styles['width'] = $this->css['#' . $attribs['id']]['width'];
1157            }
1158            if ($this->css['#' . $attribs['id']]->hasProperty('height')) {
1159                $styles['height'] = $this->css['#' . $attribs['id']]['height'];
1160            }
1161            if ($this->css['#' . $attribs['id']]->hasProperty('line-height')) {
1162                $styles['lineHeight'] = (int)$this->css['#' . $attribs['id']]['line-height'];
1163            }
1164            if ((int)$this->css['#' . $attribs['id']]['margin-top'] > 0) {
1165                $styles['marginTop'] = (int)$this->css['#' . $attribs['id']]['margin-top'];
1166            }
1167            if ((int)$this->css['#' . $attribs['id']]['padding-top'] > 0) {
1168                $styles['paddingTop'] = (int)$this->css['#' . $attribs['id']]['padding-top'];
1169            }
1170            if ((int)$this->css['#' . $attribs['id']]['margin-right'] > 0) {
1171                $styles['marginRight'] = (int)$this->css['#' . $attribs['id']]['margin-right'];
1172            }
1173            if ((int)$this->css['#' . $attribs['id']]['padding-right'] > 0) {
1174                $styles['paddingRight'] = (int)$this->css['#' . $attribs['id']]['padding-right'];
1175            }
1176            if ((int)$this->css['#' . $attribs['id']]['margin-bottom'] > 0) {
1177                $styles['marginBottom'] = (int)$this->css['#' . $attribs['id']]['margin-bottom'];
1178            }
1179            if ((int)$this->css['#' . $attribs['id']]['padding-bottom'] > 0) {
1180                $styles['paddingBottom'] = (int)$this->css['#' . $attribs['id']]['padding-bottom'];
1181            }
1182            if ((int)$this->css['#' . $attribs['id']]['margin-left'] > 0) {
1183                $styles['marginLeft'] = (int)$this->css['#' . $attribs['id']]['margin-left'];
1184            }
1185            if ((int)$this->css['#' . $attribs['id']]['padding-left'] > 0) {
1186                $styles['paddingLeft'] = (int)$this->css['#' . $attribs['id']]['padding-left'];
1187            }
1188            if ($this->css['#' . $attribs['id']]->hasProperty('text-align')) {
1189                $styles['textAlign'] = $this->css['#' . $attribs['id']]['text-align'];
1190            }
1191        }
1192
1193        if (isset($attribs['class']) && $this->css->hasSelector('.' . $attribs['class'])) {
1194            if ($this->css['.' . $attribs['class']]->hasProperty('font-family')) {
1195                $styles['fontFamily'] = str_replace('"', '', $this->css['.' . $attribs['class']]['font-family']);
1196            }
1197            if ($this->css['.' . $attribs['class']]->hasProperty('font-size')) {
1198                $styles['fontSize'] = (int)$this->css['.' . $attribs['class']]['font-size'];
1199            }
1200            if ($this->css['.' . $attribs['class']]->hasProperty('font-weight')) {
1201                $styles['fontWeight'] = $this->css['.' . $attribs['class']]['font-weight'];
1202            }
1203            if ($this->css['.' . $attribs['class']]->hasProperty('color')) {
1204                $styles['color'] = $this->css['.' . $attribs['class']]['color'];
1205                if (is_string($styles['color'])) {
1206                    $cssColor = Color::parse($styles['color']);
1207                    $styles['color'] = $cssColor->toRgb()->toArray(false);
1208                }
1209            }
1210            if ($this->css['.' . $attribs['class']]->hasProperty('float')) {
1211                $styles['float'] = $this->css['.' . $attribs['class']]['float'];
1212            }
1213            if ($this->css['.' . $attribs['class']]->hasProperty('width')) {
1214                $styles['width'] = $this->css['.' . $attribs['class']]['width'];
1215            }
1216            if ($this->css['.' . $attribs['class']]->hasProperty('height')) {
1217                $styles['height'] = $this->css['.' . $attribs['class']]['height'];
1218            }
1219            if ($this->css['.' . $attribs['class']]->hasProperty('line-height')) {
1220                $styles['lineHeight'] = (int)$this->css['.' . $attribs['class']]['line-height'];
1221            }
1222            if ((int)$this->css['.' . $attribs['class']]['margin-top'] > 0) {
1223                $styles['marginTop'] = (int)$this->css['.' . $attribs['class']]['margin-top'];
1224            }
1225            if ((int)$this->css['.' . $attribs['class']]['padding-top'] > 0) {
1226                $styles['paddingTop'] = (int)$this->css['.' . $attribs['class']]['padding-top'];
1227            }
1228            if ((int)$this->css['.' . $attribs['class']]['margin-right'] > 0) {
1229                $styles['marginRight'] = (int)$this->css['.' . $attribs['class']]['margin-right'];
1230            }
1231            if ((int)$this->css['.' . $attribs['class']]['padding-right'] > 0) {
1232                $styles['paddingRight'] = (int)$this->css['.' . $attribs['class']]['padding-right'];
1233            }
1234            if ((int)$this->css['.' . $attribs['class']]['margin-bottom'] > 0) {
1235                $styles['marginBottom'] = (int)$this->css['.' . $attribs['class']]['margin-bottom'];
1236            }
1237            if ((int)$this->css['.' . $attribs['class']]['padding-bottom'] > 0) {
1238                $styles['paddingBottom'] = (int)$this->css['.' . $attribs['class']]['padding-bottom'];
1239            }
1240            if ((int)$this->css['.' . $attribs['class']]['margin-left'] > 0) {
1241                $styles['marginLeft'] = (int)$this->css['.' . $attribs['class']]['margin-left'];
1242            }
1243            if ((int)$this->css['.' . $attribs['class']]['padding-left'] > 0) {
1244                $styles['paddingLeft'] = (int)$this->css['.' . $attribs['class']]['padding-left'];
1245            }
1246            if ($this->css['.' . $attribs['class']]->hasProperty('text-align')) {
1247                $styles['textAlign'] = $this->css['.' . $attribs['class']]['text-align'];
1248            }
1249        }
1250
1251        if (str_contains($styles['fontFamily'], ',')) {
1252            $fonts = explode(',', $styles['fontFamily']);
1253            foreach ($fonts as $font) {
1254                $font = trim($font);
1255                if ($this->document->hasFont($font)) {
1256                    $styles['currentFont'] = $font;
1257                    break;
1258                } else if ($this->document->hasFont(str_replace(' ', '-', $font))) {
1259                    $styles['currentFont'] = str_replace(' ', '-', $font);
1260                } else if ($this->document->hasFont(str_replace(' ', ',', $font))) {
1261                    $styles['currentFont'] = str_replace(' ', ',', $font);
1262                } else if ($this->document->hasFont(str_replace(' ', '', $font))) {
1263                    $styles['currentFont'] = str_replace(' ', '', $font);
1264                }
1265            }
1266        } else {
1267            $styles['currentFont'] = $styles['fontFamily'];
1268        }
1269
1270        if ($styles['currentFont'] === null) {
1271            throw new Exception('Error: No available font has been detected.');
1272        } else if ($styles['currentFont'] == 'sans-serif') {
1273            $styles['currentFont'] = 'Arial';
1274        } else if ($styles['currentFont'] == 'serif') {
1275            $styles['currentFont'] = 'TimesNewRoman';
1276        }
1277
1278        if ($styles['fontWeight'] == 'bold') {
1279            if ($this->document->hasFont($styles['currentFont'] . 'Bold')) {
1280                $styles['currentFont'] .= 'Bold';
1281            } else if ($this->document->hasFont($styles['currentFont'] . '-Bold')) {
1282                $styles['currentFont'] .= '-Bold';
1283            } else if ($this->document->hasFont($styles['currentFont'] . ',Bold')) {
1284                $styles['currentFont'] .= ',Bold';
1285            }
1286        }
1287
1288        if (!($this->document->hasFont($styles['currentFont']))) {
1289            $standardFonts = Document\Font::standardFonts();
1290            if (in_array($styles['currentFont'], $standardFonts)) {
1291                $this->document->addFont(new Document\Font($styles['currentFont']));
1292            } else if (in_array(str_replace(' ', '-', $styles['currentFont']), $standardFonts)) {
1293                $styles['currentFont'] = str_replace(' ', '-', $styles['currentFont']);
1294                $this->document->addFont(new Document\Font($styles['currentFont']));
1295            } else if (in_array(str_replace(' ', ',', $styles['currentFont']), $standardFonts)) {
1296                $styles['currentFont'] = str_replace(' ', ',', $styles['currentFont']);
1297                $this->document->addFont(new Document\Font($styles['currentFont']));
1298            } else if (in_array(str_replace(' ', '', $styles['currentFont']), $standardFonts)) {
1299                $styles['currentFont'] = str_replace(' ', '', $styles['currentFont']);
1300                $this->document->addFont(new Document\Font($styles['currentFont']));
1301            } else {
1302                throw new Exception('Error: The current font has not been added to the document.');
1303            }
1304
1305            if ($styles['fontWeight'] == 'bold') {
1306                if ($this->document->hasFont($styles['currentFont'] . 'Bold')) {
1307                    $styles['currentFont'] .= 'Bold';
1308                } else if ($this->document->hasFont($styles['currentFont'] . '-Bold')) {
1309                    $styles['currentFont'] .= '-Bold';
1310                } else if ($this->document->hasFont($styles['currentFont'] . ',Bold')) {
1311                    $styles['currentFont'] .= ',Bold';
1312                } else if (in_array($styles['currentFont'] . 'Bold', $standardFonts)) {
1313                    $styles['currentFont'] .= 'Bold';
1314                    $this->document->addFont(new Document\Font($styles['currentFont']));
1315                } else if (in_array($styles['currentFont'] . '-Bold', $standardFonts)) {
1316                    $styles['currentFont'] .= '-Bold';
1317                    $this->document->addFont(new Document\Font($styles['currentFont']));
1318                } else if (in_array($styles['currentFont'] . ',Bold', $standardFonts)) {
1319                    $styles['currentFont'] .= ',Bold';
1320                    $this->document->addFont(new Document\Font($styles['currentFont']));
1321                }
1322            }
1323        }
1324
1325        return $styles;
1326    }
1327
1328    /**
1329     * Get current X-position
1330     *
1331     * @return int
1332     */
1333    protected function getCurrentX(): int
1334    {
1335        if ($this->x < $this->pageMargins['left']) {
1336            $this->x = $this->pageMargins['left'];
1337        }
1338        return $this->x;
1339    }
1340
1341    /**
1342     * Reset X-position
1343     *
1344     * @return int
1345     */
1346    protected function resetX(): int
1347    {
1348        $this->x = $this->pageMargins['left'];
1349        return $this->x;
1350    }
1351
1352    /**
1353     * Get current Y-position
1354     *
1355     * @param  int $marginBottom
1356     * @return int
1357     */
1358    protected function getCurrentY($marginBottom = 0): int
1359    {
1360        if (!($this->document->hasPages())) {
1361            $this->page = (is_array($this->pageSize)) ?
1362                new Document\Page($this->pageSize['width'], $this->pageSize['height']) : new Document\Page($this->pageSize);
1363            $this->document->addPage($this->page);
1364        } else {
1365            $this->page = $this->document->getPage($this->document->getCurrentPage());
1366        }
1367
1368        $currentY = $this->page->getHeight() - $this->pageMargins['top'] - $this->y - $marginBottom;
1369
1370        if ($currentY <= $this->pageMargins['bottom']) {
1371            $this->page = (is_array($this->pageSize)) ?
1372                new Document\Page($this->pageSize['width'], $this->pageSize['height']) : new Document\Page($this->pageSize);
1373            $this->document->addPage($this->page);
1374            $currentY = $this->resetY();
1375        }
1376
1377        return $currentY;
1378    }
1379
1380    /**
1381     * Reset Y-position
1382     *
1383     * @return int
1384     */
1385    protected function resetY(): int
1386    {
1387        if (!($this->document->hasPages())) {
1388            $this->page = (is_array($this->pageSize)) ?
1389                new Document\Page($this->pageSize['width'], $this->pageSize['height']) : new Document\Page($this->pageSize);
1390            $this->document->addPage($this->page);
1391        } else {
1392            $this->page = $this->document->getPage($this->document->getCurrentPage());
1393        }
1394
1395        $this->y  = 0;
1396
1397        return $this->page->getHeight() - $this->pageMargins['top'];
1398    }
1399
1400    /**
1401     * Create new page
1402     *
1403     * @return int
1404     */
1405    protected function newPage(): int
1406    {
1407        $this->page = (is_array($this->pageSize)) ?
1408            new Document\Page($this->pageSize['width'], $this->pageSize['height']) : new Document\Page($this->pageSize);
1409        $this->document->addPage($this->page);
1410        $this->y = 0;
1411        return $this->page->getHeight() - $this->pageMargins['top'] - $this->y;
1412    }
1413
1414    /**
1415     * Go to next line
1416     *
1417     * @param  array $styles
1418     * @return void
1419     */
1420    protected function goToNextLine(array $styles): void
1421    {
1422        $this->y += $styles['marginBottom'] + $styles['paddingBottom'] + $styles['lineHeight'];
1423    }
1424
1425    /**
1426     * Get string lines
1427     *
1428     * @param  string        $string
1429     * @param  int           $fontSize
1430     * @param  int           $wrapLength
1431     * @param  Document\Font $fontObject
1432     * @return array
1433     */
1434    protected function getStringLines(string $string, int $fontSize, int $wrapLength, Document\Font $fontObject): array
1435    {
1436        $strings   = [];
1437        $curString = '';
1438        $words     = explode(' ', $string);
1439
1440        foreach ($words as $word) {
1441            $newString = ($curString != '') ? $curString . ' ' . $word : $word;
1442            if ($fontObject->getStringWidth($newString, $fontSize) <= $wrapLength) {
1443                $curString = $newString;
1444            } else {
1445                $strings[] = $curString;
1446                $curString = $word;
1447            }
1448        }
1449        if (!empty($curString)) {
1450            $strings[] = $curString;
1451        }
1452
1453        return $strings;
1454    }
1455
1456}