4 * @link http://dompdf.github.com/
5 * @author Benj Carson <benjcarson@digitaljunkies.ca>
6 * @author Fabien Ménager <fabien.menager@gmail.com>
7 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
9 namespace Dompdf\FrameReflower
;
12 use Dompdf\FrameDecorator\Block
as BlockFrameDecorator
;
13 use Dompdf\FrameDecorator\TableCell
as TableCellFrameDecorator
;
14 use Dompdf\FrameDecorator\Text
as TextFrameDecorator
;
19 * Reflows block frames
23 class Block
extends AbstractFrameReflower
25 // Minimum line width to justify, as fraction of available width
26 const MIN_JUSTIFY_WIDTH
= 0.80;
29 * @var BlockFrameDecorator
33 function __construct(BlockFrameDecorator
$frame)
35 parent
::__construct($frame);
39 * Calculate the ideal used value for the width property as per:
40 * http://www.w3.org/TR/CSS21/visudet.html#Computing_widths_and_margins
46 protected function _calculate_width($width)
48 $frame = $this->_frame
;
49 $style = $frame->get_style();
50 $w = $frame->get_containing_block("w");
52 if ($style->position
=== "fixed") {
53 $w = $frame->get_parent()->get_containing_block("w");
56 $rm = $style->length_in_pt($style->margin_right
, $w);
57 $lm = $style->length_in_pt($style->margin_left
, $w);
59 $left = $style->length_in_pt($style->left
, $w);
60 $right = $style->length_in_pt($style->right
, $w);
62 // Handle 'auto' values
63 $dims = array($style->border_left_width
,
64 $style->border_right_width
,
66 $style->padding_right
,
67 $width !== "auto" ?
$width : 0,
68 $rm !== "auto" ?
$rm : 0,
69 $lm !== "auto" ?
$lm : 0);
71 // absolutely positioned boxes take the 'left' and 'right' properties into account
72 if ($frame->is_absolute()) {
74 $dims[] = $left !== "auto" ?
$left : 0;
75 $dims[] = $right !== "auto" ?
$right : 0;
80 $sum = (float)$style->length_in_pt($dims, $w);
82 // Compare to the containing block
87 // resolve auto properties: see
88 // http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
90 if ($width === "auto" && $left === "auto" && $right === "auto") {
98 // Technically, the width should be "shrink-to-fit" i.e. based on the
99 // preferred width of the content... a little too costly here as a
100 // special case. Just get the width to take up the slack:
104 } else if ($width === "auto") {
105 if ($lm === "auto") {
108 if ($rm === "auto") {
111 if ($left === "auto") {
114 if ($right === "auto") {
119 } else if ($left === "auto") {
120 if ($lm === "auto") {
123 if ($rm === "auto") {
126 if ($right === "auto") {
131 } else if ($right === "auto") {
132 if ($lm === "auto") {
135 if ($rm === "auto") {
143 // Find auto properties and get them to take up the slack
144 if ($width === "auto") {
146 } else if ($lm === "auto" && $rm === "auto") {
147 $lm = $rm = round($diff / 2);
148 } else if ($lm === "auto") {
150 } else if ($rm === "auto") {
154 } else if ($diff < 0) {
155 // We are over constrained--set margin-right to the difference
161 "margin_left" => $lm,
162 "margin_right" => $rm,
169 * Call the above function, but resolve max/min widths
174 protected function _calculate_restricted_width()
176 $frame = $this->_frame
;
177 $style = $frame->get_style();
178 $cb = $frame->get_containing_block();
180 if ($style->position
=== "fixed") {
181 $cb = $frame->get_root()->get_containing_block();
184 //if ( $style->position === "absolute" )
185 // $cb = $frame->find_positionned_parent()->get_containing_block();
187 if (!isset($cb["w"])) {
188 throw new Exception("Box property calculation requires containing block width");
191 // Treat width 100% as auto
192 if ($style->width
=== "100%") {
195 $width = $style->length_in_pt($style->width
, $cb["w"]);
198 $calculate_width = $this->_calculate_width($width);
199 $margin_left = $calculate_width['margin_left'];
200 $margin_right = $calculate_width['margin_right'];
201 $width = $calculate_width['width'];
202 $left = $calculate_width['left'];
203 $right = $calculate_width['right'];
205 // Handle min/max width
206 $min_width = $style->length_in_pt($style->min_width
, $cb["w"]);
207 $max_width = $style->length_in_pt($style->max_width
, $cb["w"]);
209 if ($max_width !== "none" && $min_width > $max_width) {
210 list($max_width, $min_width) = array($min_width, $max_width);
213 if ($max_width !== "none" && $width > $max_width) {
214 extract($this->_calculate_width($max_width));
217 if ($width < $min_width) {
218 $calculate_width = $this->_calculate_width($min_width);
219 $margin_left = $calculate_width['margin_left'];
220 $margin_right = $calculate_width['margin_right'];
221 $width = $calculate_width['width'];
222 $left = $calculate_width['left'];
223 $right = $calculate_width['right'];
226 return array($width, $margin_left, $margin_right, $left, $right);
230 * Determine the unrestricted height of content within the block
231 * not by adding each line's height, but by getting the last line's position.
232 * This because lines could have been pushed lower by a clearing element.
236 protected function _calculate_content_height()
239 $lines = $this->_frame
->get_line_boxes();
240 if (count($lines) > 0) {
241 $last_line = end($lines);
242 $content_box = $this->_frame
->get_content_box();
243 $height = $last_line->y +
$last_line->h
- $content_box["y"];
249 * Determine the frame's restricted height
253 protected function _calculate_restricted_height()
255 $frame = $this->_frame
;
256 $style = $frame->get_style();
257 $content_height = $this->_calculate_content_height();
258 $cb = $frame->get_containing_block();
260 $height = $style->length_in_pt($style->height
, $cb["h"]);
262 $top = $style->length_in_pt($style->top
, $cb["h"]);
263 $bottom = $style->length_in_pt($style->bottom
, $cb["h"]);
265 $margin_top = $style->length_in_pt($style->margin_top
, $cb["h"]);
266 $margin_bottom = $style->length_in_pt($style->margin_bottom
, $cb["h"]);
268 if ($frame->is_absolute()) {
270 // see http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-height
272 $dims = array($top !== "auto" ?
$top : 0,
273 $style->margin_top
!== "auto" ?
$style->margin_top
: 0,
275 $style->border_top_width
,
276 $height !== "auto" ?
$height : 0,
277 $style->border_bottom_width
,
278 $style->padding_bottom
,
279 $style->margin_bottom
!== "auto" ?
$style->margin_bottom
: 0,
280 $bottom !== "auto" ?
$bottom : 0);
282 $sum = (float)$style->length_in_pt($dims, $cb["h"]);
284 $diff = $cb["h"] - $sum;
287 if ($height === "auto" && $top === "auto" && $bottom === "auto") {
288 if ($margin_top === "auto") {
291 if ($margin_bottom === "auto") {
296 } else if ($height === "auto" && $top === "auto") {
297 if ($margin_top === "auto") {
300 if ($margin_bottom === "auto") {
304 $height = $content_height;
305 $top = $diff - $content_height;
306 } else if ($height === "auto" && $bottom === "auto") {
307 if ($margin_top === "auto") {
310 if ($margin_bottom === "auto") {
314 $height = $content_height;
315 $bottom = $diff - $content_height;
316 } else if ($top === "auto" && $bottom === "auto") {
317 if ($margin_top === "auto") {
320 if ($margin_bottom === "auto") {
325 } else if ($top === "auto") {
326 if ($margin_top === "auto") {
329 if ($margin_bottom === "auto") {
334 } else if ($height === "auto") {
335 if ($margin_top === "auto") {
338 if ($margin_bottom === "auto") {
343 } else if ($bottom === "auto") {
344 if ($margin_top === "auto") {
347 if ($margin_bottom === "auto") {
353 if ($style->overflow
=== "visible") {
354 // set all autos to zero
355 if ($margin_top === "auto") {
358 if ($margin_bottom === "auto") {
361 if ($top === "auto") {
364 if ($bottom === "auto") {
367 if ($height === "auto") {
368 $height = $content_height;
372 // FIXME: overflow hidden
377 // Expand the height if overflow is visible
378 if ($height === "auto" && $content_height > $height /* && $style->overflow === "visible" */) {
379 $height = $content_height;
382 // FIXME: this should probably be moved to a seperate function as per
383 // _calculate_restricted_width
385 // Only handle min/max height if the height is independent of the frame's content
386 if (!($style->overflow
=== "visible" ||
($style->overflow
=== "hidden" && $height === "auto"))) {
387 $min_height = $style->min_height
;
388 $max_height = $style->max_height
;
390 if (isset($cb["h"])) {
391 $min_height = $style->length_in_pt($min_height, $cb["h"]);
392 $max_height = $style->length_in_pt($max_height, $cb["h"]);
393 } else if (isset($cb["w"])) {
394 if (mb_strpos($min_height, "%") !== false) {
397 $min_height = $style->length_in_pt($min_height, $cb["w"]);
400 if (mb_strpos($max_height, "%") !== false) {
401 $max_height = "none";
403 $max_height = $style->length_in_pt($max_height, $cb["w"]);
407 if ($max_height !== "none" && $min_height > $max_height) {
409 list($max_height, $min_height) = array($min_height, $max_height);
412 if ($max_height !== "none" && $height > $max_height) {
413 $height = $max_height;
416 if ($height < $min_height) {
417 $height = $min_height;
422 return array($height, $margin_top, $margin_bottom, $top, $bottom);
426 * Adjust the justification of each of our lines.
427 * http://www.w3.org/TR/CSS21/text.html#propdef-text-align
429 protected function _text_align()
431 $style = $this->_frame
->get_style();
432 $w = $this->_frame
->get_containing_block("w");
433 $width = (float)$style->length_in_pt($style->width
, $w);
435 switch ($style->text_align
) {
438 foreach ($this->_frame
->get_line_boxes() as $line) {
443 foreach ($line->get_frames() as $frame) {
444 if ($frame instanceof BlockFrameDecorator
) {
447 $frame->set_position($frame->get_position("x") +
$line->left
);
453 foreach ($this->_frame
->get_line_boxes() as $line) {
454 // Move each child over by $dx
455 $dx = $width - $line->w
- $line->right
;
457 foreach ($line->get_frames() as $frame) {
458 // Block frames are not aligned by text-align
459 if ($frame instanceof BlockFrameDecorator
) {
463 $frame->set_position($frame->get_position("x") +
$dx);
469 // We justify all lines except the last one
470 $lines = $this->_frame
->get_line_boxes(); // needs to be a variable (strict standards)
471 $last_line = array_pop($lines);
473 foreach ($lines as $i => $line) {
479 // One space character's width. Will be used to get a more accurate spacing
480 $space_width = $this->get_dompdf()->getFontMetrics()->getTextWidth(" ", $style->font_family
, $style->font_size
);
482 foreach ($lines as $line) {
484 foreach ($line->get_frames() as $frame) {
485 if (!$frame instanceof TextFrameDecorator
) {
489 $frame->set_position($frame->get_position("x") +
$line->left
);
493 // Set the spacing for each child
495 $spacing = ($width - ($line->left +
$line->w +
$line->right
) +
$space_width) / ($line->wc
- 1);
501 foreach ($line->get_frames() as $frame) {
502 if (!$frame instanceof TextFrameDecorator
) {
506 $text = $frame->get_text();
507 $spaces = mb_substr_count($text, " ");
509 $char_spacing = (float)$style->length_in_pt($style->letter_spacing
);
510 $_spacing = $spacing +
$char_spacing;
512 $frame->set_position($frame->get_position("x") +
$dx);
513 $frame->set_text_spacing($_spacing);
515 $dx +
= $spaces * $_spacing;
518 // The line (should) now occupy the entire width
522 // Adjust the last line if necessary
523 if ($last_line->left
) {
524 foreach ($last_line->get_frames() as $frame) {
525 if ($frame instanceof BlockFrameDecorator
) {
528 $frame->set_position($frame->get_position("x") +
$last_line->left
);
535 foreach ($this->_frame
->get_line_boxes() as $line) {
536 // Centre each line by moving each frame in the line by:
537 $dx = ($width +
$line->left
- $line->w
- $line->right
) / 2;
539 foreach ($line->get_frames() as $frame) {
540 // Block frames are not aligned by text-align
541 if ($frame instanceof BlockFrameDecorator
) {
545 $frame->set_position($frame->get_position("x") +
$dx);
553 * Align inline children vertically.
554 * Aligns each child vertically after each line is reflowed
556 function vertical_align()
560 foreach ($this->_frame
->get_line_boxes() as $line) {
564 foreach ($line->get_frames() as $frame) {
565 $style = $frame->get_style();
567 '-dompdf-image' === $style->display
568 ||
'inline-block' === $style->display
569 ||
'inline-table' === $style->display
571 if (!$isInlineBlock && $style->display
!== "inline") {
575 if (!isset($canvas)) {
576 $canvas = $frame->get_root()->get_dompdf()->get_canvas();
579 $baseline = $canvas->get_font_baseline($style->font_family
, $style->font_size
);
582 //FIXME: The 0.8 ratio applied to the height is arbitrary (used to accommodate descenders?)
584 $lineFrames = $line->get_frames();
585 if (count($lineFrames) == 1) {
588 $frameBox = $frame->get_frame()->get_border_box();
589 $imageHeightDiff = $height * 0.8 - (float)$frameBox['h'];
591 $align = $frame->get_style()->vertical_align
;
592 if (in_array($align, Style
::$vertical_align_keywords) === true) {
595 $y_offset = $imageHeightDiff / 2;
599 $y_offset = 0.3 * $height +
$imageHeightDiff;
603 $y_offset = -0.2 * $height +
$imageHeightDiff;
606 case "text-top": // FIXME: this should be the height of the frame minus the height of the text
607 $y_offset = $height - (float)$style->length_in_pt($style->get_line_height(), $style->font_size
);
613 case "text-bottom": // FIXME: align bottom of image with the descender?
615 $y_offset = 0.3 * $height +
$imageHeightDiff;
620 $y_offset = $imageHeightDiff;
624 $y_offset = $baseline - (float)$style->length_in_pt($align, $style->font_size
) - (float)$frameBox['h'];
627 $parent = $frame->get_parent();
628 if ($parent instanceof TableCellFrameDecorator
) {
631 $align = $parent->get_style()->vertical_align
;
633 if (in_array($align, Style
::$vertical_align_keywords) === true) {
636 $y_offset = ($height * 0.8 - $baseline) / 2;
640 $y_offset = $height * 0.8 - $baseline * 0.5;
644 $y_offset = $height * 0.8 - $baseline * 1.4;
648 case "top": // Not strictly accurate, but good enough for now
653 $y_offset = $height * 0.8 - $baseline;
658 $y_offset = $height * 0.8 - $baseline;
662 $y_offset = $height * 0.8 - $baseline - (float)$style->length_in_pt($align, $style->font_size
);
666 if ($y_offset !== 0) {
667 $frame->move(0, $y_offset);
674 * @param Frame $child
676 function process_clear(Frame
$child)
678 $child_style = $child->get_style();
679 $root = $this->_frame
->get_root();
682 if ($child_style->clear
!== "none") {
683 //TODO: this is a WIP for handling clear/float frames that are in between inline frames
684 if ($child->get_prev_sibling() !== null) {
685 $this->_frame
->add_line();
687 if ($child_style->float !== "none" && $child->get_next_sibling()) {
688 $this->_frame
->set_current_line_number($this->_frame
->get_current_line_number() - 1);
691 $lowest_y = $root->get_lowest_float_offset($child);
693 // If a float is still applying, we handle it
695 if ($child->is_in_flow()) {
696 $line_box = $this->_frame
->get_current_line_box();
697 $line_box->y
= $lowest_y +
$child->get_margin_height();
699 $line_box->right
= 0;
702 $child->move(0, $lowest_y - $child->get_position("y"));
708 * @param Frame $child
712 function process_float(Frame
$child, $cb_x, $cb_w)
714 $child_style = $child->get_style();
715 $root = $this->_frame
->get_root();
718 if ($child_style->float !== "none") {
719 $root->add_floating_frame($child);
721 // Remove next frame's beginning whitespace
722 $next = $child->get_next_sibling();
723 if ($next && $next instanceof TextFrameDecorator
) {
724 $next->set_text(ltrim($next->get_text()));
727 $line_box = $this->_frame
->get_current_line_box();
728 list($old_x, $old_y) = $child->get_position();
732 $float_w = $child->get_margin_width();
734 if ($child_style->clear
=== "none") {
735 switch ($child_style->float) {
737 $float_x +
= $line_box->left
;
740 $float_x +
= ($cb_w - $line_box->right
- $float_w);
744 if ($child_style->float === "right") {
745 $float_x +
= ($cb_w - $float_w);
749 if ($cb_w < $float_x +
$float_w - $old_x) {
750 // TODO handle when floating elements don't fit
753 $line_box->get_float_offsets();
755 if ($child->_float_next_line
) {
756 $float_y +
= $line_box->h
;
759 $child->set_position($float_x, $float_y);
760 $child->move($float_x - $old_x, $float_y - $old_y, true);
765 * @param BlockFrameDecorator $block
768 function reflow(BlockFrameDecorator
$block = null)
771 // Check if a page break is forced
772 $page = $this->_frame
->get_root();
773 $page->check_forced_page_break($this->_frame
);
775 // Bail if the page is full
776 if ($page->is_full()) {
781 $this->_set_content();
783 // Collapse margins if required
784 $this->_collapse_margins();
786 $style = $this->_frame
->get_style();
787 $cb = $this->_frame
->get_containing_block();
789 if ($style->position
=== "fixed") {
790 $cb = $this->_frame
->get_root()->get_containing_block();
793 // Determine the constraints imposed by this frame: calculate the width
794 // of the content area:
795 list($w, $left_margin, $right_margin, $left, $right) = $this->_calculate_restricted_width();
797 // Store the calculated properties
799 $style->margin_left
= $left_margin;
800 $style->margin_right
= $right_margin;
801 $style->left
= $left;
802 $style->right
= $right;
804 // Update the position
805 $this->_frame
->position();
806 list($x, $y) = $this->_frame
->get_position();
808 // Adjust the first line based on the text-indent property
809 $indent = (float)$style->length_in_pt($style->text_indent
, $cb["w"]);
810 $this->_frame
->increase_line_width($indent);
812 // Determine the content edge
813 $top = (float)$style->length_in_pt(array($style->margin_top
,
815 $style->border_top_width
), $cb["h"]);
817 $bottom = (float)$style->length_in_pt(array($style->border_bottom_width
,
818 $style->margin_bottom
,
819 $style->padding_bottom
), $cb["h"]);
821 $cb_x = $x +
(float)$left_margin +
(float)$style->length_in_pt(array($style->border_left_width
,
822 $style->padding_left
), $cb["w"]);
826 $cb_h = ($cb["h"] +
$cb["y"]) - $bottom - $cb_y;
828 // Set the y position of the first line in this block
829 $line_box = $this->_frame
->get_current_line_box();
830 $line_box->y
= $cb_y;
831 $line_box->get_float_offsets();
833 // Set the containing blocks and reflow each child
834 foreach ($this->_frame
->get_children() as $child) {
836 // Bail out if the page is full
837 if ($page->is_full()) {
841 $child->set_containing_block($cb_x, $cb_y, $w, $cb_h);
843 $this->process_clear($child);
845 $child->reflow($this->_frame
);
847 // Don't add the child to the line if a page break has occurred
848 if ($page->check_page_break($child)) {
852 $this->process_float($child, $cb_x, $w);
855 // Determine our height
856 list($height, $margin_top, $margin_bottom, $top, $bottom) = $this->_calculate_restricted_height();
857 $style->height
= $height;
858 $style->margin_top
= $margin_top;
859 $style->margin_bottom
= $margin_bottom;
861 $style->bottom
= $bottom;
863 $orig_style = $this->_frame
->get_original_style();
865 $needs_reposition = ($style->position
=== "absolute" && ($style->right
!== "auto" ||
$style->bottom
!== "auto"));
867 // Absolute positioning measurement
868 if ($needs_reposition) {
869 if ($orig_style->width
=== "auto" && ($orig_style->left
=== "auto" ||
$orig_style->right
=== "auto")) {
871 foreach ($this->_frame
->get_line_boxes() as $line) {
872 $width = max($line->w
, $width);
874 $style->width
= $width;
877 $style->left
= $orig_style->left
;
878 $style->right
= $orig_style->right
;
881 // Calculate inline-block / float auto-widths
882 if (($style->display
=== "inline-block" ||
$style->float !== 'none') && $orig_style->width
=== 'auto') {
885 foreach ($this->_frame
->get_line_boxes() as $line) {
886 $line->recalculate_width();
888 $width = max($line->w
, $width);
892 foreach ($this->_frame
->get_children() as $child) {
893 $width +
= $child->calculate_auto_width();
897 $style->width
= $width;
900 $this->_text_align();
901 $this->vertical_align();
903 // Absolute positioning
904 if ($needs_reposition) {
905 list($x, $y) = $this->_frame
->get_position();
906 $this->_frame
->position();
907 list($new_x, $new_y) = $this->_frame
->get_position();
908 $this->_frame
->move($new_x - $x, $new_y - $y, true);
911 if ($block && $this->_frame
->is_in_flow()) {
912 $block->add_frame_to_line($this->_frame
);
914 // May be inline-block
915 if ($style->display
=== "block") {
922 * Determine current frame width based on contents
926 public function calculate_auto_width()
930 foreach ($this->_frame
->get_line_boxes() as $line) {
933 foreach ($line->get_frames() as $frame) {
934 if ($frame->get_original_style()->width
== 'auto') {
935 $line_width +
= $frame->calculate_auto_width();
937 $line_width +
= $frame->get_margin_width();
941 $width = max($line_width, $width);
944 $this->_frame
->get_style()->width
= $width;
946 return $this->_frame
->get_margin_width();