PluginProbe
MBE eShip / trunk
MBE eShip vtrunk
2.8.1 trunk 1.0.0 1.1.0 1.1.3 1.2.1 1.2.2 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.6.0 1.7.0 1.7.1 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1.0 2.1.1 2.1.2 2.2.1 All 37 releases
mail-boxes-etc / lib / dompdf / src / FrameDecorator / Page.php

Page.php in MBE eShip trunk, at lib/dompdf/src/FrameDecorator/Page.php

754 lines 24.3 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\FrameDecorator;
8
9 use Dompdf\Dompdf;
10 use Dompdf\Helpers;
11 use Dompdf\Frame;
12 use Dompdf\Renderer;
13
14 /**
15 * Decorates frames for page layout
16 *
17 * @package dompdf
18 */
19 class Page extends AbstractFrameDecorator
20 {
21 /**
22 * The y value of the bottom edge of the page area.
23 *
24 * https://www.w3.org/TR/CSS21/page.html#page-margins
25 *
26 * @var float
27 */
28 protected $bottom_page_edge;
29
30 /**
31 * Flag indicating page is full.
32 *
33 * @var bool
34 */
35 protected $_page_full;
36
37 /**
38 * Number of tables currently being reflowed
39 *
40 * @var int
41 */
42 protected $_in_table;
43
44 /**
45 * The pdf renderer
46 *
47 * @var Renderer
48 */
49 protected $_renderer;
50
51 /**
52 * This page's floating frames
53 *
54 * @var array
55 */
56 protected $_floating_frames = [];
57
58 //........................................................................
59
60 /**
61 * Class constructor
62 *
63 * @param Frame $frame the frame to decorate
64 * @param Dompdf $dompdf
65 */
66 function __construct(Frame $frame, Dompdf $dompdf)
67 {
68 parent::__construct($frame, $dompdf);
69 $this->_page_full = false;
70 $this->_in_table = 0;
71 $this->bottom_page_edge = null;
72 }
73
74 /**
75 * Set the renderer used for this pdf
76 *
77 * @param Renderer $renderer the renderer to use
78 */
79 function set_renderer($renderer)
80 {
81 $this->_renderer = $renderer;
82 }
83
84 /**
85 * Return the renderer used for this pdf
86 *
87 * @return Renderer
88 */
89 function get_renderer()
90 {
91 return $this->_renderer;
92 }
93
94 /**
95 * Calculate the bottom edge of the page area after margins have been
96 * applied for the current page.
97 */
98 public function calculate_bottom_page_edge(): void
99 {
100 [, , , $cbh] = $this->get_containing_block();
101 $style = $this->get_style();
102 $margin_bottom = (float) $style->length_in_pt($style->margin_bottom, $cbh);
103
104 $this->bottom_page_edge = $cbh - $margin_bottom;
105 }
106
107 /**
108 * Returns true if the page is full and is no longer accepting frames.
109 *
110 * @return bool
111 */
112 function is_full()
113 {
114 return $this->_page_full;
115 }
116
117 /**
118 * Start a new page by resetting the full flag.
119 */
120 function next_page()
121 {
122 $this->_floating_frames = [];
123 $this->_renderer->new_page();
124 $this->_page_full = false;
125 }
126
127 /**
128 * Indicate to the page that a table is currently being reflowed.
129 */
130 function table_reflow_start()
131 {
132 $this->_in_table++;
133 }
134
135 /**
136 * Indicate to the page that table reflow is finished.
137 */
138 function table_reflow_end()
139 {
140 $this->_in_table--;
141 }
142
143 /**
144 * Return whether we are currently in a nested table or not
145 *
146 * @return bool
147 */
148 function in_nested_table()
149 {
150 return $this->_in_table > 1;
151 }
152
153 /**
154 * Check if a forced page break is required before $frame. This uses the
155 * frame's page_break_before property as well as the preceding frame's
156 * page_break_after property.
157 *
158 * @link http://www.w3.org/TR/CSS21/page.html#forced
159 *
160 * @param AbstractFrameDecorator $frame the frame to check
161 *
162 * @return bool true if a page break occurred
163 */
164 function check_forced_page_break(Frame $frame)
165 {
166 // Skip check if page is already split and for the body
167 if ($this->_page_full || $frame->get_node()->nodeName === "body") {
168 return false;
169 }
170
171 $page_breaks = ["always", "left", "right"];
172 $style = $frame->get_style();
173
174 if (($frame->is_block_level() || $style->display === "table-row")
175 && in_array($style->page_break_before, $page_breaks, true)
176 ) {
177 // Prevent cascading splits
178 $frame->split(null, true, true);
179 $style->page_break_before = "auto";
180 $this->_page_full = true;
181 $frame->_already_pushed = true;
182
183 return true;
184 }
185
186 // Find the preceding block-level sibling (or table row). Inline
187 // elements are treated as if wrapped in an anonymous block container
188 // here. See https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
189 $prev = $frame->get_prev_sibling();
190 while ($prev && (($prev->is_text_node() && $prev->get_node()->nodeValue === "")
191 || $prev->get_node()->nodeName === "bullet")
192 ) {
193 $prev = $prev->get_prev_sibling();
194 }
195
196 if ($prev && ($prev->is_block_level() || $prev->get_style()->display === "table-row")) {
197 if (in_array($prev->get_style()->page_break_after, $page_breaks, true)) {
198 // Prevent cascading splits
199 $frame->split(null, true, true);
200 $prev->get_style()->page_break_after = "auto";
201 $this->_page_full = true;
202 $frame->_already_pushed = true;
203
204 return true;
205 }
206
207 $prev_last_child = $prev->get_last_child();
208 while ($prev_last_child && (($prev_last_child->is_text_node() && $prev_last_child->get_node()->nodeValue === "")
209 || $prev_last_child->get_node()->nodeName === "bullet")
210 ) {
211 $prev_last_child = $prev_last_child->get_prev_sibling();
212 }
213
214 if ($prev_last_child
215 && $prev_last_child->is_block_level()
216 && in_array($prev_last_child->get_style()->page_break_after, $page_breaks, true)
217 ) {
218 $frame->split(null, true, true);
219 $prev_last_child->get_style()->page_break_after = "auto";
220 $this->_page_full = true;
221 $frame->_already_pushed = true;
222
223 return true;
224 }
225 }
226
227 return false;
228 }
229
230 /**
231 * Check for a gap between the top content edge of a frame and its child
232 * content.
233 *
234 * Additionally, the top margin, border, and padding of the frame must fit
235 * on the current page.
236 *
237 * @param float $childPos The top margin or line-box edge of the child content.
238 * @param Frame $frame The parent frame to check.
239 * @return bool
240 */
241 protected function hasGap(float $childPos, Frame $frame): bool
242 {
243 $style = $frame->get_style();
244 $cbw = $frame->get_containing_block("w");
245 $contentEdge = $frame->get_position("y") + (float) $style->length_in_pt([
246 $style->margin_top,
247 $style->border_top_width,
248 $style->padding_top
249 ], $cbw);
250
251 return Helpers::lengthGreater($childPos, $contentEdge)
252 && Helpers::lengthLessOrEqual($contentEdge, $this->bottom_page_edge);
253 }
254
255 /**
256 * Determine if a page break is allowed before $frame
257 * http://www.w3.org/TR/CSS21/page.html#allowed-page-breaks
258 *
259 * In the normal flow, page breaks can occur at the following places:
260 *
261 * 1. In the vertical margin between block boxes. When an
262 * unforced page break occurs here, the used values of the
263 * relevant 'margin-top' and 'margin-bottom' properties are set
264 * to '0'. When a forced page break occurs here, the used value
265 * of the relevant 'margin-bottom' property is set to '0'; the
266 * relevant 'margin-top' used value may either be set to '0' or
267 * retained.
268 * 2. Between line boxes inside a block container box.
269 * 3. Between the content edge of a block container box and the
270 * outer edges of its child content (margin edges of block-level
271 * children or line box edges for inline-level children) if there
272 * is a (non-zero) gap between them.
273 *
274 * These breaks are subject to the following rules:
275 *
276 * * Rule A: Breaking at (1) is allowed only if the
277 * 'page-break-after' and 'page-break-before' properties of all
278 * the elements generating boxes that meet at this margin allow
279 * it, which is when at least one of them has the value
280 * 'always', 'left', or 'right', or when all of them are 'auto'.
281 *
282 * * Rule B: However, if all of them are 'auto' and a common
283 * ancestor of all the elements has a 'page-break-inside' value
284 * of 'avoid', then breaking here is not allowed.
285 *
286 * * Rule C: Breaking at (2) is allowed only if the number of line
287 * boxes between the break and the start of the enclosing block
288 * box is the value of 'orphans' or more, and the number of line
289 * boxes between the break and the end of the box is the value
290 * of 'widows' or more.
291 *
292 * * Rule D: In addition, breaking at (2) or (3) is allowed only
293 * if the 'page-break-inside' property of the element and all
294 * its ancestors is 'auto'.
295 *
296 * If the above does not provide enough break points to keep content
297 * from overflowing the page boxes, then rules A, B and D are
298 * dropped in order to find additional breakpoints.
299 *
300 * If that still does not lead to sufficient break points, rule C is
301 * dropped as well, to find still more break points.
302 *
303 * We also allow breaks between table rows.
304 *
305 * @param AbstractFrameDecorator $frame the frame to check
306 *
307 * @return bool true if a break is allowed, false otherwise
308 */
309 protected function _page_break_allowed(Frame $frame)
310 {
311 Helpers::dompdf_debug("page-break", "_page_break_allowed(" . $frame->get_node()->nodeName . ")");
312 $display = $frame->get_style()->display;
313
314 // Block Frames (1):
315 if ($frame->is_block_level() || $display === "-dompdf-image") {
316
317 // Avoid breaks within table-cells
318 if ($this->_in_table > ($display === "table" ? 1 : 0)) {
319 Helpers::dompdf_debug("page-break", "In table: " . $this->_in_table);
320
321 return false;
322 }
323
324 // Rule A
325 if ($frame->get_style()->page_break_before === "avoid") {
326 Helpers::dompdf_debug("page-break", "before: avoid");
327
328 return false;
329 }
330
331 // Find the preceding block-level sibling. Inline elements are
332 // treated as if wrapped in an anonymous block container here. See
333 // https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
334 $prev = $frame->get_prev_sibling();
335 while ($prev && (($prev->is_text_node() && $prev->get_node()->nodeValue === "")
336 || $prev->get_node()->nodeName === "bullet")
337 ) {
338 $prev = $prev->get_prev_sibling();
339 }
340
341 // Does the previous element allow a page break after?
342 if ($prev && ($prev->is_block_level() || $prev->get_style()->display === "-dompdf-image")
343 && $prev->get_style()->page_break_after === "avoid"
344 ) {
345 Helpers::dompdf_debug("page-break", "after: avoid");
346
347 return false;
348 }
349
350 // Rules B & D
351 $parent = $frame->get_parent();
352 $p = $parent;
353 while ($p) {
354 if ($p->get_style()->page_break_inside === "avoid") {
355 Helpers::dompdf_debug("page-break", "parent->inside: avoid");
356
357 return false;
358 }
359 $p = $p->find_block_parent();
360 }
361
362 // To prevent cascading page breaks when a top-level element has
363 // page-break-inside: avoid, ensure that at least one frame is
364 // on the page before splitting.
365 if ($parent->get_node()->nodeName === "body" && !$prev) {
366 // We are the body's first child
367 Helpers::dompdf_debug("page-break", "Body's first child.");
368
369 return false;
370 }
371
372 // Check for a possible type (3) break
373 if (!$prev && $parent && !$this->hasGap($frame->get_position("y"), $parent)) {
374 Helpers::dompdf_debug("page-break", "First block-level frame, no gap");
375
376 return false;
377 }
378
379 Helpers::dompdf_debug("page-break", "block: break allowed");
380
381 return true;
382
383 } // Inline frames (2):
384 else {
385 if ($frame->is_inline_level()) {
386
387 // Avoid breaks within table-cells
388 if ($this->_in_table) {
389 Helpers::dompdf_debug("page-break", "In table: " . $this->_in_table);
390
391 return false;
392 }
393
394 // Rule C
395 $block_parent = $frame->find_block_parent();
396 $parent_style = $block_parent->get_style();
397 $line = $block_parent->get_current_line_box();
398 $line_count = count($block_parent->get_line_boxes());
399 $line_number = $frame->get_containing_line() && empty($line->get_frames())
400 ? $line_count - 1
401 : $line_count;
402
403 // The line number of the frame can be less than the current
404 // number of line boxes, in case we are backtracking. As long as
405 // we are not checking for widows yet, just checking against the
406 // number of line boxes is sufficient in most cases, though.
407 if ($line_number <= $parent_style->orphans) {
408 Helpers::dompdf_debug("page-break", "orphans");
409
410 return false;
411 }
412
413 // FIXME: Checking widows is tricky without having laid out the
414 // remaining line boxes. Just ignore it for now...
415
416 // Rule D
417 $p = $block_parent;
418 while ($p) {
419 if ($p->get_style()->page_break_inside === "avoid") {
420 Helpers::dompdf_debug("page-break", "parent->inside: avoid");
421
422 return false;
423 }
424 $p = $p->find_block_parent();
425 }
426
427 // To prevent cascading page breaks when a top-level element has
428 // page-break-inside: avoid, ensure that at least one frame with
429 // some content is on the page before splitting.
430 $prev = $frame->get_prev_sibling();
431 while ($prev && ($prev->is_text_node() && trim($prev->get_node()->nodeValue) == "")) {
432 $prev = $prev->get_prev_sibling();
433 }
434
435 if ($block_parent->get_node()->nodeName === "body" && !$prev) {
436 // We are the body's first child
437 Helpers::dompdf_debug("page-break", "Body's first child.");
438
439 return false;
440 }
441
442 Helpers::dompdf_debug("page-break", "inline: break allowed");
443
444 return true;
445
446 // Table-rows
447 } else {
448 if ($display === "table-row") {
449
450 // If this is a nested table, prevent the page from breaking
451 if ($this->_in_table > 1) {
452 Helpers::dompdf_debug("page-break", "table: nested table");
453
454 return false;
455 }
456
457 // Rule A (table row)
458 if ($frame->get_style()->page_break_before === "avoid") {
459 Helpers::dompdf_debug("page-break", "before: avoid");
460
461 return false;
462 }
463
464 // Find the preceding row
465 $prev = $frame->get_prev_sibling();
466
467 if (!$prev) {
468 $prev_group = $frame->get_parent()->get_prev_sibling();
469
470 if ($prev_group
471 && in_array($prev_group->get_style()->display, Table::ROW_GROUPS, true)
472 ) {
473 $prev = $prev_group->get_last_child();
474 }
475 }
476
477 // Check if a page break is allowed after the preceding row
478 if ($prev && $prev->get_style()->page_break_after === "avoid") {
479 Helpers::dompdf_debug("page-break", "after: avoid");
480
481 return false;
482 }
483
484 // Avoid breaking before the first row of a table
485 if (!$prev) {
486 Helpers::dompdf_debug("page-break", "table: first-row");
487
488 return false;
489 }
490
491 // Rule B (table row)
492 // Check if the page_break_inside property is not 'avoid'
493 // for the parent table or any of its ancestors
494 $table = Table::find_parent_table($frame);
495
496 $p = $table;
497 while ($p) {
498 if ($p->get_style()->page_break_inside === "avoid") {
499 Helpers::dompdf_debug("page-break", "parent->inside: avoid");
500
501 return false;
502 }
503 $p = $p->find_block_parent();
504 }
505
506 Helpers::dompdf_debug("page-break", "table-row: break allowed");
507
508 return true;
509 } else {
510 if (in_array($display, Table::ROW_GROUPS, true)) {
511
512 // Disallow breaks at row-groups: only split at row boundaries
513 return false;
514
515 } else {
516 Helpers::dompdf_debug("page-break", "? " . $display);
517
518 return false;
519 }
520 }
521 }
522 }
523 }
524
525 /**
526 * Check if $frame will fit on the page. If the frame does not fit,
527 * the frame tree is modified so that a page break occurs in the
528 * correct location.
529 *
530 * @param AbstractFrameDecorator $frame the frame to check
531 *
532 * @return bool
533 */
534 function check_page_break(Frame $frame)
535 {
536 if ($this->_page_full || $frame->_already_pushed
537 // Never check for breaks on empty text nodes
538 || ($frame->is_text_node() && $frame->get_node()->nodeValue === "")
539 ) {
540 return false;
541 }
542
543 $p = $frame;
544 do {
545 $display = $p->get_style()->display;
546 if ($display == "table-row") {
547 if ($p->_already_pushed) { return false; }
548 }
549 } while ($p = $p->get_parent());
550
551 // If the frame is absolute or fixed it shouldn't break
552 $p = $frame;
553 do {
554 if ($p->is_absolute()) {
555 return false;
556 }
557 } while ($p = $p->get_parent());
558
559 $margin_height = $frame->get_margin_height();
560
561 // Determine the frame's maximum y value
562 $max_y = (float)$frame->get_position("y") + $margin_height;
563
564 // If a split is to occur here, then the bottom margins & paddings of all
565 // parents of $frame must fit on the page as well:
566 $p = $frame->get_parent();
567 while ($p && $p !== $this) {
568 $cbw = $p->get_containing_block("w");
569 $max_y += (float) $p->get_style()->computed_bottom_spacing($cbw);
570 $p = $p->get_parent();
571 }
572
573 // Check if $frame flows off the page
574 if (Helpers::lengthLessOrEqual($max_y, $this->bottom_page_edge)) {
575 // no: do nothing
576 return false;
577 }
578
579 Helpers::dompdf_debug("page-break", "check_page_break");
580 Helpers::dompdf_debug("page-break", "in_table: " . $this->_in_table);
581
582 // yes: determine page break location
583 $iter = $frame;
584 $flg = false;
585 $pushed_flg = false;
586
587 $in_table = $this->_in_table;
588
589 Helpers::dompdf_debug("page-break", "Starting search");
590 while ($iter) {
591 // echo "\nbacktrack: " .$iter->get_node()->nodeName ." ".spl_object_hash($iter->get_node()). "";
592 if ($iter === $this) {
593 Helpers::dompdf_debug("page-break", "reached root.");
594 // We've reached the root in our search. Just split at $frame.
595 break;
596 }
597
598 if ($iter->_already_pushed) {
599 $pushed_flg = true;
600 } elseif ($this->_page_break_allowed($iter)) {
601 Helpers::dompdf_debug("page-break", "break allowed, splitting.");
602 $iter->split(null, true);
603 $this->_page_full = true;
604 $this->_in_table = $in_table;
605 $iter->_already_pushed = true;
606 $frame->_already_pushed = true;
607
608 return true;
609 }
610
611 if (!$flg && $next = $iter->get_last_child()) {
612 Helpers::dompdf_debug("page-break", "following last child.");
613
614 if ($next->is_table()) {
615 $this->_in_table++;
616 }
617
618 $iter = $next;
619 $pushed_flg = false;
620 continue;
621 }
622
623 if ($pushed_flg) {
624 // The frame was already pushed, avoid breaking on a previous page
625 break;
626 }
627
628 $next = $iter->get_prev_sibling();
629 // Skip empty text nodes
630 while ($next && $next->is_text_node() && $next->get_node()->nodeValue === "") {
631 $next = $next->get_prev_sibling();
632 }
633
634 if ($next) {
635 Helpers::dompdf_debug("page-break", "following prev sibling.");
636
637 if ($next->is_table() && !$iter->is_table()) {
638 $this->_in_table++;
639 } elseif (!$next->is_table() && $iter->is_table()) {
640 $this->_in_table--;
641 }
642
643 $iter = $next;
644 $flg = false;
645 continue;
646 }
647
648 if ($next = $iter->get_parent()) {
649 Helpers::dompdf_debug("page-break", "following parent.");
650
651 if ($iter->is_table()) {
652 $this->_in_table--;
653 }
654
655 $iter = $next;
656 $flg = true;
657 continue;
658 }
659
660 break;
661 }
662
663 $this->_in_table = $in_table;
664
665 // No valid page break found. Just break at $frame.
666 Helpers::dompdf_debug("page-break", "no valid break found, just splitting.");
667
668 // If we are in a table, backtrack to the nearest top-level table row
669 if ($this->_in_table) {
670 $iter = $frame;
671 while ($iter && $iter->get_style()->display !== "table-row" && $iter->get_style()->display !== 'table-row-group' && $iter->_already_pushed === false) {
672 $iter = $iter->get_parent();
673 }
674
675 if ($iter) {
676 $iter->split(null, true);
677 $iter->_already_pushed = true;
678 } else {
679 return false;
680 }
681 } else {
682 $frame->split(null, true);
683 }
684
685 $this->_page_full = true;
686 $frame->_already_pushed = true;
687
688 return true;
689 }
690
691 //........................................................................
692
693 public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
694 {
695 // Do nothing
696 }
697
698 /**
699 * Add a floating frame
700 *
701 * @param Frame $frame
702 *
703 * @return void
704 */
705 function add_floating_frame(Frame $frame)
706 {
707 array_unshift($this->_floating_frames, $frame);
708 }
709
710 /**
711 * @return Frame[]
712 */
713 function get_floating_frames()
714 {
715 return $this->_floating_frames;
716 }
717
718 /**
719 * @param $key
720 */
721 public function remove_floating_frame($key)
722 {
723 unset($this->_floating_frames[$key]);
724 }
725
726 /**
727 * @param Frame $child
728 * @return int|mixed
729 */
730 public function get_lowest_float_offset(Frame $child)
731 {
732 $style = $child->get_style();
733 $side = $style->clear;
734 $float = $style->float;
735
736 $y = 0;
737
738 if ($float === "none") {
739 foreach ($this->_floating_frames as $key => $frame) {
740 if ($side === "both" || $frame->get_style()->float === $side) {
741 $y = max($y, $frame->get_position("y") + $frame->get_margin_height());
742 }
743 $this->remove_floating_frame($key);
744 }
745 }
746
747 if ($y > 0) {
748 $y++; // add 1px buffer from float
749 }
750
751 return $y;
752 }
753 }
754