Issues (282)

lib/Document.php (2 issues)

1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * Document class.
6
 *
7
 * @package   YetiForcePDF
8
 *
9
 * @copyright YetiForce Sp. z o.o
10
 * @license   MIT
11
 * @author    Rafal Pospiech <[email protected]>
12
 * @author    Mariusz Krzaczkowski <[email protected]>
13
 */
14
15
namespace YetiForcePDF;
16
17
use Exception;
18
use YetiForcePDF\Html\Parser;
19
use YetiForcePDF\Layout\FooterBox;
20
use YetiForcePDF\Layout\HeaderBox;
21
use YetiForcePDF\Layout\WatermarkBox;
22
use YetiForcePDF\Objects\Meta;
23
use YetiForcePDF\Objects\PdfObject;
24
25
/**
26
 * Class Document.
27
 */
28
class Document
29
{
30
	/**
31
	 * Actual id auto incremented.
32
	 *
33
	 * @var int
34
	 */
35
	protected $actualId = 0;
36
37
	/**
38
	 * Main output buffer / content for pdf file.
39
	 *
40
	 * @var string
41
	 */
42
	protected $buffer = '';
43
44
	/**
45
	 * Main entry point - root element.
46
	 *
47
	 * @var \YetiForcePDF\Catalog
48
	 */
49
	protected $catalog;
50
51
	/**
52
	 * Pages dictionary.
53
	 *
54
	 * @var Pages
55
	 */
56
	protected $pagesObject;
57
58
	/**
59
	 * Current page object.
60
	 *
61
	 * @var Page
62
	 */
63
	protected $currentPageObject;
64
65
	/**
66
	 * @var string default page format
67
	 */
68
	protected $defaultFormat = 'A4';
69
70
	/**
71
	 * @var string default page orientation
72
	 */
73
	protected $defaultOrientation = \YetiForcePDF\Page::ORIENTATION_PORTRAIT;
74
75
	/**
76
	 * @var Page[] all pages in the document
77
	 */
78
	protected $pages = [];
79
80
	/**
81
	 * Default page margins.
82
	 *
83
	 * @var array
84
	 */
85
	protected $defaultMargins = [
86
		'left' => 40,
87
		'top' => 40,
88
		'right' => 40,
89
		'bottom' => 40,
90
	];
91
92
	/**
93
	 * All objects inside document.
94
	 *
95
	 * @var \YetiForcePDF\Objects\PdfObject[]
96
	 */
97
	protected $objects = [];
98
99
	/**
100
	 * @var Parser
101
	 */
102
	protected $htmlParser;
103
104
	/**
105
	 * Fonts data.
106
	 *
107
	 * @var array
108
	 */
109
	protected $fontsData = [];
110
111
	/**
112
	 * @var array
113
	 */
114
	protected $fontInstances = [];
115
116
	/**
117
	 * Actual font id.
118
	 *
119
	 * @var int
120
	 */
121
	protected $actualFontId = 0;
122
123
	/**
124
	 * Actual graphic state id.
125
	 *
126
	 * @var int
127
	 */
128
	protected $actualGraphicStateId = 0;
129
130
	/**
131
	 * @var bool
132
	 */
133
	protected $debugMode = false;
134
135
	/**
136
	 * @var HeaderBox|null
137
	 */
138
	protected $header;
139
140
	/**
141
	 * @var FooterBox|null
142
	 */
143
	protected $footer;
144
145
	/**
146
	 * @var WatermarkBox|null
147
	 */
148
	protected $watermark;
149
150
	/**
151
	 * @var Meta
152
	 */
153
	protected $meta;
154
155
	/**
156
	 * @var bool
157
	 */
158
	protected $parsed = false;
159
160
	/**
161
	 * Characters int values cache for fonts.
162
	 *
163
	 * @var array
164
	 */
165
	public $ordCache = [];
166
167
	/**
168
	 * Css selectors like classes ids.
169
	 *
170
	 * @var array
171
	 */
172
	protected $cssSelectors = [];
173
174
	/**
175
	 * Are we debugging?
176
	 *
177
	 * @return bool
178
	 */
179
	public function inDebugMode()
180
	{
181
		return $this->debugMode;
182
	}
183
184
	/**
185
	 * Is document already parsed?
186
	 *
187
	 * @return bool
188
	 */
189
	public function isParsed()
190
	{
191
		return $this->parsed;
192
	}
193
194
	/**
195
	 * Initialisation.
196
	 *
197
	 * @return $this
198
	 */
199
	public function init()
200
	{
201
		$this->catalog = (new \YetiForcePDF\Catalog())->setDocument($this)->init();
202
		$this->pagesObject = $this->catalog->addChild((new Pages())->setDocument($this)->init());
203
		$this->meta = (new Meta())->setDocument($this)->init();
204
205
		return $this;
206
	}
207
208
	/**
209
	 * Set default page format.
210
	 *
211
	 * @param string $defaultFormat
212
	 *
213
	 * @return $this
214
	 */
215
	public function setDefaultFormat(string $defaultFormat)
216
	{
217
		$this->defaultFormat = $defaultFormat;
218
		foreach ($this->pages as $page) {
219
			$page->setFormat($defaultFormat);
220
		}
221
222
		return $this;
223
	}
224
225
	/**
226
	 * Set default page orientation.
227
	 *
228
	 * @param string $defaultOrientation
229
	 *
230
	 * @return $this
231
	 */
232
	public function setDefaultOrientation(string $defaultOrientation)
233
	{
234
		$this->defaultOrientation = $defaultOrientation;
235
		foreach ($this->pages as $page) {
236
			$page->setOrientation($defaultOrientation);
237
		}
238
239
		return $this;
240
	}
241
242
	/**
243
	 * Set default page margins.
244
	 *
245
	 * @param float $left
246
	 * @param float $top
247
	 * @param float $right
248
	 * @param float $bottom
249
	 *
250
	 * @return $this
251
	 */
252
	public function setDefaultMargins(float $left, float $top, float $right, float $bottom)
253
	{
254
		$this->defaultMargins = [
255
			'left' => $left,
256
			'top' => $top,
257
			'right' => $right,
258
			'bottom' => $bottom,
259
			'horizontal' => $left + $right,
260
			'vertical' => $top + $bottom,
261
		];
262
		foreach ($this->pages as $page) {
263
			$page->setMargins($left, $top, $right, $bottom);
264
		}
265
266
		return $this;
267
	}
268
269
	/**
270
	 * Set default left margin.
271
	 *
272
	 * @param float $left
273
	 */
274
	public function setDefaultLeftMargin(float $left)
275
	{
276
		$this->defaultMargins['left'] = $left;
277
		foreach ($this->pages as $page) {
278
			$page->setMargins($this->defaultMargins['left'], $this->defaultMargins['top'], $this->defaultMargins['right'], $this->defaultMargins['bottom']);
279
		}
280
281
		return $this;
282
	}
283
284
	/**
285
	 * Set default top margin.
286
	 *
287
	 * @param float $left
288
	 * @param float $top
289
	 */
290
	public function setDefaultTopMargin(float $top)
291
	{
292
		$this->defaultMargins['top'] = $top;
293
		foreach ($this->pages as $page) {
294
			$page->setMargins($this->defaultMargins['left'], $this->defaultMargins['top'], $this->defaultMargins['right'], $this->defaultMargins['bottom']);
295
		}
296
297
		return $this;
298
	}
299
300
	/**
301
	 * Set default right margin.
302
	 *
303
	 * @param float $left
304
	 * @param float $right
305
	 */
306
	public function setDefaultRightMargin(float $right)
307
	{
308
		$this->defaultMargins['right'] = $right;
309
		foreach ($this->pages as $page) {
310
			$page->setMargins($this->defaultMargins['left'], $this->defaultMargins['top'], $this->defaultMargins['right'], $this->defaultMargins['bottom']);
311
		}
312
313
		return $this;
314
	}
315
316
	/**
317
	 * Set default bottom margin.
318
	 *
319
	 * @param float $left
320
	 * @param float $bottom
321
	 */
322
	public function setDefaultBottomMargin(float $bottom)
323
	{
324
		$this->defaultMargins['bottom'] = $bottom;
325
		foreach ($this->pages as $page) {
326
			$page->setMargins($this->defaultMargins['left'], $this->defaultMargins['top'], $this->defaultMargins['right'], $this->defaultMargins['bottom']);
327
		}
328
329
		return $this;
330
	}
331
332
	/**
333
	 * Get meta.
334
	 *
335
	 * @return Meta
336
	 */
337
	public function getMeta()
338
	{
339
		return $this->meta;
340
	}
341
342
	/**
343
	 * Get actual id for newly created object.
344
	 *
345
	 * @return int
346
	 */
347
	public function getActualId()
348
	{
349
		return ++$this->actualId;
350
	}
351
352
	/**
353
	 * Get actual id for newly created font.
354
	 *
355
	 * @return int
356
	 */
357
	public function getActualFontId(): int
358
	{
359
		return ++$this->actualFontId;
360
	}
361
362
	/**
363
	 * Get actual id for newly created graphic state.
364
	 *
365
	 * @return int
366
	 */
367
	public function getActualGraphicStateId(): int
368
	{
369
		return ++$this->actualGraphicStateId;
370
	}
371
372
	/**
373
	 * Set font.
374
	 *
375
	 * @param string                     $family
376
	 * @param string                     $weight
377
	 * @param string                     $style
378
	 * @param \YetiForcePDF\Objects\Font $fontInstance
379
	 *
380
	 * @return $this
381
	 */
382
	public function setFontInstance(string $family, string $weight, string $style, Objects\Font $fontInstance)
383
	{
384
		$this->fontInstances[$family][$weight][$style] = $fontInstance;
385
386
		return $this;
387
	}
388
389
	/**
390
	 * Get font instance.
391
	 *
392
	 * @param string $family
393
	 * @param string $weight
394
	 * @param string $style
395
	 *
396
	 * @return \YetiForcePDF\Objects\Font|null
397
	 */
398
	public function getFontInstance(string $family, string $weight, string $style)
399
	{
400
		if (!empty($this->fontInstances[$family][$weight][$style])) {
401
			return $this->fontInstances[$family][$weight][$style];
402
		}
403
404
		return null;
405
	}
406
407
	/**
408
	 * Get all font instances.
409
	 *
410
	 * @return \YetiForcePDF\Objects\Font[]
411
	 */
412
	public function getAllFontInstances()
413
	{
414
		$instances = [];
415
		foreach ($this->fontInstances as $weights) {
416
			foreach ($weights as $styles) {
417
				foreach ($styles as $instance) {
418
					$instances[] = $instance;
419
				}
420
			}
421
		}
422
423
		return $instances;
424
	}
425
426
	/**
427
	 * Set font information.
428
	 *
429
	 * @param string                 $family
430
	 * @param string                 $weight
431
	 * @param string                 $style
432
	 * @param \FontLib\TrueType\File $font
433
	 *
434
	 * @return $this
435
	 */
436
	public function setFontData(string $family, string $weight, string $style, \FontLib\TrueType\File $font)
437
	{
438
		if (empty($this->fontsData[$family][$weight][$style])) {
439
			$this->fontsData[$family][$weight][$style] = $font;
440
		}
441
442
		return $this;
443
	}
444
445
	/**
446
	 * Get font data.
447
	 *
448
	 * @param string $family
449
	 * @param string $weight
450
	 * @param string $style
451
	 *
452
	 * @return \FontLib\Font|null
453
	 */
454
	public function getFontData(string $family, string $weight, string $style)
455
	{
456
		if (!empty($this->fontsData[$family][$weight][$style])) {
457
			return $this->fontsData[$family][$weight][$style];
458
		}
459
460
		return null;
461
	}
462
463
	/**
464
	 * Add fonts from json.
465
	 *
466
	 * @param array $fonts
467
	 */
468
	public static function addFonts(array $fonts)
469
	{
470
		\YetiForcePDF\Objects\Font::loadFromArray($fonts);
471
	}
472
473
	/**
474
	 * Get pages object.
475
	 *
476
	 * @return \YetiForcePDF\Pages
477
	 */
478
	public function getPagesObject(): Pages
479
	{
480
		return $this->pagesObject;
481
	}
482
483
	/**
484
	 * Get default page format.
485
	 *
486
	 * @return string
487
	 */
488
	public function getDefaultFormat()
489
	{
490
		return $this->defaultFormat;
491
	}
492
493
	/**
494
	 * Get default page orientation.
495
	 *
496
	 * @return string
497
	 */
498
	public function getDefaultOrientation()
499
	{
500
		return $this->defaultOrientation;
501
	}
502
503
	/**
504
	 * Get default margins.
505
	 *
506
	 * @return array
507
	 */
508
	public function getDefaultMargins()
509
	{
510
		return $this->defaultMargins;
511
	}
512
513
	/**
514
	 * Set header.
515
	 *
516
	 * @param HeaderBox $header
517
	 *
518
	 * @return $this
519
	 */
520
	public function setHeader(HeaderBox $header)
521
	{
522
		if ($header->getParent()) {
523
			$header = $header->getParent()->removeChild($header);
524
		}
525
		$this->header = $header;
526
527
		return $this;
528
	}
529
530
	/**
531
	 * Get header.
532
	 *
533
	 * @return HeaderBox|null
534
	 */
535
	public function getHeader()
536
	{
537
		return $this->header;
538
	}
539
540
	/**
541
	 * Set watermark.
542
	 *
543
	 * @param WatermarkBox $watermark
544
	 *
545
	 * @return $this
546
	 */
547
	public function setWatermark(WatermarkBox $watermark)
548
	{
549
		if ($watermark->getParent()) {
550
			$watermark = $watermark->getParent()->removeChild($watermark);
551
		}
552
		$this->watermark = $watermark;
553
554
		return $this;
555
	}
556
557
	/**
558
	 * Get watermark.
559
	 *
560
	 * @return WatermarkBox|null
561
	 */
562
	public function getWatermark()
563
	{
564
		return $this->watermark;
565
	}
566
567
	/**
568
	 * Set footer.
569
	 *
570
	 * @param FooterBox $footer
571
	 *
572
	 * @return $this
573
	 */
574
	public function setFooter(FooterBox $footer)
575
	{
576
		if ($footer->getParent()) {
577
			$footer = $footer->getParent()->removeChild($footer);
578
		}
579
		$this->footer = $footer;
580
581
		return $this;
582
	}
583
584
	/**
585
	 * Get footer.
586
	 *
587
	 * @return FooterBox|null
588
	 */
589
	public function getFooter()
590
	{
591
		return $this->footer;
592
	}
593
594
	/**
595
	 * Add page to the document.
596
	 *
597
	 * @param string    $format      - optional format 'A4' for example
598
	 * @param string    $orientation - optional orientation 'P' or 'L'
599
	 * @param Page|null $page        - we can add cloned page or page from other document too
600
	 * @param Page|null $after       - add page after this page
601
	 *
602
	 * @return \YetiForcePDF\Page
603
	 */
604
	public function addPage(string $format = '', string $orientation = '', Page $page = null, Page $after = null): Page
605
	{
606
		if (null === $page) {
607
			$page = (new Page())->setDocument($this)->init();
608
		}
609
		if (!$format) {
610
			$format = $this->defaultFormat;
611
		}
612
		if (!$orientation) {
613
			$orientation = $this->defaultOrientation;
614
		}
615
		$page->setOrientation($orientation)->setFormat($format);
616
		$afterIndex = \count($this->pages);
617
		if ($after) {
618
			foreach ($this->pages as $afterIndex => $childPage) {
619
				if ($childPage === $after) {
620
					break;
621
				}
622
			}
623
			++$afterIndex;
624
		}
625
		$page->setPageNumber($afterIndex);
626
		if ($after) {
627
			$merge = array_splice($this->pages, $afterIndex);
628
			$this->pages[] = $page;
629
			$this->pages = array_merge($this->pages, $merge);
630
		} else {
631
			$this->pages[] = $page;
632
		}
633
		$this->currentPageObject = $page;
634
635
		return $page;
636
	}
637
638
	/**
639
	 * Get current page.
640
	 *
641
	 * @return Page
642
	 */
643
	public function getCurrentPage(): Page
644
	{
645
		return $this->currentPageObject;
646
	}
647
648
	/**
649
	 * Set current page.
650
	 *
651
	 * @param Page $page
652
	 */
653
	public function setCurrentPage(Page $page)
654
	{
655
		$this->currentPageObject = $page;
656
	}
657
658
	/**
659
	 * Get all pages.
660
	 *
661
	 * @param int|null $groupIndex
662
	 *
663
	 * @return Page[]
664
	 */
665
	public function getPages(int $groupIndex = null)
666
	{
667
		if ($groupIndex) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupIndex of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
668
			$pages = [];
669
			foreach ($this->pages as $page) {
670
				if ($page->getGroup() === $groupIndex) {
671
					$pages[] = $page;
672
				}
673
			}
674
675
			return $pages;
676
		}
677
678
		return $this->pages;
679
	}
680
681
	/**
682
	 * Fix page numbers.
683
	 *
684
	 * pages that are expanded by overflow will have the same unique id - cloned
685
	 * so they are in one group of pages - if some page is added with different unique id
686
	 * then it means that from now on pages are from other group and we should reset page numbers / count
687
	 *
688
	 * @return $this
689
	 */
690
	public function fixPageNumbers()
691
	{
692
		$groups = [];
693
		foreach ($this->getPages() as $page) {
694
			$groups[$page->getGroup()][] = $page;
695
		}
696
		foreach ($groups as $pages) {
697
			$pageCount = \count($pages);
698
			foreach ($pages as $index => $page) {
699
				$page->setPageNumber($index + 1);
700
				$page->setPageCount($pageCount);
701
			}
702
		}
703
704
		return $this;
705
	}
706
707
	/**
708
	 * Get document header.
709
	 *
710
	 * @return string
711
	 */
712
	protected function getDocumentHeader(): string
713
	{
714
		return "%PDF-1.4\n%âăĎÓ\n";
715
	}
716
717
	/**
718
	 * Get document footer.
719
	 *
720
	 * @return string
721
	 */
722
	protected function getDocumentFooter(): string
723
	{
724
		return '%%EOF';
725
	}
726
727
	/**
728
	 * Add object to document.
729
	 *
730
	 * @param PdfObject      $object
731
	 * @param PdfObject|null $after  - add after this element
732
	 *
733
	 * @return \YetiForcePDF\Document
734
	 */
735
	public function addObject(PdfObject $object, $after = null): self
736
	{
737
		$afterIndex = \count($this->objects);
738
		if ($after) {
739
			foreach ($this->objects as $afterIndex => $obj) {
740
				if ($after === $obj) {
741
					break;
742
				}
743
			}
744
			++$afterIndex;
745
		}
746
		if (!$after) {
747
			$this->objects[] = $object;
748
749
			return $this;
750
		}
751
		$merge = array_splice($this->objects, $afterIndex);
752
		foreach ($this->objects as $obj) {
753
			if ($obj->getId() === $object->getId()) {
754
				// id already exists (maybe we are merging with other doc) - generate new one
755
				$object->setId($this->getActualId());
756
757
				break;
758
			}
759
		}
760
		$this->objects[] = $object;
761
		$this->objects = array_merge($this->objects, $merge);
762
763
		return $this;
764
	}
765
766
	/**
767
	 * Remove object from document.
768
	 *
769
	 * @param \YetiForcePDF\Objects\PdfObject $object
770
	 *
771
	 * @return \YetiForcePDF\Document
772
	 */
773
	public function removeObject(PdfObject $object): self
774
	{
775
		$objects = [];
776
		foreach ($this->objects as $currentObject) {
777
			if ($currentObject !== $object) {
778
				$objects[] = $currentObject;
779
			}
780
		}
781
		$this->objects = $objects;
782
		unset($objects);
783
784
		return $this;
785
	}
786
787
	/**
788
	 * Load html string.
789
	 *
790
	 * @param string $html
791
	 * @param string $fromEncoding
792
	 *
793
	 * @return $this
794
	 * @throws Exception
795
	 */
796
	public function loadHtml(string $html, string $fromEncoding = 'UTF-8'): self
797
	{
798
		if ($fromEncoding === '') {
799
			throw new Exception('Encoding can not be empty');
800
		}
801
802
		$this->htmlParser = (new Parser())->setDocument($this)->init();
803
		$this->htmlParser->loadHtml($html, $fromEncoding);
804
805
		return $this;
806
	}
807
808
	/**
809
	 * Count objects.
810
	 *
811
	 * @param string $name - object name
812
	 *
813
	 * @return int
814
	 */
815
	public function countObjects(string $name = ''): int
816
	{
817
		if ('' === $name) {
818
			return \count($this->objects);
819
		}
820
		$typeCount = 0;
821
		foreach ($this->objects as $object) {
822
			if ($object->getName() === $name) {
823
				++$typeCount;
824
			}
825
		}
826
827
		return $typeCount;
828
	}
829
830
	/**
831
	 * Get objects.
832
	 *
833
	 * @param string $name - object name
834
	 *
835
	 * @return \YetiForcePDF\Objects\PdfObject[]
836
	 */
837
	public function getObjects(string $name = ''): array
838
	{
839
		if ('' === $name) {
840
			return $this->objects;
841
		}
842
		$objects = [];
843
		foreach ($this->objects as $object) {
844
			if ($object->getName() === $name) {
845
				$objects[] = $object;
846
			}
847
		}
848
849
		return $objects;
850
	}
851
852
	/**
853
	 * Filter text
854
	 * Filter the text, this is applied to all text just before being inserted into the pdf document
855
	 * it escapes the various things that need to be escaped, and so on.
856
	 *
857
	 * @param string $text
858
	 * @param string $encoding
859
	 * @param bool   $withParenthesis
860
	 * @param bool   $prependBom
861
	 *
862
	 * @return string
863
	 */
864
	public function filterText(string $text, string $encoding = 'UTF-16', bool $withParenthesis = true, bool $prependBom = false)
865
	{
866
		$text = preg_replace('/[\n\r\t\s]+/u', ' ', mb_convert_encoding($text, 'UTF-8'));
867
		$text = preg_replace('/^\s+|\s+$/u', '', $text);
868
		$text = preg_replace('/\s+/u', ' ', $text);
869
		$text = mb_convert_encoding($text, $encoding, mb_detect_encoding($text));
870
		$text = strtr($text, [')' => '\\)', '(' => '\\(', '\\' => '\\\\', \chr(13) => '\r']);
0 ignored issues
show
It seems like $text can also be of type array; however, parameter $str of strtr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

870
		$text = strtr(/** @scrutinizer ignore-type */ $text, [')' => '\\)', '(' => '\\(', '\\' => '\\\\', \chr(13) => '\r']);
Loading history...
871
		if ($prependBom) {
872
			$text = \chr(254) . \chr(255) . $text;
873
		}
874
		if ($withParenthesis) {
875
			return '(' . $text . ')';
876
		}
877
878
		return $text;
879
	}
880
881
	/**
882
	 * Parse html.
883
	 *
884
	 * @return $this
885
	 */
886
	public function parse()
887
	{
888
		if (!$this->isParsed()) {
889
			$this->htmlParser->parse();
890
			$this->parsed = true;
891
		}
892
893
		return $this;
894
	}
895
896
	/**
897
	 * Layout document content to pdf string.
898
	 *
899
	 * @return string
900
	 */
901
	public function render(): string
902
	{
903
		$xref = '';
904
		$this->buffer = '';
905
906
		$this->buffer .= $this->getDocumentHeader();
907
		$this->parse();
908
		$objectSize = 0;
909
910
		foreach ($this->objects as $object) {
911
			if (\in_array($object->getBasicType(), ['Dictionary', 'Stream', 'Array'])) {
912
				$xref .= sprintf("%010d 00000 n \n", \strlen($this->buffer));
913
				$this->buffer .= $object->render() . "\n";
914
				++$objectSize;
915
			}
916
		}
917
918
		$offset = \strlen($this->buffer);
919
		$this->buffer .= implode("\n", [
920
			'xref',
921
			'0 ' . $objectSize,
922
			'0000000000 65535 f ',
923
			$xref,
924
		]);
925
926
		$trailer = (new \YetiForcePDF\Objects\Trailer())
927
			->setDocument($this)->setRootObject($this->catalog)->setSize($objectSize);
928
929
		$this->buffer .= $trailer->render() . "\n";
930
		$this->buffer .= implode("\n", [
931
			'startxref',
932
			$offset,
933
			'',
934
		]);
935
		$this->buffer .= $this->getDocumentFooter();
936
		$this->removeObject($trailer);
937
938
		return $this->buffer;
939
	}
940
941
	/**
942
	 * Get css selector rules.
943
	 *
944
	 * @param string $selector
945
	 *
946
	 * @return array
947
	 */
948
	public function getCssSelectorRules(string $selector): array
949
	{
950
		$rules = [];
951
		foreach (explode(' ', $selector) as $className) {
952
			if ($className && isset($this->cssSelectors[$className])) {
953
				$rules = array_merge($rules, $this->cssSelectors[$className]);
954
			}
955
		}
956
957
		return $rules;
958
	}
959
960
	/**
961
	 * Get css selectors.
962
	 *
963
	 * @return array
964
	 */
965
	public function getCssSelectors()
966
	{
967
		return $this->cssSelectors;
968
	}
969
970
	/**
971
	 * Add css selector rules.
972
	 *
973
	 * @param string $selector .className or #id
974
	 * @param array  $rules
975
	 *
976
	 * @return $this
977
	 */
978
	public function addCssSelectorRules(string $selector, array $rules): self
979
	{
980
		if (isset($this->cssSelectors[$selector])) {
981
			$this->cssSelectors[$selector] = array_merge($this->cssSelectors[$selector], $rules);
982
		} else {
983
			$this->cssSelectors[$selector] = $rules;
984
		}
985
		return $this;
986
	}
987
}
988