diff --git a/src/FrameDecorator/AbstractFrameDecorator.php b/src/FrameDecorator/AbstractFrameDecorator.php index 2cd7898b7..cce28c79d 100644 --- a/src/FrameDecorator/AbstractFrameDecorator.php +++ b/src/FrameDecorator/AbstractFrameDecorator.php @@ -107,6 +107,13 @@ abstract class AbstractFrameDecorator extends Frame */ public $is_split = false; + /** + * Cache for the calculation of the outer baseline height + * + * @var Frame + */ + private $_cached_outer_baseline_height; + /** * Class constructor * @@ -339,6 +346,50 @@ public function get_border_box(): array return $this->_frame->get_border_box(); } + /** + * Return the height of the baseline for the object + * + * @return float + */ + public function get_outer_baseline_height() + { + if (isset($this->_cached_outer_baseline_height)) { + return $this->_cached_outer_baseline_height; + } + + $style = $this->_frame->get_style(); + $baseline_height = $this->_dompdf->getCanvas()->get_font_baseline($style->font_family, $style->font_size); + + $isInlineBlock = $style->display !== "inline" + && $style->display !== "-dompdf-list-bullet"; + + if ($this instanceof \Dompdf\FrameDecorator\Image) { + $baseline_height = $style->length_in_pt([ + $this->_frame->get_border_box()['h'], + $style->margin_top + ]); + } else if ($isInlineBlock) { + // TODO: this loop is excessive for a block with multiple lines since + // we only need the baseline height of the last line in the block + // (plus the position of the line) + /** @var AbstractFrameDecorator $child */ + foreach ($this->get_children() as $child) { + if (!$child->is_in_flow()) { + continue; + } + $child_baseline_height = $child->get_outer_baseline_height(); + $y_height = $child->get_position("y") - $this->get_content_box()["y"]; + if ($child_baseline_height + $y_height > $baseline_height) { + $baseline_height = $child_baseline_height + $y_height; + } + } + $baseline_height += $style->length_in_pt([$style->margin_top]); + } + + return $this->_cached_outer_baseline_height = $baseline_height; + } + + function set_id($id) { $this->_frame->set_id($id); diff --git a/src/FrameReflower/Block.php b/src/FrameReflower/Block.php index 9b5daccd2..0264e3ad4 100644 --- a/src/FrameReflower/Block.php +++ b/src/FrameReflower/Block.php @@ -559,9 +559,27 @@ protected function _text_align() function vertical_align() { $fontMetrics = $this->get_dompdf()->getFontMetrics(); + $style = $this->_frame->get_style(); + $baseline_height = $fontMetrics->getFontBaseline($style->font_family, $style->font_size); + + $line_height = $fontMetrics->getFontBaseline($style->font_family, $style->line_height); + $nominal_line_height = $fontMetrics->getFontBaseline($style->font_family, $style->font_size * Style::$default_line_height); + $baseline_height_adjustment = 0; + if ($line_height > $nominal_line_height) { + $baseline_height_adjustment += $line_height - $baseline_height; + } + + $line_top_adjustment = 0.0; + $line_bottom_adjustment = 0.0; foreach ($this->_frame->get_line_boxes() as $line) { - $height = $line->h; + $line->y += $line_top_adjustment + $line_bottom_adjustment; + $line_top_adjustment = 0.0; + $line_bottom_adjustment = 0.0; + + $line_baseline_height = $baseline_height; + $top_frames = []; + $bottom_frames = []; // Move all markers to the top of the line box foreach ($line->get_list_markers() as $marker) { @@ -570,115 +588,108 @@ function vertical_align() } foreach ($line->frames_to_align() as $frame) { - $style = $frame->get_style(); - $isInlineBlock = $style->display !== "inline" - && $style->display !== "-dompdf-list-bullet"; - - $baseline = $fontMetrics->getFontBaseline($style->font_family, $style->font_size); - $y_offset = 0; - - //FIXME: The 0.8 ratio applied to the height is arbitrary (used to accommodate descenders?) - if ($isInlineBlock) { - // Workaround: Skip vertical alignment if the frame is the - // only one one the line, excluding empty text frames, which - // may be the result of trailing white space - // FIXME: This special case should be removed once vertical - // alignment is properly fixed - $skip = true; - - foreach ($line->get_frames() as $other) { - if ($other !== $frame - && !($other->is_text_node() && $other->get_node()->nodeValue === "") - ) { - $skip = false; - break; - } - } + $frame_baseline_height = $frame->get_outer_baseline_height(); + if ($frame_baseline_height > $line_baseline_height) { + $line_baseline_height = $frame_baseline_height; + } + } - if ($skip) { - continue; - } + foreach ($line->frames_to_align() as $frame) { + $frame_style = $frame->get_style(); + $frame_baseline_height = $frame->get_outer_baseline_height(); + $frame_height = $frame->get_margin_height(); + + $parent = $frame->get_parent(); + if (!$frame->is_inline_level()) { + $align = $frame_style->vertical_align; + } else if ($parent instanceof TableCellFrameDecorator) { + $align = "baseline"; + } else { + $align = $parent->get_style()->vertical_align; + } - $marginHeight = $frame->get_margin_height(); - $imageHeightDiff = $height * 0.8 - $marginHeight; + $y_offset = 0.0; + if (in_array($align, Style::$vertical_align_keywords, true)) { + switch ($align) { + case "middle": + // Aligns the middle of the element with the baseline plus half the x-height of the parent + $y_offset = $line_baseline_height - ($frame_height / 2.0) - ($fontMetrics->getFontHeight($style->font_family, $style->font_size) / 5.0); + break; - $align = $frame->get_style()->vertical_align; - if (in_array($align, Style::$vertical_align_keywords, true)) { - switch ($align) { - case "middle": - $y_offset = $imageHeightDiff / 2; - break; + case "sub": + // Aligns the baseline of the element with the subscript-baseline of its parent + $y_offset = ($line_baseline_height - $frame_baseline_height) + ($baseline_height * 0.3); + break; - case "sub": - $y_offset = 0.3 * $height + $imageHeightDiff; - break; + case "super": + // Aligns the baseline of the element with the superscript-baseline of its parent + $y_offset = ($line_baseline_height - $frame_baseline_height) - ($baseline_height * 0.5); + break; - case "super": - $y_offset = -0.2 * $height + $imageHeightDiff; - break; + case "text-top": + // Aligns the top of the element with the top of the parent element's font + $y_offset = $line_baseline_height - $style->line_height; + break; - case "text-top": // FIXME: this should be the height of the frame minus the height of the text - $y_offset = $height - $style->line_height; - break; + case "top": + // Aligns the top of the element and its descendants with the top of the entire line + // ... *after* other adjustments :/ + $top_frames[] = $frame; + break; - case "top": - break; + case "text-bottom": + // Aligns the bottom of the element with the bottom of the parent element's font + $y_offset = $line->h - $frame_height; + break; - case "text-bottom": // FIXME: align bottom of image with the descender? - case "bottom": - $y_offset = 0.3 * $height + $imageHeightDiff; - break; + case "bottom": + // Aligns the bottom of the element and its descendants with the bottom of the entire line + // ... *after* other adjustments :/ + $y_offset = $line->h - $frame_height; + $bottom_frames[] = $frame; + break; - case "baseline": - default: - $y_offset = $imageHeightDiff; - break; - } - } else { - $y_offset = $baseline - (float)$style->length_in_pt($align, $style->font_size) - $marginHeight; + case "baseline": + default: + // Aligns the baseline of the element with the baseline of its parent + $y_offset = $line_baseline_height - $frame_baseline_height; + break; } + } else if (Helpers::is_percent($align)) { + // Aligns the baseline of the element to the given percentage above the baseline of its parent, with the value being a percentage of the line-height property + $y_offset = $line_baseline_height - $frame_baseline_height - (float)$style->length_in_pt($align, $style->line_height); } else { - $parent = $frame->get_parent(); - if ($parent instanceof TableCellFrameDecorator) { - $align = "baseline"; - } else { - $align = $parent->get_style()->vertical_align; - } - if (in_array($align, Style::$vertical_align_keywords, true)) { - switch ($align) { - case "middle": - $y_offset = ($height * 0.8 - $baseline) / 2; - break; - - case "sub": - $y_offset = $height * 0.8 - $baseline * 0.5; - break; - - case "super": - $y_offset = $height * 0.8 - $baseline * 1.4; - break; - - case "text-top": - case "top": // Not strictly accurate, but good enough for now - break; - - case "text-bottom": - case "bottom": - $y_offset = $height * 0.8 - $baseline; - break; - - case "baseline": - default: - $y_offset = $height * 0.8 - $baseline; - break; + // Aligns the baseline of the element to the given length above the baseline of its parent. + $y_offset = $line_baseline_height - $frame_baseline_height - (float)$frame_style->length_in_pt($align, $style->font_size); + } + + $y_offset += $baseline_height_adjustment; + + if (!Helpers::lengthEqual($y_offset, 0)) { + $frame->move(0, $y_offset); + if ($frame->get_position("y") < $line->y) { + if ($line->y - $frame->get_position("y") > $line_top_adjustment) { + $line_top_adjustment = $line->y - $frame->get_position("y"); + } + } else if ($frame->get_position("y") + $frame_height > $line->y + $line->h) { + if (abs($line->y + $line->h - $frame->get_position("y") - $frame_height) > $line_bottom_adjustment) { + $line_bottom_adjustment = abs($line->y + $line->h - $frame->get_position("y") - $frame_height); } - } else { - $y_offset = $height * 0.8 - $baseline - (float)$style->length_in_pt($align, $style->font_size); } } + } - if ($y_offset !== 0) { - $frame->move(0, $y_offset); + $line->h += $line_top_adjustment + $line_bottom_adjustment; + $style->height += $line_top_adjustment + $line_bottom_adjustment; + if ($line_top_adjustment > 0) { + foreach ($line->get_frames() as $frame) { + if (in_array($frame, $top_frames, true)) { + continue; + } else if (in_array($frame, $bottom_frames, true)) { + $frame->move(0, $line->h - $frame->get_margin_height()); + } else { + $frame->move(0, $line_top_adjustment); + } } } }