composer package updates
[openemr.git] / vendor / dompdf / dompdf / src / FrameDecorator / Page.php
blob74d4284adc8fdb7fb83e870c30a6a8472a0f71b0
1 <?php
2 /**
3 * @package dompdf
4 * @link http://dompdf.github.com/
5 * @author Benj Carson <benjcarson@digitaljunkies.ca>
6 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
7 */
8 namespace Dompdf\FrameDecorator;
10 use Dompdf\Css\Style;
11 use Dompdf\Dompdf;
12 use Dompdf\Helpers;
13 use Dompdf\Frame;
14 use Dompdf\Renderer;
16 /**
17 * Decorates frames for page layout
19 * @access private
20 * @package dompdf
22 class Page extends AbstractFrameDecorator
25 /**
26 * y value of bottom page margin
28 * @var float
30 protected $_bottom_page_margin;
32 /**
33 * Flag indicating page is full.
35 * @var bool
37 protected $_page_full;
39 /**
40 * Number of tables currently being reflowed
42 * @var int
44 protected $_in_table;
46 /**
47 * The pdf renderer
49 * @var Renderer
51 protected $_renderer;
53 /**
54 * This page's floating frames
56 * @var array
58 protected $_floating_frames = array();
60 //........................................................................
62 /**
63 * Class constructor
65 * @param Frame $frame the frame to decorate
66 * @param Dompdf $dompdf
68 function __construct(Frame $frame, Dompdf $dompdf)
70 parent::__construct($frame, $dompdf);
71 $this->_page_full = false;
72 $this->_in_table = 0;
73 $this->_bottom_page_margin = null;
76 /**
77 * Set the renderer used for this pdf
79 * @param Renderer $renderer the renderer to use
81 function set_renderer($renderer)
83 $this->_renderer = $renderer;
86 /**
87 * Return the renderer used for this pdf
89 * @return Renderer
91 function get_renderer()
93 return $this->_renderer;
96 /**
97 * Set the frame's containing block. Overridden to set $this->_bottom_page_margin.
99 * @param float $x
100 * @param float $y
101 * @param float $w
102 * @param float $h
104 function set_containing_block($x = null, $y = null, $w = null, $h = null)
106 parent::set_containing_block($x, $y, $w, $h);
107 //$w = $this->get_containing_block("w");
108 if (isset($h)) {
109 $this->_bottom_page_margin = $h;
110 } // - $this->_frame->get_style()->length_in_pt($this->_frame->get_style()->margin_bottom, $w);
114 * Returns true if the page is full and is no longer accepting frames.
116 * @return bool
118 function is_full()
120 return $this->_page_full;
124 * Start a new page by resetting the full flag.
126 function next_page()
128 $this->_floating_frames = array();
129 $this->_renderer->new_page();
130 $this->_page_full = false;
134 * Indicate to the page that a table is currently being reflowed.
136 function table_reflow_start()
138 $this->_in_table++;
142 * Indicate to the page that table reflow is finished.
144 function table_reflow_end()
146 $this->_in_table--;
150 * Return whether we are currently in a nested table or not
152 * @return bool
154 function in_nested_table()
156 return $this->_in_table > 1;
160 * Check if a forced page break is required before $frame. This uses the
161 * frame's page_break_before property as well as the preceeding frame's
162 * page_break_after property.
164 * @link http://www.w3.org/TR/CSS21/page.html#forced
166 * @param Frame $frame the frame to check
168 * @return bool true if a page break occured
170 function check_forced_page_break(Frame $frame)
173 // Skip check if page is already split
174 if ($this->_page_full) {
175 return null;
178 $block_types = array("block", "list-item", "table", "inline");
179 $page_breaks = array("always", "left", "right");
181 $style = $frame->get_style();
183 if (!in_array($style->display, $block_types)) {
184 return false;
187 // Find the previous block-level sibling
188 $prev = $frame->get_prev_sibling();
190 while ($prev && !in_array($prev->get_style()->display, $block_types)) {
191 $prev = $prev->get_prev_sibling();
194 if (in_array($style->page_break_before, $page_breaks)) {
195 // Prevent cascading splits
196 $frame->split(null, true);
197 // We have to grab the style again here because split() resets
198 // $frame->style to the frame's orignal style.
199 $frame->get_style()->page_break_before = "auto";
200 $this->_page_full = true;
202 return true;
205 if ($prev && in_array($prev->get_style()->page_break_after, $page_breaks)) {
206 // Prevent cascading splits
207 $frame->split(null, true);
208 $prev->get_style()->page_break_after = "auto";
209 $this->_page_full = true;
211 return true;
214 if ($prev && $prev->get_last_child() && $frame->get_node()->nodeName != "body") {
215 $prev_last_child = $prev->get_last_child();
216 if (in_array($prev_last_child->get_style()->page_break_after, $page_breaks)) {
217 $frame->split(null, true);
218 $prev_last_child->get_style()->page_break_after = "auto";
219 $this->_page_full = true;
221 return true;
225 return false;
229 * Determine if a page break is allowed before $frame
230 * http://www.w3.org/TR/CSS21/page.html#allowed-page-breaks
232 * In the normal flow, page breaks can occur at the following places:
234 * 1. In the vertical margin between block boxes. When a page
235 * break occurs here, the used values of the relevant
236 * 'margin-top' and 'margin-bottom' properties are set to '0'.
237 * 2. Between line boxes inside a block box.
239 * These breaks are subject to the following rules:
241 * * Rule A: Breaking at (1) is allowed only if the
242 * 'page-break-after' and 'page-break-before' properties of
243 * all the elements generating boxes that meet at this margin
244 * allow it, which is when at least one of them has the value
245 * 'always', 'left', or 'right', or when all of them are
246 * 'auto'.
248 * * Rule B: However, if all of them are 'auto' and the
249 * nearest common ancestor of all the elements has a
250 * 'page-break-inside' value of 'avoid', then breaking here is
251 * not allowed.
253 * * Rule C: Breaking at (2) is allowed only if the number of
254 * line boxes between the break and the start of the enclosing
255 * block box is the value of 'orphans' or more, and the number
256 * of line boxes between the break and the end of the box is
257 * the value of 'widows' or more.
259 * * Rule D: In addition, breaking at (2) is allowed only if
260 * the 'page-break-inside' property is 'auto'.
262 * If the above doesn't provide enough break points to keep
263 * content from overflowing the page boxes, then rules B and D are
264 * dropped in order to find additional breakpoints.
266 * If that still does not lead to sufficient break points, rules A
267 * and C are dropped as well, to find still more break points.
269 * We will also allow breaks between table rows. However, when
270 * splitting a table, the table headers should carry over to the
271 * next page (but they don't yet).
273 * @param Frame $frame the frame to check
275 * @return bool true if a break is allowed, false otherwise
277 protected function _page_break_allowed(Frame $frame)
279 $block_types = array("block", "list-item", "table", "-dompdf-image");
280 Helpers::dompdf_debug("page-break", "_page_break_allowed(" . $frame->get_node()->nodeName . ")");
281 $display = $frame->get_style()->display;
283 // Block Frames (1):
284 if (in_array($display, $block_types)) {
286 // Avoid breaks within table-cells
287 if ($this->_in_table) {
288 Helpers::dompdf_debug("page-break", "In table: " . $this->_in_table);
290 return false;
293 // Rules A & B
295 if ($frame->get_style()->page_break_before === "avoid") {
296 Helpers::dompdf_debug("page-break", "before: avoid");
298 return false;
301 // Find the preceeding block-level sibling
302 $prev = $frame->get_prev_sibling();
303 while ($prev && !in_array($prev->get_style()->display, $block_types)) {
304 $prev = $prev->get_prev_sibling();
307 // Does the previous element allow a page break after?
308 if ($prev && $prev->get_style()->page_break_after === "avoid") {
309 Helpers::dompdf_debug("page-break", "after: avoid");
311 return false;
314 // If both $prev & $frame have the same parent, check the parent's
315 // page_break_inside property.
316 $parent = $frame->get_parent();
317 if ($prev && $parent && $parent->get_style()->page_break_inside === "avoid") {
318 Helpers::dompdf_debug("page-break", "parent inside: avoid");
320 return false;
323 // To prevent cascading page breaks when a top-level element has
324 // page-break-inside: avoid, ensure that at least one frame is
325 // on the page before splitting.
326 if ($parent->get_node()->nodeName === "body" && !$prev) {
327 // We are the body's first child
328 Helpers::dompdf_debug("page-break", "Body's first child.");
330 return false;
333 // If the frame is the first block-level frame, use the value from
334 // $frame's parent instead.
335 if (!$prev && $parent) {
336 return $this->_page_break_allowed($parent);
339 Helpers::dompdf_debug("page-break", "block: break allowed");
341 return true;
343 } // Inline frames (2):
344 else {
345 if (in_array($display, Style::$INLINE_TYPES)) {
347 // Avoid breaks within table-cells
348 if ($this->_in_table) {
349 Helpers::dompdf_debug("page-break", "In table: " . $this->_in_table);
351 return false;
354 // Rule C
355 $block_parent = $frame->find_block_parent();
356 if (count($block_parent->get_line_boxes()) < $frame->get_style()->orphans) {
357 Helpers::dompdf_debug("page-break", "orphans");
359 return false;
362 // FIXME: Checking widows is tricky without having laid out the
363 // remaining line boxes. Just ignore it for now...
365 // Rule D
366 $p = $block_parent;
367 while ($p) {
368 if ($p->get_style()->page_break_inside === "avoid") {
369 Helpers::dompdf_debug("page-break", "parent->inside: avoid");
371 return false;
373 $p = $p->find_block_parent();
376 // To prevent cascading page breaks when a top-level element has
377 // page-break-inside: avoid, ensure that at least one frame with
378 // some content is on the page before splitting.
379 $prev = $frame->get_prev_sibling();
380 while ($prev && ($prev->is_text_node() && trim($prev->get_node()->nodeValue) == "")) {
381 $prev = $prev->get_prev_sibling();
384 if ($block_parent->get_node()->nodeName === "body" && !$prev) {
385 // We are the body's first child
386 Helpers::dompdf_debug("page-break", "Body's first child.");
388 return false;
391 // Skip breaks on empty text nodes
392 if ($frame->is_text_node() && $frame->get_node()->nodeValue == "") {
393 return false;
396 Helpers::dompdf_debug("page-break", "inline: break allowed");
398 return true;
400 // Table-rows
401 } else {
402 if ($display === "table-row") {
403 // Simply check if the parent table's page_break_inside property is
404 // not 'avoid'
405 $table = Table::find_parent_table($frame);
407 $p = $table;
408 while ($p) {
409 if ($p->get_style()->page_break_inside === "avoid") {
410 Helpers::dompdf_debug("page-break", "parent->inside: avoid");
412 return false;
414 $p = $p->find_block_parent();
417 // Avoid breaking after the first row of a table
418 if ($table && $table->get_first_child() === $frame || $table->get_first_child()->get_first_child() === $frame) {
419 Helpers::dompdf_debug("page-break", "table: first-row");
421 return false;
424 // If this is a nested table, prevent the page from breaking
425 if ($this->_in_table > 1) {
426 Helpers::dompdf_debug("page-break", "table: nested table");
428 return false;
431 Helpers::dompdf_debug("page-break", "table-row/row-groups: break allowed");
433 return true;
434 } else {
435 if (in_array($display, Table::$ROW_GROUPS)) {
437 // Disallow breaks at row-groups: only split at row boundaries
438 return false;
440 } else {
441 Helpers::dompdf_debug("page-break", "? " . $frame->get_style()->display . "");
443 return false;
452 * Check if $frame will fit on the page. If the frame does not fit,
453 * the frame tree is modified so that a page break occurs in the
454 * correct location.
456 * @param Frame $frame the frame to check
458 * @return bool
460 function check_page_break(Frame $frame)
462 //FIXME: should not need to do this since we're tracking table status in `$this->_in_table`
463 $p = $frame;
464 $in_table = false;
465 while ($p) {
466 if ($p->is_table()) { $in_table = true; break; }
467 $p = $p->get_parent();
469 // Do not split if we have already or if the frame was already
470 // pushed to the next page (prevents infinite loops)
471 if ($in_table) {
472 if ($this->_page_full && $frame->_already_pushed) {
473 return false;
475 } elseif ($this->_page_full || $frame->_already_pushed) {
476 return false;
479 //FIXME: work-around for infinite loop due to tables
480 if ($in_table && $frame->_already_pushed) {
481 return false;
483 $p = $frame;
484 do {
485 $display = $p->get_style()->display;
486 if ($display == "table-row") {
487 if ($p->_already_pushed) { return false; }
489 } while ($p = $p->get_parent());
491 // If the frame is absolute of fixed it shouldn't break
492 $p = $frame;
493 do {
494 if ($p->is_absolute()) {
495 return false;
498 // FIXME If the row is taller than the page and
499 // if it the first of the page, we don't break
500 $display = $p->get_style()->display;
501 if ($display === "table-row"
502 && !$p->get_prev_sibling()
503 && $p->get_margin_height() > $this->get_margin_height()
505 return false;
507 } while ($p = $p->get_parent());
509 $margin_height = $frame->get_margin_height();
511 // Determine the frame's maximum y value
512 $max_y = (float)$frame->get_position("y") + $margin_height;
514 // If a split is to occur here, then the bottom margins & paddings of all
515 // parents of $frame must fit on the page as well:
516 $p = $frame->get_parent();
517 while ($p) {
518 $max_y += $p->get_style()->computed_bottom_spacing();
519 $p = $p->get_parent();
523 // Check if $frame flows off the page
524 if ($max_y <= $this->_bottom_page_margin) {
525 // no: do nothing
526 return false;
529 Helpers::dompdf_debug("page-break", "check_page_break");
530 Helpers::dompdf_debug("page-break", "in_table: " . $this->_in_table);
532 // yes: determine page break location
533 $iter = $frame;
534 $flg = false;
536 $in_table = $this->_in_table;
538 Helpers::dompdf_debug("page-break", "Starting search");
539 while ($iter) {
540 // echo "\nbacktrack: " .$iter->get_node()->nodeName ." ".spl_object_hash($iter->get_node()). "";
541 if ($iter === $this) {
542 Helpers::dompdf_debug("page-break", "reached root.");
543 // We've reached the root in our search. Just split at $frame.
544 break;
547 if ($this->_page_break_allowed($iter)) {
548 Helpers::dompdf_debug("page-break", "break allowed, splitting.");
549 $iter->split(null, true);
550 $this->_page_full = true;
551 $this->_in_table = $in_table;
552 $frame->_already_pushed = true;
554 return true;
557 if (!$flg && $next = $iter->get_last_child()) {
558 Helpers::dompdf_debug("page-break", "following last child.");
560 if ($next->is_table()) {
561 $this->_in_table++;
564 $iter = $next;
565 continue;
568 if ($next = $iter->get_prev_sibling()) {
569 Helpers::dompdf_debug("page-break", "following prev sibling.");
571 if ($next->is_table() && !$iter->is_table()) {
572 $this->_in_table++;
573 } else if (!$next->is_table() && $iter->is_table()) {
574 $this->_in_table--;
577 $iter = $next;
578 $flg = false;
579 continue;
582 if ($next = $iter->get_parent()) {
583 Helpers::dompdf_debug("page-break", "following parent.");
585 if ($iter->is_table()) {
586 $this->_in_table--;
589 $iter = $next;
590 $flg = true;
591 continue;
594 break;
597 $this->_in_table = $in_table;
599 // No valid page break found. Just break at $frame.
600 Helpers::dompdf_debug("page-break", "no valid break found, just splitting.");
602 // If we are in a table, backtrack to the nearest top-level table row
603 if ($this->_in_table) {
604 $iter = $frame;
605 while ($iter && $iter->get_style()->display !== "table-row" && $iter->get_style()->display !== 'table-row-group' && $iter->_already_pushed === false) {
606 $iter = $iter->get_parent();
609 if ($iter) {
610 $iter->split(null, true);
611 } else {
612 return false;
614 } else {
615 $frame->split(null, true);
618 $this->_page_full = true;
619 $frame->_already_pushed = true;
621 return true;
624 //........................................................................
627 * @param Frame|null $frame
628 * @param bool $force_pagebreak
630 function split(Frame $frame = null, $force_pagebreak = false)
632 // Do nothing
636 * Add a floating frame
638 * @param Frame $frame
640 * @return void
642 function add_floating_frame(Frame $frame)
644 array_unshift($this->_floating_frames, $frame);
648 * @return Frame[]
650 function get_floating_frames()
652 return $this->_floating_frames;
656 * @param $key
658 public function remove_floating_frame($key)
660 unset($this->_floating_frames[$key]);
664 * @param Frame $child
665 * @return int|mixed
667 public function get_lowest_float_offset(Frame $child)
669 $style = $child->get_style();
670 $side = $style->clear;
671 $float = $style->float;
673 $y = 0;
675 if ($float === "none") {
676 foreach ($this->_floating_frames as $key => $frame) {
677 if ($side === "both" || $frame->get_style()->float === $side) {
678 $y = max($y, $frame->get_position("y") + $frame->get_margin_height());
680 $this->remove_floating_frame($key);
684 if ($y > 0) {
685 $y++; // add 1px buffer from float
688 return $y;