Skip to content

Commit

Permalink
Improve vertical alignment and related adjustments
Browse files Browse the repository at this point in the history
This change simplifies the vertical alignment logic by using a consistent
frame of reference for djustments.

1. Determine the line baseline by calculating the "outer baseline" of the frames
   contained on the line.
2. For each frame on a line calculate the vertical alignment adjustment
3. Add the line height difference to the previous calculation
4. Re-position the frame per the calculated adjustment
4. Determine if the line height is adjusted after the frame is aligned
5. Re-position any following lines if the previous line height was adjusted

The outer baseline height calculation is cached to aid in performance
since this operation is performed inside-out after the frame has reflowed.
  • Loading branch information
bsweeney committed Jun 24, 2022
1 parent 71a1e1d commit 56d1557
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 93 deletions.
59 changes: 59 additions & 0 deletions src/FrameDecorator/AbstractFrameDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ abstract class AbstractFrameDecorator extends Frame
*/
public $is_split_off = false;

/**
* Cache for the calculation of the outer baseline height
*
* @var Frame
*/
private $_cached_outer_baseline_height;

/**
* Class constructor
*
Expand Down Expand Up @@ -360,6 +367,58 @@ 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) {
// per https://drafts.csswg.org/css2/#propdef-vertical-align
// The baseline of an inline-block is the baseline of its last line box
// in the normal flow...
$hasInflowChild = false;
/** @var AbstractFrameDecorator $child */
foreach ($this->get_children() as $child) {
if (!$child->is_in_flow()) {
continue;
}
$hasInflowChild = true;
$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;
}
}
// ...unless it has either no in-flow line boxes or if its overflow property
// has a computed value other than visible, in which case the baseline is the
// bottom margin edge.
if (($style->overflow !== "visible" && $this->get_margin_height() < $baseline_height) || !$hasInflowChild) {
$baseline_height = $this->get_margin_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);
Expand Down
228 changes: 135 additions & 93 deletions src/FrameReflower/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = $line_height - $nominal_line_height;

$line_top_adjustment = 0.0;
$line_bottom_adjustment = 0.0;
$running_line_top_adjustment = 0.0;
$running_line_bottom_adjustment = 0.0;

foreach ($this->_frame->get_line_boxes() as $line) {
$height = $line->h;
$line->y += $running_line_top_adjustment + $running_line_bottom_adjustment;
$line_bottom = $line->y + $line->h;
$line_top_adjustment = 0.0;
$line_bottom_adjustment = 0.0;

$line_baseline_height = $baseline_height + $baseline_height_adjustment;
$top_frames = [];
$bottom_frames = [];

// Move all markers to the top of the line box
foreach ($line->get_list_markers() as $marker) {
Expand All @@ -570,115 +588,139 @@ 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;
}
}
if (!$frame->is_in_flow()) {
continue;
}
$frame_style = $frame->get_style();
$frame_baseline_height = $frame->get_outer_baseline_height();
$frame_baseline_height_adjustment = 0;
if ($frame instanceof TextFrameDecorator) {
$frame_line_height = $fontMetrics->getFontBaseline($frame_style->font_family, $frame_style->line_height);
$frame_nominal_line_height = $fontMetrics->getFontBaseline($frame_style->font_family, $frame_style->font_size * Style::$default_line_height);
$frame_baseline_height_adjustment = $frame_line_height - $frame_nominal_line_height;
}
if ($frame_baseline_height + $frame_baseline_height_adjustment > $line_baseline_height) {
$line_baseline_height = $frame_baseline_height + $frame_baseline_height_adjustment;
}
}

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;
} elseif ($parent instanceof TableCellFrameDecorator) {
$align = $frame_style->vertical_align;
switch ($parent->get_style()->vertical_align) {
case "top":
break;
case "middle":
break;
case "bottom":
break;
default:
$align = "baseline";
break;
}
} 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;
}
} elseif (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;
// 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 += $running_line_top_adjustment + $running_line_bottom_adjustment;

if (!Helpers::lengthEqual($y_offset, 0)) {
$frame->move(0, $y_offset);

if ($frame->get_position("y") < $line->y) {
$current_line_top_adjustment = $line->y - $frame->get_position("y");
if ($current_line_top_adjustment > $line_top_adjustment) {
$line_top_adjustment = $current_line_top_adjustment;
}
}
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;

$frame_bottom = $frame->get_position("y") + $frame_height;
if ($frame_bottom > $line_bottom) {
$current_line_bottom_adjustment = $frame_bottom - $line_bottom;
if ($current_line_bottom_adjustment > $line_bottom_adjustment) {
$line_bottom_adjustment = $current_line_bottom_adjustment;
}
} 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;
$running_line_top_adjustment += $line_top_adjustment;
$running_line_bottom_adjustment += $line_bottom_adjustment;
if ($style->height !== "auto") {
$style->height += $line_top_adjustment + $line_bottom_adjustment;
}
if (Helpers::lengthGreater($line_top_adjustment, 0)) {
foreach ($line->get_frames() as $frame) {
if (in_array($frame, $top_frames, true)) {
continue;
} elseif (in_array($frame, $bottom_frames, true)) {
$frame->move(0, $line->h - $frame->get_margin_height());
} else {
$frame->move(0, $line_top_adjustment);
}
}
}
}
Expand Down

0 comments on commit 56d1557

Please sign in to comment.