PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.3
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.3
2.4.3 2.4.4 2.4.2 2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 All 60 releases
easy-invoice / vendor / dompdf / dompdf / src / Dompdf.php

Dompdf.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.3, at vendor/dompdf/dompdf/src/Dompdf.php

1,548 lines 42.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package dompdf
4 * @link https://github.com/dompdf/dompdf
5 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
6 */
7 namespace Dompdf;
8
9 use DOMDocument;
10 use DOMNode;
11 use Dompdf\Adapter\CPDF;
12 use DOMXPath;
13 use Dompdf\Frame\Factory;
14 use Dompdf\Frame\FrameTree;
15 use Dompdf\Image\Cache;
16 use Dompdf\Css\Stylesheet;
17 use Dompdf\Helpers;
18 use Masterminds\HTML5;
19
20 /**
21 * Dompdf - PHP5 HTML to PDF renderer
22 *
23 * Dompdf loads HTML and does its best to render it as a PDF. It gets its
24 * name from the new DomDocument PHP5 extension. Source HTML is first
25 * parsed by a DomDocument object. Dompdf takes the resulting DOM tree and
26 * attaches a {@link Frame} object to each node. {@link Frame} objects store
27 * positioning and layout information and each has a reference to a {@link
28 * Style} object.
29 *
30 * Style information is loaded and parsed (see {@link Stylesheet}) and is
31 * applied to the frames in the tree by using XPath. CSS selectors are
32 * converted into XPath queries, and the computed {@link Style} objects are
33 * applied to the {@link Frame}s.
34 *
35 * {@link Frame}s are then decorated (in the design pattern sense of the
36 * word) based on their CSS display property ({@link
37 * http://www.w3.org/TR/CSS21/visuren.html#propdef-display}).
38 * Frame_Decorators augment the basic {@link Frame} class by adding
39 * additional properties and methods specific to the particular type of
40 * {@link Frame}. For example, in the CSS layout model, block frames
41 * (display: block;) contain line boxes that are usually filled with text or
42 * other inline frames. The Block therefore adds a $lines
43 * property as well as methods to add {@link Frame}s to lines and to add
44 * additional lines. {@link Frame}s also are attached to specific
45 * AbstractPositioner and {@link AbstractFrameReflower} objects that contain the
46 * positioining and layout algorithm for a specific type of frame,
47 * respectively. This is an application of the Strategy pattern.
48 *
49 * Layout, or reflow, proceeds recursively (post-order) starting at the root
50 * of the document. Space constraints (containing block width & height) are
51 * pushed down, and resolved positions and sizes bubble up. Thus, every
52 * {@link Frame} in the document tree is traversed once (except for tables
53 * which use a two-pass layout algorithm). If you are interested in the
54 * details, see the reflow() method of the Reflower classes.
55 *
56 * Rendering is relatively straightforward once layout is complete. {@link
57 * Frame}s are rendered using an adapted {@link Cpdf} class, originally
58 * written by Wayne Munro, http://www.ros.co.nz/pdf/. (Some performance
59 * related changes have been made to the original {@link Cpdf} class, and
60 * the {@link Dompdf\Adapter\CPDF} class provides a simple, stateless interface to
61 * PDF generation.) PDFLib support has now also been added, via the {@link
62 * Dompdf\Adapter\PDFLib}.
63 *
64 *
65 * @package dompdf
66 */
67 class Dompdf
68 {
69 /**
70 * Version string for dompdf
71 *
72 * @var string
73 */
74 private $version = 'dompdf';
75
76 /**
77 * DomDocument representing the HTML document
78 *
79 * @var DOMDocument
80 */
81 private $dom;
82
83 /**
84 * FrameTree derived from the DOM tree
85 *
86 * @var FrameTree
87 */
88 private $tree;
89
90 /**
91 * Stylesheet for the document
92 *
93 * @var Stylesheet
94 */
95 private $css;
96
97 /**
98 * Actual PDF renderer
99 *
100 * @var Canvas
101 */
102 private $canvas;
103
104 /**
105 * Desired paper size ('letter', 'legal', 'A4', etc.)
106 *
107 * @var string|float[]
108 */
109 private $paperSize;
110
111 /**
112 * Paper orientation ('portrait' or 'landscape')
113 *
114 * @var string
115 */
116 private $paperOrientation = "portrait";
117
118 /**
119 * Callbacks on new page and new element
120 *
121 * @var array
122 */
123 private $callbacks = [];
124
125 /**
126 * Experimental caching capability
127 *
128 * @var string
129 */
130 private $cacheId;
131
132 /**
133 * Base hostname
134 *
135 * Used for relative paths/urls
136 * @var string
137 */
138 private $baseHost = "";
139
140 /**
141 * Absolute base path
142 *
143 * Used for relative paths/urls
144 * @var string
145 */
146 private $basePath = "";
147
148 /**
149 * Protocol used to request file (file://, http://, etc)
150 *
151 * @var string
152 */
153 private $protocol = "";
154
155 /**
156 * The system's locale
157 *
158 * @var string
159 */
160 private $systemLocale = null;
161
162 /**
163 * The system's mbstring internal encoding
164 *
165 * @var string
166 */
167 private $mbstringEncoding = null;
168
169 /**
170 * The system's PCRE JIT configuration
171 *
172 * @var string
173 */
174 private $pcreJit = null;
175
176 /**
177 * The default view of the PDF in the viewer
178 *
179 * @var string
180 */
181 private $defaultView = "Fit";
182
183 /**
184 * The default view options of the PDF in the viewer
185 *
186 * @var array
187 */
188 private $defaultViewOptions = [];
189
190 /**
191 * Tells whether the DOM document is in quirksmode (experimental)
192 *
193 * @var bool
194 */
195 private $quirksmode = false;
196
197 /**
198 * Local file extension whitelist
199 *
200 * File extensions supported by dompdf for local files.
201 *
202 * @var array
203 */
204 private $allowedLocalFileExtensions = ["htm", "html"];
205
206 /**
207 * @var array
208 */
209 private $messages = [];
210
211 /**
212 * @var Options
213 */
214 private $options;
215
216 /**
217 * @var FontMetrics
218 */
219 private $fontMetrics;
220
221 /**
222 * The list of built-in fonts
223 *
224 * @var array
225 * @deprecated
226 */
227 public static $native_fonts = [
228 "courier", "courier-bold", "courier-oblique", "courier-boldoblique",
229 "helvetica", "helvetica-bold", "helvetica-oblique", "helvetica-boldoblique",
230 "times-roman", "times-bold", "times-italic", "times-bolditalic",
231 "symbol", "zapfdinbats"
232 ];
233
234 /**
235 * The list of built-in fonts
236 *
237 * @var array
238 */
239 public static $nativeFonts = [
240 "courier", "courier-bold", "courier-oblique", "courier-boldoblique",
241 "helvetica", "helvetica-bold", "helvetica-oblique", "helvetica-boldoblique",
242 "times-roman", "times-bold", "times-italic", "times-bolditalic",
243 "symbol", "zapfdinbats"
244 ];
245
246 /**
247 * Class constructor
248 *
249 * @param Options|array|null $options
250 */
251 public function __construct($options = null)
252 {
253 if (isset($options) && $options instanceof Options) {
254 $this->setOptions($options);
255 } elseif (is_array($options)) {
256 $this->setOptions(new Options($options));
257 } else {
258 $this->setOptions(new Options());
259 }
260
261 $versionFile = realpath(__DIR__ . '/../VERSION');
262 if (($version = file_get_contents($versionFile)) !== false) {
263 $version = trim($version);
264 if ($version !== '$Format:<%h>$') {
265 $this->version = sprintf('dompdf %s', $version);
266 }
267 }
268
269 $this->setPhpConfig();
270
271 $this->paperSize = $this->options->getDefaultPaperSize();
272 $this->paperOrientation = $this->options->getDefaultPaperOrientation();
273
274 $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation);
275 $this->fontMetrics = new FontMetrics($this->canvas, $this->options);
276 $this->css = new Stylesheet($this);
277
278 $this->restorePhpConfig();
279 }
280
281 /**
282 * Save the system's existing locale, PCRE JIT, and MBString encoding
283 * configuration and configure the system for Dompdf processing
284 */
285 private function setPhpConfig()
286 {
287 if (sprintf('%.1f', 1.0) !== '1.0') {
288 $this->systemLocale = setlocale(LC_NUMERIC, "0");
289 setlocale(LC_NUMERIC, "C");
290 }
291
292 if (function_exists('ini_get') && function_exists('ini_set')) {
293 $this->pcreJit = @ini_get('pcre.jit');
294 @ini_set('pcre.jit', '0');
295 }
296
297 $this->mbstringEncoding = mb_internal_encoding();
298 mb_internal_encoding('UTF-8');
299 }
300
301 /**
302 * Restore the system's locale configuration
303 */
304 private function restorePhpConfig()
305 {
306 if ($this->systemLocale !== null) {
307 setlocale(LC_NUMERIC, $this->systemLocale);
308 $this->systemLocale = null;
309 }
310
311 if (function_exists('ini_get') && function_exists('ini_set')) {
312 if ($this->pcreJit !== null) {
313 @ini_set('pcre.jit', $this->pcreJit);
314 $this->pcreJit = null;
315 }
316 }
317
318 if ($this->mbstringEncoding !== null) {
319 mb_internal_encoding($this->mbstringEncoding);
320 $this->mbstringEncoding = null;
321 }
322 }
323
324 /**
325 * @param $file
326 * @deprecated
327 */
328 public function load_html_file($file)
329 {
330 $this->loadHtmlFile($file);
331 }
332
333 /**
334 * Loads an HTML file.
335 *
336 * If no encoding is given or set via `Content-Type` header, the document
337 * encoding specified via `<meta>` tag is used. An existing Unicode BOM
338 * always takes precedence.
339 *
340 * Parse errors are stored in the global array `$_dompdf_warnings`.
341 *
342 * @param string $file A filename or URL to load.
343 * @param string|null $encoding Encoding of the file.
344 */
345 public function loadHtmlFile($file, $encoding = null)
346 {
347 $this->setPhpConfig();
348
349 if (!$this->protocol && !$this->baseHost && !$this->basePath) {
350 [$this->protocol, $this->baseHost, $this->basePath] = Helpers::explode_url($file);
351 }
352 $protocol = strtolower($this->protocol);
353 $uri = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $file, $this->options->getChroot());
354
355 $allowed_protocols = $this->options->getAllowedProtocols();
356 if (!array_key_exists($protocol, $allowed_protocols)) {
357 throw new Exception("Permission denied on $file. The communication protocol is not supported.");
358 }
359
360 if ($protocol === "file://") {
361 $ext = strtolower(pathinfo($uri, PATHINFO_EXTENSION));
362 if (!in_array($ext, $this->allowedLocalFileExtensions)) {
363 throw new Exception("Permission denied on $file: The file extension is forbidden.");
364 }
365 }
366
367 foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
368 [$result, $message] = $rule($uri);
369 if (!$result) {
370 throw new Exception("Error loading $file: $message");
371 }
372 }
373
374 [$contents, $http_response_header] = Helpers::getFileContent($uri, $this->options->getHttpContext());
375 if ($contents === null) {
376 throw new Exception("File '$file' not found.");
377 }
378
379 // See http://the-stickman.com/web-development/php/getting-http-response-headers-when-using-file_get_contents/
380 if (isset($http_response_header)) {
381 foreach ($http_response_header as $_header) {
382 if (preg_match("@Content-Type:\s*[\w/]+;\s*?charset=([^\s]+)@i", $_header, $matches)) {
383 $encoding = strtoupper($matches[1]);
384 break;
385 }
386 }
387 }
388
389 $this->restorePhpConfig();
390
391 $this->loadHtml($contents, $encoding);
392 }
393
394 /**
395 * @param string $str
396 * @param string $encoding
397 * @deprecated
398 */
399 public function load_html($str, $encoding = null)
400 {
401 $this->loadHtml($str, $encoding);
402 }
403
404 /**
405 * @param DOMDocument $doc
406 * @param bool $quirksmode
407 */
408 public function loadDOM($doc, $quirksmode = false)
409 {
410 // Remove #text children nodes in nodes that shouldn't have
411 $tag_names = ["html", "head", "table", "tbody", "thead", "tfoot", "tr"];
412 foreach ($tag_names as $tag_name) {
413 $nodes = $doc->getElementsByTagName($tag_name);
414
415 foreach ($nodes as $node) {
416 self::removeTextNodes($node);
417 }
418 }
419
420 $this->dom = $doc;
421 $this->quirksmode = $quirksmode;
422 $this->tree = new FrameTree($this->dom);
423 }
424
425 /**
426 * Loads an HTML document from a string.
427 *
428 * If no encoding is given, the document encoding specified via `<meta>`
429 * tag is used. An existing Unicode BOM always takes precedence.
430 *
431 * Parse errors are stored in the global array `$_dompdf_warnings`.
432 *
433 * @param string $str The HTML to load.
434 * @param string|null $encoding Encoding of the string.
435 */
436 public function loadHtml($str, $encoding = null)
437 {
438 $this->setPhpConfig();
439
440 // Detect Unicode via BOM, taking precedence over the given encoding.
441 // Remove the mark, as it is treated as document text by DOMDocument.
442 // http://us2.php.net/manual/en/function.mb-detect-encoding.php#91051
443 if (strncmp($str, "\xFE\xFF", 2) === 0) {
444 $str = substr($str, 2);
445 $encoding = "UTF-16BE";
446 } elseif (strncmp($str, "\xFF\xFE", 2) === 0) {
447 $str = substr($str, 2);
448 $encoding = "UTF-16LE";
449 } elseif (strncmp($str, "\xEF\xBB\xBF", 3) === 0) {
450 $str = substr($str, 3);
451 $encoding = "UTF-8";
452 }
453
454 // Convert document using the given encoding
455 $encodingGiven = $encoding !== null && $encoding !== "";
456
457 if ($encodingGiven && !in_array(strtoupper($encoding), ["UTF-8", "UTF8"], true)) {
458 $converted = mb_convert_encoding($str, "UTF-8", $encoding);
459
460 if ($converted !== false) {
461 $str = $converted;
462 }
463 }
464
465 // Parse document encoding from `<meta>` tag ...
466 $charset = "(?<charset>[a-z0-9\-]+)";
467 $contentType = "http-equiv\s*=\s* ([\"']?)\s* Content-Type";
468 $contentStart = "content\s*=\s* ([\"']?)\s* [\w\/]+ \s*;\s* charset\s*=\s*";
469 $metaTags = [
470 "/<meta \s[^>]* $contentType \s*\g1\s* $contentStart $charset \s*\g2 [^>]*>/isx", // <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
471 "/<meta \s[^>]* $contentStart $charset \s*\g1\s* $contentType \s*\g3 [^>]*>/isx", // <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
472 "/<meta \s[^>]* charset\s*=\s* ([\"']?)\s* $charset \s*\g1 [^>]*>/isx", // <meta charset="UTF-8">
473 ];
474
475 foreach ($metaTags as $pattern) {
476 if (preg_match($pattern, $str, $matches, PREG_OFFSET_CAPTURE)) {
477 [$documentEncoding, $offset] = $matches["charset"];
478 break;
479 }
480 }
481
482 // ... and replace it with UTF-8; add a corresponding `<meta>` tag if
483 // missing. This is to ensure that `DOMDocument` handles the document
484 // encoding properly, as it will mess up the encoding if the charset
485 // declaration is missing or different from the actual encoding
486 if (isset($documentEncoding) && isset($offset)) {
487 if (!in_array(strtoupper($documentEncoding), ["UTF-8", "UTF8"], true)) {
488 $str = substr($str, 0, $offset) . "UTF-8" . substr($str, $offset + strlen($documentEncoding));
489 }
490 } elseif (($headPos = stripos($str, "<head>")) !== false) {
491 $str = substr($str, 0, $headPos + 6) . '<meta charset="UTF-8">' . substr($str, $headPos + 6);
492 } else {
493 $str = '<meta charset="UTF-8">' . $str;
494 }
495
496 // If no encoding was passed, use the document encoding, falling back to
497 // auto-detection
498 $fallbackEncoding = $documentEncoding ?? "auto";
499
500 if (!$encodingGiven && !in_array(strtoupper($fallbackEncoding), ["UTF-8", "UTF8"], true)) {
501 $converted = mb_convert_encoding($str, "UTF-8", $fallbackEncoding);
502
503 if ($converted !== false) {
504 $str = $converted;
505 }
506 }
507
508 // Store parsing warnings as messages
509 set_error_handler([Helpers::class, "record_warnings"]);
510
511 try {
512 // @todo Take the quirksmode into account
513 // https://quirks.spec.whatwg.org/
514 // http://hsivonen.iki.fi/doctype/
515 $quirksmode = false;
516
517 $html5 = new HTML5(["encoding" => "UTF-8", "disable_html_ns" => true]);
518 $dom = $html5->loadHTML($str);
519
520 // extra step to normalize the HTML document structure
521 // see Masterminds/html5-php#166
522 $doc = new DOMDocument("1.0", "UTF-8");
523 $doc->preserveWhiteSpace = true;
524 $doc->loadHTML($html5->saveHTML($dom), LIBXML_NOWARNING | LIBXML_NOERROR);
525
526 $this->loadDOM($doc, $quirksmode);
527 } finally {
528 restore_error_handler();
529 $this->restorePhpConfig();
530 }
531 }
532
533 /**
534 * @param DOMNode $node
535 * @deprecated
536 */
537 public static function remove_text_nodes(DOMNode $node)
538 {
539 self::removeTextNodes($node);
540 }
541
542 /**
543 * @param DOMNode $node
544 */
545 public static function removeTextNodes(DOMNode $node)
546 {
547 $children = [];
548 for ($i = 0; $i < $node->childNodes->length; $i++) {
549 $child = $node->childNodes->item($i);
550 if ($child->nodeName === "#text") {
551 $children[] = $child;
552 }
553 }
554
555 foreach ($children as $child) {
556 $node->removeChild($child);
557 }
558 }
559
560 /**
561 * Builds the {@link FrameTree}, loads any CSS and applies the styles to
562 * the {@link FrameTree}
563 */
564 private function processHtml()
565 {
566 $this->tree->build_tree();
567
568 $this->css->load_css_file($this->css->getDefaultStylesheet(), Stylesheet::ORIG_UA);
569
570 $acceptedmedia = Stylesheet::$ACCEPTED_GENERIC_MEDIA_TYPES;
571 $acceptedmedia[] = $this->options->getDefaultMediaType();
572
573 // <base href="" />
574 /** @var \DOMElement|null */
575 $baseNode = $this->dom->getElementsByTagName("base")->item(0);
576 $baseHref = $baseNode ? $baseNode->getAttribute("href") : "";
577 if ($baseHref !== "") {
578 [$this->protocol, $this->baseHost, $this->basePath] = Helpers::explode_url($baseHref);
579 }
580
581 // Set the base path of the Stylesheet to that of the file being processed
582 $this->css->set_protocol($this->protocol);
583 $this->css->set_host($this->baseHost);
584 $this->css->set_base_path($this->basePath);
585
586 // Get all the stylesheets so that they are processed in document order
587 $xpath = new DOMXPath($this->dom);
588 $stylesheets = $xpath->query("//*[name() = 'link' or name() = 'style']");
589
590 /** @var \DOMElement $tag */
591 foreach ($stylesheets as $tag) {
592 switch (strtolower($tag->nodeName)) {
593 // load <link rel="STYLESHEET" ... /> tags
594 case "link":
595 if (
596 (stripos($tag->getAttribute("rel"), "stylesheet") !== false // may be "appendix stylesheet"
597 || mb_strtolower($tag->getAttribute("type")) === "text/css")
598 && stripos($tag->getAttribute("rel"), "alternate") === false // don't load "alternate stylesheet"
599 ) {
600 //Check if the css file is for an accepted media type
601 //media not given then always valid
602 $formedialist = preg_split("/[\s\n,]/", $tag->getAttribute("media"), -1, PREG_SPLIT_NO_EMPTY);
603 if (count($formedialist) > 0) {
604 $accept = false;
605 foreach ($formedialist as $type) {
606 if (in_array(mb_strtolower(trim($type)), $acceptedmedia)) {
607 $accept = true;
608 break;
609 }
610 }
611
612 if (!$accept) {
613 //found at least one mediatype, but none of the accepted ones
614 //Skip this css file.
615 break;
616 }
617 }
618
619 $url = $tag->getAttribute("href");
620 $url = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $url, $this->options->getChroot());
621
622 if ($url !== null) {
623 $this->css->load_css_file($url, Stylesheet::ORIG_AUTHOR);
624 }
625 }
626 break;
627
628 // load <style> tags
629 case "style":
630 // Accept all <style> tags by default (note this is contrary to W3C
631 // HTML 4.0 spec:
632 // http://www.w3.org/TR/REC-html40/present/styles.html#adef-media
633 // which states that the default media type is 'screen'
634 if ($tag->hasAttributes() &&
635 ($media = $tag->getAttribute("media")) &&
636 !in_array($media, $acceptedmedia)
637 ) {
638 break;
639 }
640
641 $css = "";
642 if ($tag->hasChildNodes()) {
643 $child = $tag->firstChild;
644 while ($child) {
645 $css .= $child->nodeValue; // Handle <style><!-- blah --></style>
646 $child = $child->nextSibling;
647 }
648 } else {
649 $css = $tag->nodeValue;
650 }
651
652 // Set the base path of the Stylesheet to that of the file being processed
653 $this->css->set_protocol($this->protocol);
654 $this->css->set_host($this->baseHost);
655 $this->css->set_base_path($this->basePath);
656
657 $this->css->load_css($css, Stylesheet::ORIG_AUTHOR);
658 break;
659 }
660
661 // Set the base path of the Stylesheet to that of the file being processed
662 $this->css->set_protocol($this->protocol);
663 $this->css->set_host($this->baseHost);
664 $this->css->set_base_path($this->basePath);
665 }
666 }
667
668 /**
669 * @param string $cacheId
670 * @deprecated
671 */
672 public function enable_caching($cacheId)
673 {
674 $this->enableCaching($cacheId);
675 }
676
677 /**
678 * Enable experimental caching capability
679 *
680 * @param string $cacheId
681 */
682 public function enableCaching($cacheId)
683 {
684 $this->cacheId = $cacheId;
685 }
686
687 /**
688 * @param string $value
689 * @return bool
690 * @deprecated
691 */
692 public function parse_default_view($value)
693 {
694 return $this->parseDefaultView($value);
695 }
696
697 /**
698 * @param string $value
699 * @return bool
700 */
701 public function parseDefaultView($value)
702 {
703 $valid = ["XYZ", "Fit", "FitH", "FitV", "FitR", "FitB", "FitBH", "FitBV"];
704
705 $options = preg_split("/\s*,\s*/", trim($value));
706 $defaultView = array_shift($options);
707
708 if (!in_array($defaultView, $valid)) {
709 return false;
710 }
711
712 $this->setDefaultView($defaultView, $options);
713 return true;
714 }
715
716 /**
717 * Renders the HTML to PDF
718 */
719 public function render()
720 {
721 $this->setPhpConfig();
722
723 $logOutputFile = $this->options->getLogOutputFile();
724 if ($logOutputFile) {
725 if (!file_exists($logOutputFile) && is_writable(dirname($logOutputFile))) {
726 touch($logOutputFile);
727 }
728
729 $startTime = microtime(true);
730 if (is_writable($logOutputFile)) {
731 ob_start();
732 }
733 }
734
735 $this->processHtml();
736
737 $this->css->apply_styles($this->tree);
738
739 // @page style rules : size, margins
740 $pageStyles = $this->css->get_page_styles();
741 $basePageStyle = $pageStyles["base"];
742 unset($pageStyles["base"]);
743
744 foreach ($pageStyles as $pageStyle) {
745 $pageStyle->inherit($basePageStyle);
746 }
747
748 // Set paper size if defined via CSS
749 if (is_array($basePageStyle->size)) {
750 // Orientation is already applied when reading the computed CSS
751 // `size` value. The `Canvas` back ends, however, unconditionally
752 // swap with an orientation of `landscape` and leave the defined
753 // size as-is with `portrait`; so passing `portrait` as orientation
754 // here (via the default value) is correct
755 [$width, $height] = $basePageStyle->size;
756 $this->setPaper([0, 0, $width, $height]);
757 }
758
759 // Create a new canvas instance if the current one does not match the
760 // desired paper size
761 $canvasWidth = $this->canvas->get_width();
762 $canvasHeight = $this->canvas->get_height();
763 $size = $this->getPaperSize();
764
765 if (
766 \Dompdf\Helpers::lengthEqual($canvasWidth, $size[2]) === false ||
767 \Dompdf\Helpers::lengthEqual($canvasHeight, $size[3]) === false
768 ) {
769 $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation);
770 $this->fontMetrics->setCanvas($this->canvas);
771 }
772
773 $canvas = $this->canvas;
774
775 LineBox::reset_float_reflow_limit(); // FIXME smelly hack
776
777 $root_frame = $this->tree->get_root();
778 $root = Factory::decorate_root($root_frame, $this);
779 foreach ($this->tree as $frame) {
780 if ($frame === $root_frame) {
781 continue;
782 }
783 Factory::decorate_frame($frame, $this, $root);
784 }
785
786 // Add meta information
787 $title = $this->dom->getElementsByTagName("title");
788 if ($title->length) {
789 $canvas->add_info("Title", trim($title->item(0)->nodeValue));
790 }
791
792 $metas = $this->dom->getElementsByTagName("meta");
793 $labels = [
794 "author" => "Author",
795 "keywords" => "Keywords",
796 "description" => "Subject",
797 ];
798 /** @var \DOMElement $meta */
799 foreach ($metas as $meta) {
800 $name = mb_strtolower($meta->getAttribute("name"));
801 $value = trim($meta->getAttribute("content"));
802
803 if (isset($labels[$name])) {
804 $canvas->add_info($labels[$name], $value);
805 continue;
806 }
807
808 if ($name === "dompdf.view" && $this->parseDefaultView($value)) {
809 $canvas->set_default_view($this->defaultView, $this->defaultViewOptions);
810 }
811 }
812
813 $root->set_containing_block(0, 0, $canvas->get_width(), $canvas->get_height());
814 $root->set_renderer(new Renderer($this));
815
816 // This is where the magic happens:
817 $root->reflow();
818
819 if (isset($this->callbacks["end_document"])) {
820 $fs = $this->callbacks["end_document"];
821
822 foreach ($fs as $f) {
823 $canvas->page_script($f);
824 }
825 }
826
827 // Clean up cached images
828 if (!$this->options->getDebugKeepTemp()) {
829 Cache::clear($this->options->getDebugPng());
830 }
831
832 global $_dompdf_warnings, $_dompdf_show_warnings;
833 if ($_dompdf_show_warnings && isset($_dompdf_warnings)) {
834 echo '<b>Dompdf Warnings</b><br><pre>';
835 foreach ($_dompdf_warnings as $msg) {
836 echo $msg . "\n";
837 }
838
839 if ($canvas instanceof CPDF) {
840 echo $canvas->get_cpdf()->messages;
841 }
842 echo '</pre>';
843 flush();
844 }
845
846 if ($logOutputFile && is_writable($logOutputFile)) {
847 $this->writeLog($logOutputFile, $startTime);
848 ob_end_clean();
849 }
850
851 $this->restorePhpConfig();
852 }
853
854 /**
855 * Writes the output buffer in the log file
856 *
857 * @param string $logOutputFile
858 * @param float $startTime
859 */
860 private function writeLog(string $logOutputFile, float $startTime): void
861 {
862 $frames = Frame::$ID_COUNTER;
863 $memory = memory_get_peak_usage(true) / 1024;
864 $time = (microtime(true) - $startTime) * 1000;
865
866 $out = sprintf(
867 "<span style='color: #000' title='Frames'>%6d</span>" .
868 "<span style='color: #009' title='Memory'>%10.2f KB</span>" .
869 "<span style='color: #900' title='Time'>%10.2f ms</span>" .
870 "<span title='Quirksmode'> " .
871 ($this->quirksmode ? "<span style='color: #d00'> ON</span>" : "<span style='color: #0d0'>OFF</span>") .
872 "</span><br />", $frames, $memory, $time);
873
874 $out .= ob_get_contents();
875 ob_clean();
876
877 file_put_contents($logOutputFile, $out);
878 }
879
880 /**
881 * Add meta information to the PDF after rendering.
882 *
883 * @deprecated
884 */
885 public function add_info($label, $value)
886 {
887 $this->addInfo($label, $value);
888 }
889
890 /**
891 * Add meta information to the PDF after rendering.
892 *
893 * @param string $label Label of the value (Creator, Producer, etc.)
894 * @param string $value The text to set
895 */
896 public function addInfo(string $label, string $value): void
897 {
898 $this->canvas->add_info($label, $value);
899 }
900
901 /**
902 * Streams the PDF to the client.
903 *
904 * The file will open a download dialog by default. The options
905 * parameter controls the output. Accepted options (array keys) are:
906 *
907 * 'compress' = > 1 (=default) or 0:
908 * Apply content stream compression
909 *
910 * 'Attachment' => 1 (=default) or 0:
911 * Set the 'Content-Disposition:' HTTP header to 'attachment'
912 * (thereby causing the browser to open a download dialog)
913 *
914 * @param string $filename the name of the streamed file
915 * @param array $options header options (see above)
916 */
917 public function stream($filename = "document.pdf", $options = [])
918 {
919 $this->setPhpConfig();
920
921 $this->canvas->stream($filename, $options);
922
923 $this->restorePhpConfig();
924 }
925
926 /**
927 * Returns the PDF as a string.
928 *
929 * The options parameter controls the output. Accepted options are:
930 *
931 * 'compress' = > 1 or 0 - apply content stream compression, this is
932 * on (1) by default
933 *
934 * @param array $options options (see above)
935 *
936 * @return string
937 */
938 public function output($options = [])
939 {
940 $this->setPhpConfig();
941
942 $output = $this->canvas->output($options);
943
944 $this->restorePhpConfig();
945
946 return $output;
947 }
948
949 /**
950 * @return string
951 * @deprecated
952 */
953 public function output_html()
954 {
955 return $this->outputHtml();
956 }
957
958 /**
959 * Returns the underlying HTML document as a string
960 *
961 * @return string
962 */
963 public function outputHtml()
964 {
965 return $this->dom->saveHTML();
966 }
967
968 /**
969 * Get the dompdf option value
970 *
971 * @param string $key
972 * @return mixed
973 * @deprecated
974 */
975 public function get_option($key)
976 {
977 return $this->options->get($key);
978 }
979
980 /**
981 * @param string $key
982 * @param mixed $value
983 * @return $this
984 * @deprecated
985 */
986 public function set_option($key, $value)
987 {
988 $new_options = clone $this->options;
989 $new_options->set($key, $value);
990 $this->setOptions($new_options);
991 return $this;
992 }
993
994 /**
995 * @param array $options
996 * @return $this
997 * @deprecated
998 */
999 public function set_options(array $options)
1000 {
1001 $new_options = clone $this->options;
1002 $new_options->set($options);
1003 $this->setOptions($new_options);
1004 return $this;
1005 }
1006
1007 /**
1008 * @param string $size
1009 * @param string $orientation
1010 * @deprecated
1011 */
1012 public function set_paper($size, $orientation = "portrait")
1013 {
1014 $this->setPaper($size, $orientation);
1015 }
1016
1017 /**
1018 * Sets the paper size & orientation
1019 *
1020 * @param string|float[] $size 'letter', 'legal', 'A4', etc. {@link Dompdf\Adapter\CPDF::$PAPER_SIZES}
1021 * @param string $orientation 'portrait' or 'landscape'
1022 * @return $this
1023 */
1024 public function setPaper($size, string $orientation = "portrait"): self
1025 {
1026 $current_size = $this->getPaperSize();
1027 $this->paperSize = $size;
1028 $this->paperOrientation = $orientation;
1029 $new_size = $this->getPaperSize();
1030 if (
1031 \Dompdf\Helpers::lengthEqual($current_size[2], $new_size[2]) === false ||
1032 \Dompdf\Helpers::lengthEqual($current_size[3], $new_size[3]) === false
1033 ) {
1034 $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation);
1035 }
1036 return $this;
1037 }
1038
1039 /**
1040 * Gets the paper size
1041 *
1042 * @return float[] A four-element float array
1043 */
1044 public function getPaperSize(): array
1045 {
1046 $paper = $this->paperSize;
1047 $orientation = $this->paperOrientation;
1048
1049 if (is_array($paper)) {
1050 $size = array_map("floatval", $paper);
1051 } else {
1052 $paper = strtolower($paper);
1053 $size = CPDF::$PAPER_SIZES[$paper] ?? CPDF::$PAPER_SIZES["letter"];
1054 }
1055
1056 if (strtolower($orientation) === "landscape") {
1057 [$size[2], $size[3]] = [$size[3], $size[2]];
1058 }
1059
1060 return $size;
1061 }
1062
1063 /**
1064 * Gets the paper orientation
1065 *
1066 * @return string Either "portrait" or "landscape"
1067 */
1068 public function getPaperOrientation(): string
1069 {
1070 return $this->paperOrientation;
1071 }
1072
1073 /**
1074 * @param FrameTree $tree
1075 * @return $this
1076 */
1077 public function setTree(FrameTree $tree)
1078 {
1079 $this->tree = $tree;
1080 return $this;
1081 }
1082
1083 /**
1084 * @return FrameTree
1085 * @deprecated
1086 */
1087 public function get_tree()
1088 {
1089 return $this->getTree();
1090 }
1091
1092 /**
1093 * Returns the underlying {@link FrameTree} object
1094 *
1095 * @return FrameTree
1096 */
1097 public function getTree()
1098 {
1099 return $this->tree;
1100 }
1101
1102 /**
1103 * @param string $protocol
1104 * @return $this
1105 * @deprecated
1106 */
1107 public function set_protocol($protocol)
1108 {
1109 return $this->setProtocol($protocol);
1110 }
1111
1112 /**
1113 * Sets the protocol to use
1114 * FIXME validate these
1115 *
1116 * @param string $protocol
1117 * @return $this
1118 */
1119 public function setProtocol(string $protocol)
1120 {
1121 $this->protocol = $protocol;
1122 return $this;
1123 }
1124
1125 /**
1126 * @return string
1127 * @deprecated
1128 */
1129 public function get_protocol()
1130 {
1131 return $this->getProtocol();
1132 }
1133
1134 /**
1135 * Returns the protocol in use
1136 *
1137 * @return string
1138 */
1139 public function getProtocol()
1140 {
1141 return $this->protocol;
1142 }
1143
1144 /**
1145 * @param string $host
1146 * @deprecated
1147 */
1148 public function set_host($host)
1149 {
1150 $this->setBaseHost($host);
1151 }
1152
1153 /**
1154 * Sets the base hostname
1155 *
1156 * @param string $baseHost
1157 * @return $this
1158 */
1159 public function setBaseHost(string $baseHost)
1160 {
1161 $this->baseHost = $baseHost;
1162 return $this;
1163 }
1164
1165 /**
1166 * @return string
1167 * @deprecated
1168 */
1169 public function get_host()
1170 {
1171 return $this->getBaseHost();
1172 }
1173
1174 /**
1175 * Returns the base hostname
1176 *
1177 * @return string
1178 */
1179 public function getBaseHost()
1180 {
1181 return $this->baseHost;
1182 }
1183
1184 /**
1185 * Sets the base path
1186 *
1187 * @param string $path
1188 * @deprecated
1189 */
1190 public function set_base_path($path)
1191 {
1192 $this->setBasePath($path);
1193 }
1194
1195 /**
1196 * Sets the base path
1197 *
1198 * @param string $basePath
1199 * @return $this
1200 */
1201 public function setBasePath(string $basePath)
1202 {
1203 $this->basePath = $basePath;
1204 return $this;
1205 }
1206
1207 /**
1208 * @return string
1209 * @deprecated
1210 */
1211 public function get_base_path()
1212 {
1213 return $this->getBasePath();
1214 }
1215
1216 /**
1217 * Returns the base path
1218 *
1219 * @return string
1220 */
1221 public function getBasePath()
1222 {
1223 return $this->basePath;
1224 }
1225
1226 /**
1227 * @param string $default_view The default document view
1228 * @param array $options The view's options
1229 * @return $this
1230 * @deprecated
1231 */
1232 public function set_default_view($default_view, $options)
1233 {
1234 return $this->setDefaultView($default_view, $options);
1235 }
1236
1237 /**
1238 * Sets the default view
1239 *
1240 * @param string $defaultView The default document view
1241 * @param array $options The view's options
1242 * @return $this
1243 */
1244 public function setDefaultView($defaultView, $options)
1245 {
1246 $this->defaultView = $defaultView;
1247 $this->defaultViewOptions = $options;
1248 return $this;
1249 }
1250
1251 /**
1252 * @param resource $http_context
1253 * @return $this
1254 * @deprecated
1255 */
1256 public function set_http_context($http_context)
1257 {
1258 return $this->setHttpContext($http_context);
1259 }
1260
1261 /**
1262 * Sets the HTTP context
1263 *
1264 * @param resource|array $httpContext
1265 * @return $this
1266 */
1267 public function setHttpContext($httpContext)
1268 {
1269 $this->options->setHttpContext($httpContext);
1270 return $this;
1271 }
1272
1273 /**
1274 * @return resource
1275 * @deprecated
1276 */
1277 public function get_http_context()
1278 {
1279 return $this->getHttpContext();
1280 }
1281
1282 /**
1283 * Returns the HTTP context
1284 *
1285 * @return resource
1286 */
1287 public function getHttpContext()
1288 {
1289 return $this->options->getHttpContext();
1290 }
1291
1292 /**
1293 * Set a custom `Canvas` instance to render the document to.
1294 *
1295 * Be aware that the instance will be replaced on render if the document
1296 * defines a paper size different from the canvas.
1297 *
1298 * @param Canvas $canvas
1299 * @return $this
1300 */
1301 public function setCanvas(Canvas $canvas)
1302 {
1303 $this->canvas = $canvas;
1304 $canvasWidth = $this->canvas->get_width();
1305 $canvasHeight = $this->canvas->get_height();
1306 $this->paperSize = [0, 0, $canvasWidth, $canvasHeight];
1307 $this->paperOrientation = "portrait";
1308 return $this;
1309 }
1310
1311 /**
1312 * @return Canvas
1313 * @deprecated
1314 */
1315 public function get_canvas()
1316 {
1317 return $this->getCanvas();
1318 }
1319
1320 /**
1321 * Return the underlying Canvas instance (e.g. Dompdf\Adapter\CPDF, Dompdf\Adapter\GD)
1322 *
1323 * @return Canvas
1324 */
1325 public function getCanvas()
1326 {
1327 return $this->canvas;
1328 }
1329
1330 /**
1331 * @param Stylesheet $css
1332 * @return $this
1333 */
1334 public function setCss(Stylesheet $css)
1335 {
1336 $this->css = $css;
1337 return $this;
1338 }
1339
1340 /**
1341 * @return Stylesheet
1342 * @deprecated
1343 */
1344 public function get_css()
1345 {
1346 return $this->getCss();
1347 }
1348
1349 /**
1350 * Returns the stylesheet
1351 *
1352 * @return Stylesheet
1353 */
1354 public function getCss()
1355 {
1356 return $this->css;
1357 }
1358
1359 /**
1360 * @param DOMDocument $dom
1361 * @return $this
1362 */
1363 public function setDom(DOMDocument $dom)
1364 {
1365 $this->dom = $dom;
1366 return $this;
1367 }
1368
1369 /**
1370 * @return DOMDocument
1371 * @deprecated
1372 */
1373 public function get_dom()
1374 {
1375 return $this->getDom();
1376 }
1377
1378 /**
1379 * @return DOMDocument
1380 */
1381 public function getDom()
1382 {
1383 return $this->dom;
1384 }
1385
1386 /**
1387 * @param Options $options
1388 * @return $this
1389 */
1390 public function setOptions(Options $options)
1391 {
1392 // For backwards compatibility
1393 if ($this->options && $this->options->getHttpContext() && !$options->getHttpContext()) {
1394 $options->setHttpContext($this->options->getHttpContext());
1395 }
1396
1397 $this->options = $options;
1398
1399 $fontMetrics = $this->fontMetrics;
1400 if (isset($fontMetrics)) {
1401 $fontMetrics->setOptions($options);
1402 }
1403
1404 if (isset($this->canvas)) {
1405 $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation);
1406 if (isset($fontMetrics)) {
1407 $this->fontMetrics->setCanvas($this->canvas)->setOptions($options);
1408 }
1409 }
1410
1411 return $this;
1412 }
1413
1414 /**
1415 * @return Options
1416 */
1417 public function getOptions()
1418 {
1419 return $this->options;
1420 }
1421
1422 /**
1423 * @return array
1424 * @deprecated
1425 */
1426 public function get_callbacks()
1427 {
1428 return $this->getCallbacks();
1429 }
1430
1431 /**
1432 * Returns the callbacks array
1433 *
1434 * @return array
1435 */
1436 public function getCallbacks()
1437 {
1438 return $this->callbacks;
1439 }
1440
1441 /**
1442 * @param array $callbacks the set of callbacks to set
1443 * @return $this
1444 * @deprecated
1445 */
1446 public function set_callbacks($callbacks)
1447 {
1448 return $this->setCallbacks($callbacks);
1449 }
1450
1451 /**
1452 * Define callbacks that allow modifying the document during render.
1453 *
1454 * The callbacks array should contain arrays with `event` set to a callback
1455 * event name and `f` set to a function or any other callable.
1456 *
1457 * The available callback events are:
1458 * * `begin_page_reflow`: called before page reflow
1459 * * `begin_frame`: called before a frame is rendered
1460 * * `end_frame`: called after frame rendering is complete
1461 * * `begin_page_render`: called before a page is rendered
1462 * * `end_page_render`: called after page rendering is complete
1463 * * `end_document`: called for every page after rendering is complete
1464 *
1465 * The function `f` receives three arguments `Frame $frame`, `Canvas $canvas`,
1466 * and `FontMetrics $fontMetrics` for all events but `end_document`. For
1467 * `end_document`, the function receives four arguments `int $pageNumber`,
1468 * `int $pageCount`, `Canvas $canvas`, and `FontMetrics $fontMetrics` instead.
1469 *
1470 * @param array $callbacks The set of callbacks to set.
1471 * @return $this
1472 */
1473 public function setCallbacks(array $callbacks): self
1474 {
1475 $this->callbacks = [];
1476
1477 foreach ($callbacks as $c) {
1478 if (is_array($c) && isset($c["event"]) && isset($c["f"])) {
1479 $event = $c["event"];
1480 $f = $c["f"];
1481 if (is_string($event) && is_callable($f)) {
1482 $this->callbacks[$event][] = $f;
1483 }
1484 }
1485 }
1486
1487 return $this;
1488 }
1489
1490 /**
1491 * @return boolean
1492 * @deprecated
1493 */
1494 public function get_quirksmode()
1495 {
1496 return $this->getQuirksmode();
1497 }
1498
1499 /**
1500 * Get the quirks mode
1501 *
1502 * @return boolean true if quirks mode is active
1503 */
1504 public function getQuirksmode()
1505 {
1506 return $this->quirksmode;
1507 }
1508
1509 /**
1510 * @param FontMetrics $fontMetrics
1511 * @return $this
1512 */
1513 public function setFontMetrics(FontMetrics $fontMetrics)
1514 {
1515 $this->fontMetrics = $fontMetrics;
1516 return $this;
1517 }
1518
1519 /**
1520 * @return FontMetrics
1521 */
1522 public function getFontMetrics()
1523 {
1524 return $this->fontMetrics;
1525 }
1526
1527 /**
1528 * PHP5 overloaded getter
1529 * Along with {@link Dompdf::__set()} __get() provides access to all
1530 * properties directly. Typically __get() is not called directly outside
1531 * of this class.
1532 *
1533 * @param string $prop
1534 *
1535 * @throws Exception
1536 * @return mixed
1537 */
1538 function __get($prop)
1539 {
1540 switch ($prop) {
1541 case 'version':
1542 return $this->version;
1543 default:
1544 throw new Exception('Invalid property: ' . $prop);
1545 }
1546 }
1547 }
1548