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 / FrameReflower / AbstractFrameReflower.php

AbstractFrameReflower.php in MBE eShip trunk, at lib/dompdf/src/FrameReflower/AbstractFrameReflower.php

706 lines 22.0 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\FrameReflower;
8
9 use Dompdf\Dompdf;
10 use Dompdf\Helpers;
11 use Dompdf\Frame;
12 use Dompdf\Frame\Factory;
13 use Dompdf\FrameDecorator\AbstractFrameDecorator;
14 use Dompdf\FrameDecorator\Block;
15
16 /**
17 * Base reflower class
18 *
19 * Reflower objects are responsible for determining the width and height of
20 * individual frames. They also create line and page breaks as necessary.
21 *
22 * @package dompdf
23 */
24 abstract class AbstractFrameReflower
25 {
26
27 /**
28 * Frame for this reflower
29 *
30 * @var AbstractFrameDecorator
31 */
32 protected $_frame;
33
34 /**
35 * Cached min/max child size
36 *
37 * @var array
38 */
39 protected $_min_max_child_cache;
40
41 /**
42 * Cached min/max size
43 *
44 * @var array
45 */
46 protected $_min_max_cache;
47
48 /**
49 * AbstractFrameReflower constructor.
50 * @param AbstractFrameDecorator $frame
51 */
52 function __construct(AbstractFrameDecorator $frame)
53 {
54 $this->_frame = $frame;
55 $this->_min_max_child_cache = null;
56 $this->_min_max_cache = null;
57 }
58
59 /**
60 * @return Dompdf
61 */
62 function get_dompdf()
63 {
64 return $this->_frame->get_dompdf();
65 }
66
67 public function reset(): void
68 {
69 $this->_min_max_child_cache = null;
70 $this->_min_max_cache = null;
71 }
72
73 /**
74 * Determine the actual containing block for absolute and fixed position.
75 *
76 * https://www.w3.org/TR/CSS21/visudet.html#containing-block-details
77 */
78 protected function determine_absolute_containing_block(): void
79 {
80 $frame = $this->_frame;
81 $style = $frame->get_style();
82
83 switch ($style->position) {
84 case "absolute":
85 $parent = $frame->find_positioned_parent();
86 if ($parent !== $frame->get_root()) {
87 $parent_style = $parent->get_style();
88 $parent_padding_box = $parent->get_padding_box();
89 //FIXME: an accurate measure of the positioned parent height
90 // is not possible until reflow has completed;
91 // we'll fall back to the parent's containing block,
92 // which is wrong for auto-height parents
93 if ($parent_style->height === "auto") {
94 $parent_containing_block = $parent->get_containing_block();
95 $containing_block_height = $parent_containing_block["h"] -
96 (float)$parent_style->length_in_pt([
97 $parent_style->margin_top,
98 $parent_style->margin_bottom,
99 $parent_style->border_top_width,
100 $parent_style->border_bottom_width
101 ], $parent_containing_block["w"]);
102 } else {
103 $containing_block_height = $parent_padding_box["h"];
104 }
105 $frame->set_containing_block($parent_padding_box["x"], $parent_padding_box["y"], $parent_padding_box["w"], $containing_block_height);
106 break;
107 }
108 case "fixed":
109 $initial_cb = $frame->get_root()->get_first_child()->get_containing_block();
110 $frame->set_containing_block($initial_cb["x"], $initial_cb["y"], $initial_cb["w"], $initial_cb["h"]);
111 break;
112 default:
113 // Nothing to do, containing block already set via parent
114 break;
115 }
116 }
117
118 /**
119 * Collapse frames margins
120 * http://www.w3.org/TR/CSS21/box.html#collapsing-margins
121 */
122 protected function _collapse_margins(): void
123 {
124 $frame = $this->_frame;
125
126 // Margins of float/absolutely positioned/inline-level elements do not collapse
127 if (!$frame->is_in_flow() || $frame->is_inline_level()
128 || $frame->get_root() === $frame || $frame->get_parent() === $frame->get_root()
129 ) {
130 return;
131 }
132
133 $cb = $frame->get_containing_block();
134 $style = $frame->get_style();
135
136 $t = $style->length_in_pt($style->margin_top, $cb["w"]);
137 $b = $style->length_in_pt($style->margin_bottom, $cb["w"]);
138
139 // Handle 'auto' values
140 if ($t === "auto") {
141 $style->set_used("margin_top", 0.0);
142 $t = 0.0;
143 }
144
145 if ($b === "auto") {
146 $style->set_used("margin_bottom", 0.0);
147 $b = 0.0;
148 }
149
150 // Collapse vertical margins:
151 $n = $frame->get_next_sibling();
152 if ( $n && !($n->is_block_level() && $n->is_in_flow()) ) {
153 while ($n = $n->get_next_sibling()) {
154 if ($n->is_block_level() && $n->is_in_flow()) {
155 break;
156 }
157
158 if (!$n->get_first_child()) {
159 $n = null;
160 break;
161 }
162 }
163 }
164
165 if ($n) {
166 $n_style = $n->get_style();
167 $n_t = (float)$n_style->length_in_pt($n_style->margin_top, $cb["w"]);
168
169 $b = $this->get_collapsed_margin_length($b, $n_t);
170 $style->set_used("margin_bottom", $b);
171 $n_style->set_used("margin_top", 0.0);
172 }
173
174 // Collapse our first child's margin, if there is no border or padding
175 if ($style->border_top_width == 0 && $style->length_in_pt($style->padding_top) == 0) {
176 $f = $this->_frame->get_first_child();
177 if ( $f && !($f->is_block_level() && $f->is_in_flow()) ) {
178 while ($f = $f->get_next_sibling()) {
179 if ($f->is_block_level() && $f->is_in_flow()) {
180 break;
181 }
182
183 if (!$f->get_first_child()) {
184 $f = null;
185 break;
186 }
187 }
188 }
189
190 // Margins are collapsed only between block-level boxes
191 if ($f) {
192 $f_style = $f->get_style();
193 $f_t = (float)$f_style->length_in_pt($f_style->margin_top, $cb["w"]);
194
195 $t = $this->get_collapsed_margin_length($t, $f_t);
196 $style->set_used("margin_top", $t);
197 $f_style->set_used("margin_top", 0.0);
198 }
199 }
200
201 // Collapse our last child's margin, if there is no border or padding
202 if ($style->border_bottom_width == 0 && $style->length_in_pt($style->padding_bottom) == 0) {
203 $l = $this->_frame->get_last_child();
204 if ( $l && !($l->is_block_level() && $l->is_in_flow()) ) {
205 while ($l = $l->get_prev_sibling()) {
206 if ($l->is_block_level() && $l->is_in_flow()) {
207 break;
208 }
209
210 if (!$l->get_last_child()) {
211 $l = null;
212 break;
213 }
214 }
215 }
216
217 // Margins are collapsed only between block-level boxes
218 if ($l) {
219 $l_style = $l->get_style();
220 $l_b = (float)$l_style->length_in_pt($l_style->margin_bottom, $cb["w"]);
221
222 $b = $this->get_collapsed_margin_length($b, $l_b);
223 $style->set_used("margin_bottom", $b);
224 $l_style->set_used("margin_bottom", 0.0);
225 }
226 }
227 }
228
229 /**
230 * Get the combined (collapsed) length of two adjoining margins.
231 *
232 * See http://www.w3.org/TR/CSS21/box.html#collapsing-margins.
233 *
234 * @param float $l1
235 * @param float $l2
236 *
237 * @return float
238 */
239 private function get_collapsed_margin_length(float $l1, float $l2): float
240 {
241 if ($l1 < 0 && $l2 < 0) {
242 return min($l1, $l2); // min(x, y) = - max(abs(x), abs(y)), if x < 0 && y < 0
243 }
244
245 if ($l1 < 0 || $l2 < 0) {
246 return $l1 + $l2; // x + y = x - abs(y), if y < 0
247 }
248
249 return max($l1, $l2);
250 }
251
252 /**
253 * Handle relative positioning according to
254 * https://www.w3.org/TR/CSS21/visuren.html#relative-positioning.
255 *
256 * @param AbstractFrameDecorator $frame The frame to handle.
257 */
258 protected function position_relative(AbstractFrameDecorator $frame): void
259 {
260 $style = $frame->get_style();
261
262 if ($style->position === "relative") {
263 $cb = $frame->get_containing_block();
264 $top = $style->length_in_pt($style->top, $cb["h"]);
265 $right = $style->length_in_pt($style->right, $cb["w"]);
266 $bottom = $style->length_in_pt($style->bottom, $cb["h"]);
267 $left = $style->length_in_pt($style->left, $cb["w"]);
268
269 // FIXME RTL case:
270 // if ($left !== "auto" && $right !== "auto") $left = -$right;
271 if ($left === "auto" && $right === "auto") {
272 $left = 0;
273 } elseif ($left === "auto") {
274 $left = -$right;
275 }
276
277 if ($top === "auto" && $bottom === "auto") {
278 $top = 0;
279 } elseif ($top === "auto") {
280 $top = -$bottom;
281 }
282
283 $frame->move($left, $top);
284 }
285 }
286
287 /**
288 * @param Block|null $block
289 */
290 abstract function reflow(Block $block = null);
291
292 /**
293 * Resolve the `min-width` property.
294 *
295 * Resolves to 0 if not set or if a percentage and the containing-block
296 * width is not defined.
297 *
298 * @param float|null $cbw Width of the containing block.
299 *
300 * @return float
301 */
302 protected function resolve_min_width(?float $cbw): float
303 {
304 $style = $this->_frame->get_style();
305 $min_width = $style->min_width;
306
307 return $min_width !== "auto"
308 ? $style->length_in_pt($min_width, $cbw ?? 0)
309 : 0.0;
310 }
311
312 /**
313 * Resolve the `max-width` property.
314 *
315 * Resolves to `INF` if not set or if a percentage and the containing-block
316 * width is not defined.
317 *
318 * @param float|null $cbw Width of the containing block.
319 *
320 * @return float
321 */
322 protected function resolve_max_width(?float $cbw): float
323 {
324 $style = $this->_frame->get_style();
325 $max_width = $style->max_width;
326
327 return $max_width !== "none"
328 ? $style->length_in_pt($max_width, $cbw ?? INF)
329 : INF;
330 }
331
332 /**
333 * Resolve the `min-height` property.
334 *
335 * Resolves to 0 if not set or if a percentage and the containing-block
336 * height is not defined.
337 *
338 * @param float|null $cbh Height of the containing block.
339 *
340 * @return float
341 */
342 protected function resolve_min_height(?float $cbh): float
343 {
344 $style = $this->_frame->get_style();
345 $min_height = $style->min_height;
346
347 return $min_height !== "auto"
348 ? $style->length_in_pt($min_height, $cbh ?? 0)
349 : 0.0;
350 }
351
352 /**
353 * Resolve the `max-height` property.
354 *
355 * Resolves to `INF` if not set or if a percentage and the containing-block
356 * height is not defined.
357 *
358 * @param float|null $cbh Height of the containing block.
359 *
360 * @return float
361 */
362 protected function resolve_max_height(?float $cbh): float
363 {
364 $style = $this->_frame->get_style();
365 $max_height = $style->max_height;
366
367 return $max_height !== "none"
368 ? $style->length_in_pt($style->max_height, $cbh ?? INF)
369 : INF;
370 }
371
372 /**
373 * Get the minimum and maximum preferred width of the contents of the frame,
374 * as requested by its children.
375 *
376 * @return array A two-element array of min and max width.
377 */
378 public function get_min_max_child_width(): array
379 {
380 if (!is_null($this->_min_max_child_cache)) {
381 return $this->_min_max_child_cache;
382 }
383
384 $low = [];
385 $high = [];
386
387 for ($iter = $this->_frame->get_children(); $iter->valid(); $iter->next()) {
388 $inline_min = 0;
389 $inline_max = 0;
390
391 // Add all adjacent inline widths together to calculate max width
392 while ($iter->valid() && ($iter->current()->is_inline_level() || $iter->current()->get_style()->display === "-dompdf-image")) {
393 /** @var AbstractFrameDecorator */
394 $child = $iter->current();
395 $child->get_reflower()->_set_content();
396 $minmax = $child->get_min_max_width();
397
398 if (in_array($child->get_style()->white_space, ["pre", "nowrap"], true)) {
399 $inline_min += $minmax["min"];
400 } else {
401 $low[] = $minmax["min"];
402 }
403
404 $inline_max += $minmax["max"];
405 $iter->next();
406 }
407
408 if ($inline_min > 0) {
409 $low[] = $inline_min;
410 }
411 if ($inline_max > 0) {
412 $high[] = $inline_max;
413 }
414
415 // Skip children with absolute position
416 if ($iter->valid() && !$iter->current()->is_absolute()) {
417 /** @var AbstractFrameDecorator */
418 $child = $iter->current();
419 $child->get_reflower()->_set_content();
420 list($low[], $high[]) = $child->get_min_max_width();
421 }
422 }
423
424 $min = count($low) ? max($low) : 0;
425 $max = count($high) ? max($high) : 0;
426
427 return $this->_min_max_child_cache = [$min, $max];
428 }
429
430 /**
431 * Get the minimum and maximum preferred content-box width of the frame.
432 *
433 * @return array A two-element array of min and max width.
434 */
435 public function get_min_max_content_width(): array
436 {
437 return $this->get_min_max_child_width();
438 }
439
440 /**
441 * Get the minimum and maximum preferred border-box width of the frame.
442 *
443 * Required for shrink-to-fit width calculation, as used in automatic table
444 * layout, absolute positioning, float and inline-block. This provides a
445 * basic implementation. Child classes should override this or
446 * `get_min_max_content_width` as necessary.
447 *
448 * @return array An array `[0 => min, 1 => max, "min" => min, "max" => max]`
449 * of min and max width.
450 */
451 public function get_min_max_width(): array
452 {
453 if (!is_null($this->_min_max_cache)) {
454 return $this->_min_max_cache;
455 }
456
457 $style = $this->_frame->get_style();
458 [$min, $max] = $this->get_min_max_content_width();
459
460 // Account for margins, borders, and padding
461 $dims = [
462 $style->padding_left,
463 $style->padding_right,
464 $style->border_left_width,
465 $style->border_right_width,
466 $style->margin_left,
467 $style->margin_right
468 ];
469
470 // The containing block is not defined yet, treat percentages as 0
471 $delta = (float) $style->length_in_pt($dims, 0);
472 $min += $delta;
473 $max += $delta;
474
475 return $this->_min_max_cache = [$min, $max, "min" => $min, "max" => $max];
476 }
477
478 /**
479 * Parses a CSS string containing quotes and escaped hex characters
480 *
481 * @param $string string The CSS string to parse
482 * @param $single_trim
483 * @return string
484 */
485 protected function _parse_string($string, $single_trim = false)
486 {
487 if ($single_trim) {
488 $string = preg_replace('/^[\"\']/', "", $string);
489 $string = preg_replace('/[\"\']$/', "", $string);
490 } else {
491 $string = trim($string, "'\"");
492 }
493
494 $string = str_replace(["\\\n", '\\"', "\\'"],
495 ["", '"', "'"], $string);
496
497 // Convert escaped hex characters into ascii characters (e.g. \A => newline)
498 $string = preg_replace_callback("/\\\\([0-9a-fA-F]{0,6})/",
499 function ($matches) { return \Dompdf\Helpers::unichr(hexdec($matches[1])); },
500 $string);
501 return $string;
502 }
503
504 /**
505 * Parses a CSS "quotes" property
506 *
507 * https://www.w3.org/TR/css-content-3/#quotes
508 *
509 * @return array An array of pairs of quotes
510 */
511 protected function _parse_quotes(): array
512 {
513 $quotes = $this->_frame->get_style()->quotes;
514
515 if ($quotes === "none") {
516 return [];
517 }
518
519 if ($quotes === "auto") {
520 // TODO: Use typographically appropriate quotes for the current
521 // language here
522 return [['"', '"'], ["'", "'"]];
523 }
524
525 // Matches quote types
526 $re = '/(\'[^\']*\')|(\"[^\"]*\")/';
527
528 // Split on spaces, except within quotes
529 if (!preg_match_all($re, $quotes, $matches, PREG_SET_ORDER)) {
530 return [];
531 }
532
533 $quotes_array = [];
534 foreach ($matches as $_quote) {
535 $quotes_array[] = $this->_parse_string($_quote[0], true);
536 }
537
538 return array_chunk($quotes_array, 2);
539 }
540
541 /**
542 * Parses the CSS "content" property
543 *
544 * https://www.w3.org/TR/CSS21/generate.html#content
545 *
546 * @return string The resulting string
547 */
548 protected function _parse_content(): string
549 {
550 $style = $this->_frame->get_style();
551 $content = $style->content;
552
553 if ($content === "normal" || $content === "none") {
554 return "";
555 }
556
557 $quotes = $this->_parse_quotes();
558 $text = "";
559
560 foreach ($content as $val) {
561 // String
562 if (in_array(mb_substr($val, 0, 1), ['"', "'"], true)) {
563 $text .= $this->_parse_string($val);
564 continue;
565 }
566
567 $val = mb_strtolower($val);
568
569 // Keywords
570 if ($val === "open-quote") {
571 // FIXME: Take quotation depth into account
572 if (isset($quotes[0][0])) {
573 $text .= $quotes[0][0];
574 }
575 continue;
576 } elseif ($val === "close-quote") {
577 // FIXME: Take quotation depth into account
578 if (isset($quotes[0][1])) {
579 $text .= $quotes[0][1];
580 }
581 continue;
582 } elseif ($val === "no-open-quote") {
583 // FIXME: Increment quotation depth
584 continue;
585 } elseif ($val === "no-close-quote") {
586 // FIXME: Decrement quotation depth
587 continue;
588 }
589
590 // attr()
591 if (mb_substr($val, 0, 5) === "attr(") {
592 $i = mb_strpos($val, ")");
593 if ($i === false) {
594 continue;
595 }
596
597 $attr = trim(mb_substr($val, 5, $i - 5));
598 if ($attr === "") {
599 continue;
600 }
601
602 $text .= $this->_frame->get_parent()->get_node()->getAttribute($attr);
603 continue;
604 }
605
606 // counter()/counters()
607 if (mb_substr($val, 0, 7) === "counter") {
608 // Handle counter() references:
609 // http://www.w3.org/TR/CSS21/generate.html#content
610
611 $i = mb_strpos($val, ")");
612 if ($i === false) {
613 continue;
614 }
615
616 preg_match('/(counters?)(^\()*?\(\s*([^\s,]+)\s*(,\s*["\']?([^"\'\)]*)["\']?\s*(,\s*([^\s)]+)\s*)?)?\)/i', $val, $args);
617 $counter_id = $args[3];
618
619 if (strtolower($args[1]) === "counter") {
620 // counter(name [,style])
621 if (isset($args[5])) {
622 $type = trim($args[5]);
623 } else {
624 $type = "decimal";
625 }
626 $p = $this->_frame->lookup_counter_frame($counter_id);
627
628 $text .= $p->counter_value($counter_id, $type);
629 } elseif (strtolower($args[1]) === "counters") {
630 // counters(name, string [,style])
631 if (isset($args[5])) {
632 $string = $this->_parse_string($args[5]);
633 } else {
634 $string = "";
635 }
636
637 if (isset($args[7])) {
638 $type = trim($args[7]);
639 } else {
640 $type = "decimal";
641 }
642
643 $p = $this->_frame->lookup_counter_frame($counter_id);
644 $tmp = [];
645 while ($p) {
646 // We only want to use the counter values when they actually increment the counter
647 if (array_key_exists($counter_id, $p->_counters)) {
648 array_unshift($tmp, $p->counter_value($counter_id, $type));
649 }
650 $p = $p->lookup_counter_frame($counter_id);
651 }
652 $text .= implode($string, $tmp);
653 } else {
654 // countertops?
655 }
656
657 continue;
658 }
659 }
660
661 return $text;
662 }
663
664 /**
665 * Handle counters and set generated content if the frame is a
666 * generated-content frame.
667 */
668 protected function _set_content(): void
669 {
670 $frame = $this->_frame;
671
672 if ($frame->content_set) {
673 return;
674 }
675
676 $style = $frame->get_style();
677
678 if (($reset = $style->counter_reset) !== "none") {
679 $frame->reset_counters($reset);
680 }
681
682 if (($increment = $style->counter_increment) !== "none") {
683 $frame->increment_counters($increment);
684 }
685
686 if ($frame->get_node()->nodeName === "dompdf_generated") {
687 $content = $this->_parse_content();
688
689 if ($content !== "") {
690 $node = $frame->get_node()->ownerDocument->createTextNode($content);
691
692 $new_style = $style->get_stylesheet()->create_style();
693 $new_style->inherit($style);
694
695 $new_frame = new Frame($node);
696 $new_frame->set_style($new_style);
697
698 Factory::decorate_frame($new_frame, $frame->get_dompdf(), $frame->get_root());
699 $frame->append_child($new_frame);
700 }
701 }
702
703 $frame->content_set = true;
704 }
705 }
706