Initial commit

This commit is contained in:
root
2026-05-26 08:07:45 +00:00
commit a234e55e64
4263 changed files with 855927 additions and 0 deletions
@@ -0,0 +1,607 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Css\Content\Attr;
use Dompdf\Css\Content\CloseQuote;
use Dompdf\Css\Content\Counter;
use Dompdf\Css\Content\Counters;
use Dompdf\Css\Content\NoCloseQuote;
use Dompdf\Css\Content\NoOpenQuote;
use Dompdf\Css\Content\OpenQuote;
use Dompdf\Css\Content\StringPart;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\Frame\Factory;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameDecorator\Block;
/**
* Base reflower class
*
* Reflower objects are responsible for determining the width and height of
* individual frames. They also create line and page breaks as necessary.
*
* @package dompdf
*/
abstract class AbstractFrameReflower
{
/**
* Frame for this reflower
*
* @var AbstractFrameDecorator
*/
protected $_frame;
/**
* Cached min/max child size
*
* @var array
*/
protected $_min_max_child_cache;
/**
* Cached min/max size
*
* @var array
*/
protected $_min_max_cache;
/**
* AbstractFrameReflower constructor.
* @param AbstractFrameDecorator $frame
*/
function __construct(AbstractFrameDecorator $frame)
{
$this->_frame = $frame;
$this->_min_max_child_cache = null;
$this->_min_max_cache = null;
}
/**
* @return Dompdf
*/
function get_dompdf()
{
return $this->_frame->get_dompdf();
}
public function reset(): void
{
$this->_min_max_child_cache = null;
$this->_min_max_cache = null;
}
/**
* Determine the actual containing block for absolute and fixed position.
*
* https://www.w3.org/TR/CSS21/visudet.html#containing-block-details
*/
protected function determine_absolute_containing_block(): void
{
$frame = $this->_frame;
$style = $frame->get_style();
switch ($style->position) {
case "absolute":
$parent = $frame->find_positioned_parent();
if ($parent !== $frame->get_root()) {
$parent_style = $parent->get_style();
$parent_padding_box = $parent->get_padding_box();
//FIXME: an accurate measure of the positioned parent height
// is not possible until reflow has completed;
// we'll fall back to the parent's containing block,
// which is wrong for auto-height parents
if ($parent_style->height === "auto") {
$parent_containing_block = $parent->get_containing_block();
$containing_block_height = $parent_containing_block["h"] -
(float)$parent_style->length_in_pt([
$parent_style->margin_top,
$parent_style->margin_bottom,
$parent_style->border_top_width,
$parent_style->border_bottom_width
], $parent_containing_block["w"]);
} else {
$containing_block_height = $parent_padding_box["h"];
}
$frame->set_containing_block($parent_padding_box["x"], $parent_padding_box["y"], $parent_padding_box["w"], $containing_block_height);
break;
}
case "fixed":
$root = $frame->get_root();
$parent = $frame->get_parent();
do {
$parents_parent = $parent->get_parent();
if ($parents_parent == $root) {
break;
}
$parent = $parents_parent;
} while ($parent);
$initial_cb = $parent->get_containing_block();
$frame->set_containing_block($initial_cb["x"], $initial_cb["y"], $initial_cb["w"], $initial_cb["h"]);
break;
default:
// Nothing to do, containing block already set via parent
break;
}
}
/**
* Collapse frames margins
* http://www.w3.org/TR/CSS21/box.html#collapsing-margins
*/
protected function _collapse_margins(): void
{
$frame = $this->_frame;
// Margins of float/absolutely positioned/inline-level elements do not collapse
if (!$frame->is_in_flow() || $frame->is_inline_level()
|| $frame->get_root() === $frame || $frame->get_parent() === $frame->get_root()
) {
return;
}
$cb = $frame->get_containing_block();
$style = $frame->get_style();
$t = $style->length_in_pt($style->margin_top, $cb["w"]);
$b = $style->length_in_pt($style->margin_bottom, $cb["w"]);
// Handle 'auto' values
if ($t === "auto") {
$style->set_used("margin_top", 0.0);
$t = 0.0;
}
if ($b === "auto") {
$style->set_used("margin_bottom", 0.0);
$b = 0.0;
}
// Collapse vertical margins:
$n = $frame->get_next_sibling();
if ( $n && !($n->is_block_level() && $n->is_in_flow()) ) {
while ($n = $n->get_next_sibling()) {
if ($n->is_block_level() && $n->is_in_flow()) {
break;
}
if (!$n->get_first_child()) {
$n = null;
break;
}
}
}
if ($n) {
$n_style = $n->get_style();
$n_t = (float)$n_style->length_in_pt($n_style->margin_top, $cb["w"]);
$b = $this->get_collapsed_margin_length($b, $n_t);
$style->set_used("margin_bottom", $b);
$n_style->set_used("margin_top", 0.0);
}
// Collapse our first child's margin, if there is no border or padding
if ($style->border_top_width == 0 && $style->length_in_pt($style->padding_top) == 0) {
$f = $this->_frame->get_first_child();
if ( $f && !($f->is_block_level() && $f->is_in_flow()) ) {
while ($f = $f->get_next_sibling()) {
if ($f->is_block_level() && $f->is_in_flow()) {
break;
}
if (!$f->get_first_child()) {
$f = null;
break;
}
}
}
// Margins are collapsed only between block-level boxes
if ($f) {
$f_style = $f->get_style();
$f_t = (float)$f_style->length_in_pt($f_style->margin_top, $cb["w"]);
$t = $this->get_collapsed_margin_length($t, $f_t);
$style->set_used("margin_top", $t);
$f_style->set_used("margin_top", 0.0);
}
}
// Collapse our last child's margin, if there is no border or padding
if ($style->border_bottom_width == 0 && $style->length_in_pt($style->padding_bottom) == 0) {
$l = $this->_frame->get_last_child();
if ( $l && !($l->is_block_level() && $l->is_in_flow()) ) {
while ($l = $l->get_prev_sibling()) {
if ($l->is_block_level() && $l->is_in_flow()) {
break;
}
if (!$l->get_last_child()) {
$l = null;
break;
}
}
}
// Margins are collapsed only between block-level boxes
if ($l) {
$l_style = $l->get_style();
$l_b = (float)$l_style->length_in_pt($l_style->margin_bottom, $cb["w"]);
$b = $this->get_collapsed_margin_length($b, $l_b);
$style->set_used("margin_bottom", $b);
$l_style->set_used("margin_bottom", 0.0);
}
}
}
/**
* Get the combined (collapsed) length of two adjoining margins.
*
* See http://www.w3.org/TR/CSS21/box.html#collapsing-margins.
*
* @param float $l1
* @param float $l2
*
* @return float
*/
private function get_collapsed_margin_length(float $l1, float $l2): float
{
if ($l1 < 0 && $l2 < 0) {
return min($l1, $l2); // min(x, y) = - max(abs(x), abs(y)), if x < 0 && y < 0
}
if ($l1 < 0 || $l2 < 0) {
return $l1 + $l2; // x + y = x - abs(y), if y < 0
}
return max($l1, $l2);
}
/**
* Handle relative positioning according to
* https://www.w3.org/TR/CSS21/visuren.html#relative-positioning.
*
* @param AbstractFrameDecorator $frame The frame to handle.
*/
protected function position_relative(AbstractFrameDecorator $frame): void
{
$style = $frame->get_style();
if ($style->position === "relative") {
$cb = $frame->get_containing_block();
$top = $style->length_in_pt($style->top, $cb["h"]);
$right = $style->length_in_pt($style->right, $cb["w"]);
$bottom = $style->length_in_pt($style->bottom, $cb["h"]);
$left = $style->length_in_pt($style->left, $cb["w"]);
// FIXME RTL case:
// if ($left !== "auto" && $right !== "auto") $left = -$right;
if ($left === "auto" && $right === "auto") {
$left = 0;
} elseif ($left === "auto") {
$left = -$right;
}
if ($top === "auto" && $bottom === "auto") {
$top = 0;
} elseif ($top === "auto") {
$top = -$bottom;
}
$frame->move($left, $top);
}
}
/**
* @param Block|null $block
*/
abstract function reflow(?Block $block = null);
/**
* Resolve the `min-width` property.
*
* Resolves to 0 if not set or if a percentage and the containing-block
* width is not defined.
*
* @param float|null $cbw Width of the containing block.
*
* @return float
*/
protected function resolve_min_width(?float $cbw): float
{
$style = $this->_frame->get_style();
$min_width = $style->min_width;
return $min_width !== "auto"
? $style->length_in_pt($min_width, $cbw ?? 0)
: 0.0;
}
/**
* Resolve the `max-width` property.
*
* Resolves to `INF` if not set or if a percentage and the containing-block
* width is not defined.
*
* @param float|null $cbw Width of the containing block.
*
* @return float
*/
protected function resolve_max_width(?float $cbw): float
{
$style = $this->_frame->get_style();
$max_width = $style->max_width;
return $max_width !== "none"
? $style->length_in_pt($max_width, $cbw ?? INF)
: INF;
}
/**
* Resolve the `min-height` property.
*
* Resolves to 0 if not set or if a percentage and the containing-block
* height is not defined.
*
* @param float|null $cbh Height of the containing block.
*
* @return float
*/
protected function resolve_min_height(?float $cbh): float
{
$style = $this->_frame->get_style();
$min_height = $style->min_height;
return $min_height !== "auto"
? $style->length_in_pt($min_height, $cbh ?? 0)
: 0.0;
}
/**
* Resolve the `max-height` property.
*
* Resolves to `INF` if not set or if a percentage and the containing-block
* height is not defined.
*
* @param float|null $cbh Height of the containing block.
*
* @return float
*/
protected function resolve_max_height(?float $cbh): float
{
$style = $this->_frame->get_style();
$max_height = $style->max_height;
return $max_height !== "none"
? $style->length_in_pt($style->max_height, $cbh ?? INF)
: INF;
}
/**
* Get the minimum and maximum preferred width of the contents of the frame,
* as requested by its children.
*
* @return array A two-element array of min and max width.
*/
public function get_min_max_child_width(): array
{
if (!is_null($this->_min_max_child_cache)) {
return $this->_min_max_child_cache;
}
$low = [];
$high = [];
for ($iter = $this->_frame->get_children(); $iter->valid(); $iter->next()) {
$inline_min = 0;
$inline_max = 0;
// Add all adjacent inline widths together to calculate max width
while ($iter->valid() && ($iter->current()->is_inline_level() || $iter->current()->get_style()->display === "-dompdf-image")) {
/** @var AbstractFrameDecorator */
$child = $iter->current();
$child->get_reflower()->_set_content();
$minmax = $child->get_min_max_width();
if (in_array($child->get_style()->white_space, ["pre", "nowrap"], true)) {
$inline_min += $minmax["min"];
} else {
$low[] = $minmax["min"];
}
$inline_max += $minmax["max"];
$iter->next();
}
if ($inline_min > 0) {
$low[] = $inline_min;
}
if ($inline_max > 0) {
$high[] = $inline_max;
}
// Skip children with absolute position
if ($iter->valid()) {
/** @var AbstractFrameDecorator */
$child = $iter->current();
$child->get_reflower()->_set_content();
if (!$iter->current()->is_absolute()) {
list($low[], $high[]) = $child->get_min_max_width();
}
}
}
$min = count($low) ? max($low) : 0;
$max = count($high) ? max($high) : 0;
return $this->_min_max_child_cache = [$min, $max];
}
/**
* Get the minimum and maximum preferred content-box width of the frame.
*
* @return array A two-element array of min and max width.
*/
public function get_min_max_content_width(): array
{
return $this->get_min_max_child_width();
}
/**
* Get the minimum and maximum preferred border-box width of the frame.
*
* Required for shrink-to-fit width calculation, as used in automatic table
* layout, absolute positioning, float and inline-block. This provides a
* basic implementation. Child classes should override this or
* `get_min_max_content_width` as necessary.
*
* @return array An array `[0 => min, 1 => max, "min" => min, "max" => max]`
* of min and max width.
*/
public function get_min_max_width(): array
{
if (!is_null($this->_min_max_cache)) {
return $this->_min_max_cache;
}
$style = $this->_frame->get_style();
[$min, $max] = $this->get_min_max_content_width();
// Account for margins, borders, and padding
$dims = [
$style->padding_left,
$style->padding_right,
$style->border_left_width,
$style->border_right_width,
$style->margin_left,
$style->margin_right
];
// The containing block is not defined yet, treat percentages as 0
$delta = (float) $style->length_in_pt($dims, 0);
$min += $delta;
$max += $delta;
return $this->_min_max_cache = [$min, $max, "min" => $min, "max" => $max];
}
/**
* Resolves the `content` property to string.
*
* https://www.w3.org/TR/CSS21/generate.html#content
*
* @return string The resulting string
*/
protected function resolve_content(): ?string
{
$frame = $this->_frame;
$style = $frame->get_style();
$content = $style->content;
if ($content === "normal" || $content === "none") {
return null;
}
$quotes = $style->quotes;
$text = "";
foreach ($content as $val) {
if ($val instanceof StringPart) {
$text .= $val->string;
}
elseif ($val instanceof OpenQuote) {
// FIXME: Take quotation depth into account
if ($quotes !== "none" && isset($quotes[0][0])) {
$text .= $quotes[0][0];
}
}
elseif ($val instanceof CloseQuote) {
// FIXME: Take quotation depth into account
if ($quotes !== "none" && isset($quotes[0][1])) {
$text .= $quotes[0][1];
}
}
elseif ($val instanceof NoOpenQuote) {
// FIXME: Increment quotation depth
}
elseif ($val instanceof NoCloseQuote) {
// FIXME: Decrement quotation depth
}
elseif ($val instanceof Attr) {
$text .= $frame->get_parent()->get_node()->getAttribute($val->attribute);
}
elseif ($val instanceof Counter) {
$p = $frame->lookup_counter_frame($val->name, true);
$text .= $p->counter_value($val->name, $val->style);
}
elseif ($val instanceof Counters) {
$p = $frame->lookup_counter_frame($val->name, true);
$tmp = [];
while ($p) {
array_unshift($tmp, $p->counter_value($val->name, $val->style));
$p = $p->lookup_counter_frame($val->name);
}
$text .= implode($val->string, $tmp);
}
}
return $text;
}
/**
* Handle counters and set generated content if the frame is a
* generated-content frame.
*/
protected function _set_content(): void
{
$frame = $this->_frame;
if ($frame->content_set) {
return;
}
$style = $frame->get_style();
if (($reset = $style->counter_reset) !== "none") {
$frame->reset_counters($reset);
}
if (($increment = $style->counter_increment) !== "none") {
$frame->increment_counters($increment);
}
if ($frame->get_node()->nodeName === "dompdf_generated") {
$content = $this->resolve_content();
if ($content !== null) {
$node = $frame->get_node()->ownerDocument->createTextNode($content);
$new_style = $style->get_stylesheet()->create_style();
$new_style->inherit($style);
$new_frame = new Frame($node);
$new_frame->set_style($new_style);
Factory::decorate_frame($new_frame, $frame->get_dompdf(), $frame->get_root());
$frame->append_child($new_frame);
}
}
$frame->content_set = true;
}
}
@@ -0,0 +1,948 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\TableCell as TableCellFrameDecorator;
use Dompdf\FrameDecorator\Text as TextFrameDecorator;
use Dompdf\Exception;
use Dompdf\Css\Style;
use Dompdf\Helpers;
/**
* Reflows block frames
*
* @package dompdf
*/
class Block extends AbstractFrameReflower
{
// Minimum line width to justify, as fraction of available width
const MIN_JUSTIFY_WIDTH = 0.80;
/**
* Frame for this reflower
*
* @var BlockFrameDecorator
*/
protected $_frame;
function __construct(BlockFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* Calculate the ideal used value for the width property as per:
* http://www.w3.org/TR/CSS21/visudet.html#Computing_widths_and_margins
*
* @param float $width
*
* @return array
*/
protected function _calculate_width($width)
{
$frame = $this->_frame;
$style = $frame->get_style();
$absolute = $frame->is_absolute();
$cb = $frame->get_containing_block();
$w = $cb["w"];
$rm = $style->length_in_pt($style->margin_right, $w);
$lm = $style->length_in_pt($style->margin_left, $w);
$left = $style->length_in_pt($style->left, $w);
$right = $style->length_in_pt($style->right, $w);
// Handle 'auto' values
$dims = [$style->border_left_width,
$style->border_right_width,
$style->padding_left,
$style->padding_right,
$width !== "auto" ? $width : 0,
$rm !== "auto" ? $rm : 0,
$lm !== "auto" ? $lm : 0];
// absolutely positioned boxes take the 'left' and 'right' properties into account
if ($absolute) {
$dims[] = $left !== "auto" ? $left : 0;
$dims[] = $right !== "auto" ? $right : 0;
}
$sum = (float)$style->length_in_pt($dims, $w);
// Compare to the containing block
$diff = $w - $sum;
if ($absolute) {
// Absolutely positioned
// http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
if ($width === "auto" || $left === "auto" || $right === "auto") {
// "all of the three are 'auto'" logic + otherwise case
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
$block_parent = $frame->find_block_parent();
$parent_content = $block_parent->get_content_box();
$line = $block_parent->get_current_line_box();
// TODO: This is the in-flow inline position. Use the in-flow
// block position if the original display type is block-level
$inflow_x = $parent_content["x"] - $cb["x"] + $line->left + $line->w;
if ($width === "auto" && $left === "auto" && $right === "auto") {
// rule 3, per instruction preceding rule set
// shrink-to-fit width
$left = $inflow_x;
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff - $left), $max);
$right = $diff - $left - $width;
} elseif ($width === "auto" && $left === "auto") {
// rule 1
// shrink-to-fit width
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff), $max);
$left = $diff - $width;
} elseif ($width === "auto" && $right === "auto") {
// rule 3
// shrink-to-fit width
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff), $max);
$right = $diff - $width;
} elseif ($left === "auto" && $right === "auto") {
// rule 2
$left = $inflow_x;
$right = $diff - $left;
} elseif ($left === "auto") {
// rule 4
$left = $diff;
} elseif ($width === "auto") {
// rule 5
$width = max($diff, 0);
} else {
// $right === "auto"
// rule 6
$right = $diff;
}
} else {
// "none of the three are 'auto'" logic described in paragraph preceding the rules
if ($diff >= 0) {
if ($lm === "auto" && $rm === "auto") {
$lm = $rm = $diff / 2;
} elseif ($lm === "auto") {
$lm = $diff;
} elseif ($rm === "auto") {
$rm = $diff;
}
} else {
// over-constrained, solve for right
$right = $right + $diff;
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
}
}
} elseif ($style->float !== "none" || $style->display === "inline-block") {
// Shrink-to-fit width for float and inline block
// https://www.w3.org/TR/CSS21/visudet.html#float-width
// https://www.w3.org/TR/CSS21/visudet.html#inlineblock-width
if ($width === "auto") {
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff), $max);
}
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
} else {
// Block-level, normal flow
// https://www.w3.org/TR/CSS21/visudet.html#blockwidth
if ($diff >= 0) {
// Find auto properties and get them to take up the slack
if ($width === "auto") {
$width = $diff;
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
} elseif ($lm === "auto" && $rm === "auto") {
$lm = $rm = $diff / 2;
} elseif ($lm === "auto") {
$lm = $diff;
} elseif ($rm === "auto") {
$rm = $diff;
}
} else {
// We are over constrained--set margin-right to the difference
$rm = (float) $rm + $diff;
if ($width === "auto") {
$width = 0;
}
if ($lm === "auto") {
$lm = 0;
}
}
}
return [
"width" => $width,
"margin_left" => $lm,
"margin_right" => $rm,
"left" => $left,
"right" => $right,
];
}
/**
* Call the above function, but resolve max/min widths
*
* @throws Exception
* @return array
*/
protected function _calculate_restricted_width()
{
$frame = $this->_frame;
$style = $frame->get_style();
$cb = $frame->get_containing_block();
if (!isset($cb["w"])) {
throw new Exception("Box property calculation requires containing block width");
}
$width = $style->length_in_pt($style->width, $cb["w"]);
$values = $this->_calculate_width($width);
$margin_left = $values["margin_left"];
$margin_right = $values["margin_right"];
$width = $values["width"];
$left = $values["left"];
$right = $values["right"];
// Handle min/max width
// https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
$min_width = $this->resolve_min_width($cb["w"]);
$max_width = $this->resolve_max_width($cb["w"]);
if ($width > $max_width) {
$values = $this->_calculate_width($max_width);
$margin_left = $values["margin_left"];
$margin_right = $values["margin_right"];
$width = $values["width"];
$left = $values["left"];
$right = $values["right"];
}
if ($width < $min_width) {
$values = $this->_calculate_width($min_width);
$margin_left = $values["margin_left"];
$margin_right = $values["margin_right"];
$width = $values["width"];
$left = $values["left"];
$right = $values["right"];
}
return [$width, $margin_left, $margin_right, $left, $right];
}
/**
* Determine the unrestricted height of content within the block
* not by adding each line's height, but by getting the last line's position.
* This because lines could have been pushed lower by a clearing element.
*
* @return float
*/
protected function _calculate_content_height(): float
{
$height = 0.0;
$lines = $this->_frame->get_line_boxes();
if (count($lines) > 0) {
$last_line = end($lines);
$content_box = $this->_frame->get_content_box();
$height = $last_line->y + $last_line->h - $content_box["y"];
}
return $height;
}
/**
* Determine the frame's restricted height
*
* @return array
*/
protected function _calculate_restricted_height()
{
$frame = $this->_frame;
$style = $frame->get_style();
$content_height = $this->_calculate_content_height();
$cb = $frame->get_containing_block();
$height = $style->length_in_pt($style->height, $cb["h"]);
$margin_top = $style->length_in_pt($style->margin_top, $cb["w"]);
$margin_bottom = $style->length_in_pt($style->margin_bottom, $cb["w"]);
$top = $style->length_in_pt($style->top, $cb["h"]);
$bottom = $style->length_in_pt($style->bottom, $cb["h"]);
if ($frame->is_absolute()) {
// Absolutely positioned
// http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-height
$h_dims = [
$top !== "auto" ? $top : 0,
$height !== "auto" ? $height : 0,
$bottom !== "auto" ? $bottom : 0
];
$w_dims = [
$style->margin_top !== "auto" ? $style->margin_top : 0,
$style->padding_top,
$style->border_top_width,
$style->border_bottom_width,
$style->padding_bottom,
$style->margin_bottom !== "auto" ? $style->margin_bottom : 0
];
$sum = (float)$style->length_in_pt($h_dims, $cb["h"])
+ (float)$style->length_in_pt($w_dims, $cb["w"]);
$diff = $cb["h"] - $sum;
if ($height === "auto" || $top === "auto" || $bottom === "auto") {
// "all of the three are 'auto'" logic + otherwise case
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$block_parent = $frame->find_block_parent();
$current_line = $block_parent->get_current_line_box();
// TODO: This is the in-flow inline position. Use the in-flow
// block position if the original display type is block-level
$inflow_y = $current_line->y - $cb["y"];
if ($height === "auto" && $top === "auto" && $bottom === "auto") {
// rule 3, per instruction preceding rule set
$top = $inflow_y;
$height = $content_height;
$bottom = $diff - $top - $height;
} elseif ($height === "auto" && $top === "auto") {
// rule 1
$height = $content_height;
$top = $diff - $height;
} elseif ($height === "auto" && $bottom === "auto") {
// rule 3
$height = $content_height;
$bottom = $diff - $height;
} elseif ($top === "auto" && $bottom === "auto") {
// rule 2
$top = $inflow_y;
$bottom = $diff - $top;
} elseif ($top === "auto") {
// rule 4
$top = $diff;
} elseif ($height === "auto") {
// rule 5
$height = max($diff, 0);
} else {
// $bottom === "auto"
// rule 6
$bottom = $diff;
}
} else {
// "none of the three are 'auto'" logic described in paragraph preceding the rules
if ($diff >= 0) {
if ($margin_top === "auto" && $margin_bottom === "auto") {
$margin_top = $margin_bottom = $diff / 2;
} elseif ($margin_top === "auto") {
$margin_top = $diff;
} elseif ($margin_bottom === "auto") {
$margin_bottom = $diff;
}
} else {
// over-constrained, solve for bottom
$bottom = $bottom + $diff;
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
}
}
} else {
// https://www.w3.org/TR/CSS21/visudet.html#normal-block
// https://www.w3.org/TR/CSS21/visudet.html#block-root-margin
if ($height === "auto") {
$height = $content_height;
}
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
// Handle min/max height
// https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
$min_height = $this->resolve_min_height($cb["h"]);
$max_height = $this->resolve_max_height($cb["h"]);
$height = Helpers::clamp($height, $min_height, $max_height);
}
// TODO: Need to also take min/max height into account for absolute
// positioning, using similar logic to the `_calculate_width`/
// `calculate_restricted_width` split above. The non-absolute case
// can simply clamp height within min/max, as margins and offsets are
// not affected
return [$height, $margin_top, $margin_bottom, $top, $bottom];
}
/**
* Adjust the justification of each of our lines.
* http://www.w3.org/TR/CSS21/text.html#propdef-text-align
*/
protected function _text_align()
{
$style = $this->_frame->get_style();
$w = $this->_frame->get_containing_block("w");
$width = (float)$style->length_in_pt($style->width, $w);
$text_indent = (float)$style->length_in_pt($style->text_indent, $w);
switch ($style->text_align) {
default:
case "left":
foreach ($this->_frame->get_line_boxes() as $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
if ($line->left) {
foreach ($line->frames_to_align() as $frame) {
$frame->move($line->left, 0);
}
}
}
break;
case "right":
foreach ($this->_frame->get_line_boxes() as $i => $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
$indent = $i === 0 ? $text_indent : 0;
$dx = $width - $line->w - $line->right - $indent;
foreach ($line->frames_to_align() as $frame) {
$frame->move($dx, 0);
}
}
break;
case "justify":
// We justify all lines except the last one, unless the frame
// has been split, in which case the actual last line is part of
// the split-off frame
$lines = $this->_frame->get_line_boxes();
$last_line_index = $this->_frame->is_split ? null : count($lines) - 1;
foreach ($lines as $i => $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
if ($line->left) {
foreach ($line->frames_to_align() as $frame) {
$frame->move($line->left, 0);
}
}
if ($line->br || $i === $last_line_index) {
continue;
}
$frames = $line->get_frames();
$other_frame_count = 0;
foreach ($frames as $frame) {
if (!($frame instanceof TextFrameDecorator)) {
$other_frame_count++;
}
}
$word_count = $line->wc + $other_frame_count;
// Set the spacing for each child
if ($word_count > 1) {
$indent = $i === 0 ? $text_indent : 0;
$spacing = ($width - $line->get_width() - $indent) / ($word_count - 1);
} else {
$spacing = 0;
}
$dx = 0;
foreach ($frames as $frame) {
if ($frame instanceof TextFrameDecorator) {
$text = $frame->get_text();
$spaces = mb_substr_count($text, " ");
$frame->move($dx, 0);
$frame->set_text_spacing($spacing);
$dx += $spaces * $spacing;
} else {
$frame->move($dx, 0);
}
}
// The line (should) now occupy the entire width
$line->w = $width;
}
break;
case "center":
case "centre":
foreach ($this->_frame->get_line_boxes() as $i => $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
$indent = $i === 0 ? $text_indent : 0;
$dx = ($width + $line->left - $line->w - $line->right - $indent) / 2;
foreach ($line->frames_to_align() as $frame) {
$frame->move($dx, 0);
}
}
break;
}
}
/**
* Align inline children vertically.
* Aligns each child vertically after each line is reflowed
*/
function vertical_align()
{
$fontMetrics = $this->get_dompdf()->getFontMetrics();
foreach ($this->_frame->get_line_boxes() as $line) {
$height = $line->h;
// Move all markers to the top of the line box
foreach ($line->get_list_markers() as $marker) {
$x = $marker->get_position("x");
$marker->set_position($x, $line->y);
}
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 ($skip) {
continue;
}
$marginHeight = $frame->get_margin_height();
$imageHeightDiff = $height * 0.8 - $marginHeight;
$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":
$y_offset = 0.3 * $height + $imageHeightDiff;
break;
case "super":
$y_offset = -0.2 * $height + $imageHeightDiff;
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":
break;
case "text-bottom": // FIXME: align bottom of image with the descender?
case "bottom":
$y_offset = 0.3 * $height + $imageHeightDiff;
break;
case "baseline":
default:
$y_offset = $imageHeightDiff;
break;
}
} else {
$y_offset = $baseline - (float)$style->length_in_pt($align, $style->font_size) - $marginHeight;
}
} 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;
}
} 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);
}
}
}
}
/**
* @param AbstractFrameDecorator $child
*/
function process_clear(AbstractFrameDecorator $child)
{
$child_style = $child->get_style();
$root = $this->_frame->get_root();
// Handle "clear"
if ($child_style->clear !== "none") {
//TODO: this is a WIP for handling clear/float frames that are in between inline frames
if ($child->get_prev_sibling() !== null) {
$this->_frame->add_line();
}
if ($child_style->float !== "none" && $child->get_next_sibling()) {
$this->_frame->set_current_line_number($this->_frame->get_current_line_number() - 1);
}
$lowest_y = $root->get_lowest_float_offset($child);
// If a float is still applying, we handle it
if ($lowest_y) {
if ($child->is_in_flow()) {
$line_box = $this->_frame->get_current_line_box();
$line_box->y = $lowest_y + $child->get_margin_height();
$line_box->left = 0;
$line_box->right = 0;
}
$child->move(0, $lowest_y - $child->get_position("y"));
}
}
}
/**
* @param AbstractFrameDecorator $child
* @param float $cb_x
* @param float $cb_w
*/
function process_float(AbstractFrameDecorator $child, $cb_x, $cb_w)
{
$child_style = $child->get_style();
$root = $this->_frame->get_root();
// Handle "float"
if ($child_style->float !== "none") {
$root->add_floating_frame($child);
// Remove next frame's beginning whitespace
$next = $child->get_next_sibling();
if ($next && $next instanceof TextFrameDecorator) {
$next->set_text(ltrim($next->get_text()));
}
$line_box = $this->_frame->get_current_line_box();
list($old_x, $old_y) = $child->get_position();
$float_x = $cb_x;
$float_y = $old_y;
$float_w = $child->get_margin_width();
if ($child_style->clear === "none") {
switch ($child_style->float) {
case "left":
$float_x += $line_box->left;
break;
case "right":
$float_x += ($cb_w - $line_box->right - $float_w);
break;
}
} else {
if ($child_style->float === "right") {
$float_x += ($cb_w - $float_w);
}
}
if ($cb_w < $float_x + $float_w - $old_x) {
// TODO handle when floating elements don't fit
}
$line_box->get_float_offsets();
if ($child->_float_next_line) {
$float_y += $line_box->h;
}
$child->set_position($float_x, $float_y);
$child->move($float_x - $old_x, $float_y - $old_y, true);
}
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
// Check if a page break is forced
$page = $this->_frame->get_root();
$page->check_forced_page_break($this->_frame);
// Bail if the page is full
if ($page->is_full()) {
return;
}
$this->determine_absolute_containing_block();
// Counters and generated content
$this->_set_content();
// Inherit any dangling list markers
if ($block && $this->_frame->is_in_flow()) {
$this->_frame->inherit_dangling_markers($block);
}
// Collapse margins if required
$this->_collapse_margins();
$style = $this->_frame->get_style();
$cb = $this->_frame->get_containing_block();
// Determine the constraints imposed by this frame: calculate the width
// of the content area:
[$width, $margin_left, $margin_right, $left, $right] = $this->_calculate_restricted_width();
// Store the calculated properties
$style->set_used("width", $width);
$style->set_used("margin_left", $margin_left);
$style->set_used("margin_right", $margin_right);
$style->set_used("left", $left);
$style->set_used("right", $right);
$margin_top = $style->length_in_pt($style->margin_top, $cb["w"]);
$margin_bottom = $style->length_in_pt($style->margin_bottom, $cb["w"]);
$auto_top = $style->top === "auto";
$auto_margin_top = $margin_top === "auto";
// Update the position
$this->_frame->position();
[$x, $y] = $this->_frame->get_position();
// Adjust the first line based on the text-indent property
$indent = (float)$style->length_in_pt($style->text_indent, $cb["w"]);
$this->_frame->increase_line_width($indent);
// Determine the content edge
$top = (float)$style->length_in_pt([
$margin_top !== "auto" ? $margin_top : 0,
$style->border_top_width,
$style->padding_top
], $cb["w"]);
$bottom = (float)$style->length_in_pt([
$margin_bottom !== "auto" ? $margin_bottom : 0,
$style->border_bottom_width,
$style->padding_bottom
], $cb["w"]);
$cb_x = $x + (float)$margin_left + (float)$style->length_in_pt([$style->border_left_width,
$style->padding_left], $cb["w"]);
$cb_y = $y + $top;
$height = $style->length_in_pt($style->height, $cb["h"]);
if ($height === "auto") {
$height = ($cb["h"] + $cb["y"]) - $bottom - $cb_y;
}
// Set the y position of the first line in this block
$line_box = $this->_frame->get_current_line_box();
$line_box->y = $cb_y;
$line_box->get_float_offsets();
// Set the containing blocks and reflow each child
foreach ($this->_frame->get_children() as $child) {
$child->set_containing_block($cb_x, $cb_y, $width, $height);
$this->process_clear($child);
$child->reflow($this->_frame);
// Check for a page break before the child
$page->check_page_break($child);
// Don't add the child to the line if a page break has occurred
// before it (possibly via a descendant), in which case it has been
// reset, including its position
if ($page->is_full() && $child->get_position("x") === null) {
break;
}
$this->process_float($child, $cb_x, $width);
}
// Stop reflow if a page break has occurred before the frame, in which
// case it has been reset, including its position
if ($page->is_full() && $this->_frame->get_position("x") === null) {
return;
}
// Determine our height
[$height, $margin_top, $margin_bottom, $top, $bottom] = $this->_calculate_restricted_height();
$style->set_used("height", $height);
$style->set_used("margin_top", $margin_top);
$style->set_used("margin_bottom", $margin_bottom);
$style->set_used("top", $top);
$style->set_used("bottom", $bottom);
if ($this->_frame->is_absolute()) {
if ($auto_top) {
$this->_frame->move(0, $top);
}
if ($auto_margin_top) {
$this->_frame->move(0, $margin_top, true);
}
}
$this->_text_align();
$this->vertical_align();
// Handle relative positioning
foreach ($this->_frame->get_children() as $child) {
$this->position_relative($child);
}
if ($block && $this->_frame->is_in_flow()) {
$block->add_frame_to_line($this->_frame);
if ($this->_frame->is_block_level()) {
$block->add_line();
}
}
}
public function get_min_max_content_width(): array
{
// TODO: While the containing block is not set yet on the frame, it can
// already be determined in some cases due to fixed dimensions on the
// ancestor forming the containing block. In such cases, percentage
// values could be resolved here
$style = $this->_frame->get_style();
$width = $style->width;
$fixed_width = $width !== "auto" && !Helpers::is_percent($width);
// If the frame has a specified width, then we don't need to check
// its children
if ($fixed_width) {
$min = (float) $style->length_in_pt($width, 0);
$max = $min;
} else {
[$min, $max] = $this->get_min_max_child_width();
}
// Handle min/max width style properties
$min_width = $this->resolve_min_width(null);
$max_width = $this->resolve_max_width(null);
$min = Helpers::clamp($min, $min_width, $max_width);
$max = Helpers::clamp($max, $min_width, $max_width);
return [$min, $max];
}
}
@@ -0,0 +1,213 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Helpers;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Image as ImageFrameDecorator;
/**
* Image reflower class
*
* @package dompdf
*/
class Image extends AbstractFrameReflower
{
/**
* Image constructor.
* @param ImageFrameDecorator $frame
*/
function __construct(ImageFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
$this->determine_absolute_containing_block();
// Counters and generated content
$this->_set_content();
//FLOAT
//$frame = $this->_frame;
//$page = $frame->get_root();
//if ($frame->get_style()->float !== "none" ) {
// $page->add_floating_frame($this);
//}
$this->resolve_dimensions();
$this->resolve_margins();
$frame = $this->_frame;
$frame->position();
if ($block && $frame->is_in_flow()) {
$block->add_frame_to_line($frame);
}
}
public function get_min_max_content_width(): array
{
// TODO: While the containing block is not set yet on the frame, it can
// already be determined in some cases due to fixed dimensions on the
// ancestor forming the containing block. In such cases, percentage
// values could be resolved here
$style = $this->_frame->get_style();
[$width] = $this->calculate_size(null, null);
$min_width = $this->resolve_min_width(null);
$percent_width = Helpers::is_percent($style->width)
|| Helpers::is_percent($style->max_width)
|| ($style->width === "auto"
&& (Helpers::is_percent($style->height) || Helpers::is_percent($style->max_height)));
// Use the specified min width as minimum when width or max width depend
// on the containing block and cannot be resolved yet. This mimics
// browser behavior
$min = $percent_width ? $min_width : $width;
$max = $width;
return [$min, $max];
}
/**
* Calculate width and height, accounting for min/max constraints.
*
* * https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
* * https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
* * https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
* * https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
*
* @param float|null $cbw Width of the containing block.
* @param float|null $cbh Height of the containing block.
*
* @return float[]
*/
protected function calculate_size(?float $cbw, ?float $cbh): array
{
/** @var ImageFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
$computed_width = $style->width;
$computed_height = $style->height;
$width = $cbw === null && Helpers::is_percent($computed_width)
? "auto"
: $style->length_in_pt($computed_width, $cbw ?? 0);
$height = $cbh === null && Helpers::is_percent($computed_height)
? "auto"
: $style->length_in_pt($computed_height, $cbh ?? 0);
$min_width = $this->resolve_min_width($cbw);
$max_width = $this->resolve_max_width($cbw);
$min_height = $this->resolve_min_height($cbh);
$max_height = $this->resolve_max_height($cbh);
if ($width === "auto" && $height === "auto") {
// Use intrinsic dimensions, resampled to pt
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
$w = $frame->resample($img_width);
$h = $frame->resample($img_height);
// Resolve min/max constraints according to the constraint-violation
// table in https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
$max_width = max($min_width, $max_width);
$max_height = max($min_height, $max_height);
if (($w > $max_width && $h <= $max_height)
|| ($w > $max_width && $h > $max_height && $max_width / $w <= $max_height / $h)
|| ($w < $min_width && $h > $min_height)
|| ($w < $min_width && $h < $min_height && $min_width / $w > $min_height / $h)
) {
$width = Helpers::clamp($w, $min_width, $max_width);
$height = $width * ($img_height / $img_width);
$height = Helpers::clamp($height, $min_height, $max_height);
} else {
$height = Helpers::clamp($h, $min_height, $max_height);
$width = $height * ($img_width / $img_height);
$width = Helpers::clamp($width, $min_width, $max_width);
}
} elseif ($height === "auto") {
// Width is fixed, scale height according to aspect ratio
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
$width = Helpers::clamp((float) $width, $min_width, $max_width);
$height = $width * ($img_height / $img_width);
$height = Helpers::clamp($height, $min_height, $max_height);
} elseif ($width === "auto") {
// Height is fixed, scale width according to aspect ratio
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
$height = Helpers::clamp((float) $height, $min_height, $max_height);
$width = $height * ($img_width / $img_height);
$width = Helpers::clamp($width, $min_width, $max_width);
} else {
// Width and height are fixed
$width = Helpers::clamp((float) $width, $min_width, $max_width);
$height = Helpers::clamp((float) $height, $min_height, $max_height);
}
return [$width, $height];
}
protected function resolve_dimensions(): void
{
/** @var ImageFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
$debug_png = $this->get_dompdf()->getOptions()->getDebugPng();
if ($debug_png) {
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
print "resolve_dimensions() " .
$frame->get_style()->width . " " .
$frame->get_style()->height . ";" .
$frame->get_parent()->get_style()->width . " " .
$frame->get_parent()->get_style()->height . ";" .
$frame->get_parent()->get_parent()->get_style()->width . " " .
$frame->get_parent()->get_parent()->get_style()->height . ";" .
$img_width . " " .
$img_height . "|";
}
[, , $cbw, $cbh] = $frame->get_containing_block();
[$width, $height] = $this->calculate_size($cbw, $cbh);
if ($debug_png) {
print $width . " " . $height . ";";
}
$style->set_used("width", $width);
$style->set_used("height", $height);
}
protected function resolve_margins(): void
{
// Only handle the inline case for now
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
$style = $this->_frame->get_style();
if ($style->margin_left === "auto") {
$style->set_used("margin_left", 0.0);
}
if ($style->margin_right === "auto") {
$style->set_used("margin_right", 0.0);
}
if ($style->margin_top === "auto") {
$style->set_used("margin_top", 0.0);
}
if ($style->margin_bottom === "auto") {
$style->set_used("margin_bottom", 0.0);
}
}
}
@@ -0,0 +1,191 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Inline as InlineFrameDecorator;
use Dompdf\FrameDecorator\Text as TextFrameDecorator;
/**
* Reflows inline frames
*
* @package dompdf
*/
class Inline extends AbstractFrameReflower
{
/**
* Inline constructor.
* @param InlineFrameDecorator $frame
*/
function __construct(InlineFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* Handle reflow of empty inline frames.
*
* Regular inline frames are positioned together with their text (or inline)
* children after child reflow. Empty inline frames have no children that
* could determine the positioning, so they need to be handled separately.
*
* @param BlockFrameDecorator $block
*/
protected function reflow_empty(BlockFrameDecorator $block): void
{
/** @var InlineFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
// Resolve width, so the margin width can be checked
$style->set_used("width", 0.0);
$cb = $frame->get_containing_block();
$line = $block->get_current_line_box();
$width = $frame->get_margin_width();
if ($width > ($cb["w"] - $line->left - $line->w - $line->right)) {
$block->add_line();
// Find the appropriate inline ancestor to split
$child = $frame;
$p = $child->get_parent();
while ($p instanceof InlineFrameDecorator && !$child->get_prev_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if ($p instanceof InlineFrameDecorator) {
// Split parent and stop current reflow. Reflow continues
// via child-reflow loop of split parent
$p->split($child);
return;
}
}
$frame->position();
$block->add_frame_to_line($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
/** @var InlineFrameDecorator */
$frame = $this->_frame;
// Check if a page break is forced
$page = $frame->get_root();
$page->check_forced_page_break($frame);
if ($page->is_full()) {
return;
}
// Counters and generated content
$this->_set_content();
$style = $frame->get_style();
// Resolve auto margins
// https://www.w3.org/TR/CSS21/visudet.html#inline-width
// https://www.w3.org/TR/CSS21/visudet.html#inline-non-replaced
if ($style->margin_left === "auto") {
$style->set_used("margin_left", 0.0);
}
if ($style->margin_right === "auto") {
$style->set_used("margin_right", 0.0);
}
if ($style->margin_top === "auto") {
$style->set_used("margin_top", 0.0);
}
if ($style->margin_bottom === "auto") {
$style->set_used("margin_bottom", 0.0);
}
// Handle line breaks
if ($frame->get_node()->nodeName === "br") {
if ($block) {
$line = $block->get_current_line_box();
$frame->set_containing_line($line);
$block->maximize_line_height($frame->get_margin_height(), $frame);
$block->add_line(true);
$next = $frame->get_next_sibling();
$p = $frame->get_parent();
if ($next && $p instanceof InlineFrameDecorator) {
$p->split($next);
}
}
return;
}
// Handle empty inline frames
if (!$frame->get_first_child()) {
if ($block) {
$this->reflow_empty($block);
}
return;
}
// Add margin, padding & border width to the first and last children,
// so they are accounted for during text layout
if (($f = $frame->get_first_child()) && $f instanceof TextFrameDecorator) {
$f_style = $f->get_style();
$f_style->margin_left = $style->margin_left;
$f_style->padding_left = $style->padding_left;
$f_style->border_left_width = $style->border_left_width;
}
if (($l = $frame->get_last_child()) && $l instanceof TextFrameDecorator) {
$l_style = $l->get_style();
$l_style->margin_right = $style->margin_right;
$l_style->padding_right = $style->padding_right;
$l_style->border_right_width = $style->border_right_width;
}
$frame->position();
$cb = $frame->get_containing_block();
// Set the containing blocks and reflow each child. The containing
// block is not changed by line boxes.
foreach ($frame->get_children() as $child) {
$child->set_containing_block($cb);
$child->reflow($block);
// Stop reflow if the frame has been reset by a line or page break
// due to child reflow
if (!$frame->content_set) {
return;
}
}
// Assume the position of the first in-flow child, otherwise use the
// fallback position that was set before child reflow
$child = $frame->get_first_child();
while ($child && !$child->is_in_flow()) {
$child = $child->get_next_sibling();
}
if ($child) {
[$x, $y] = $child->get_position();
$frame->set_position($x, $y);
}
// Handle relative positioning
foreach ($frame->get_children() as $child) {
$this->position_relative($child);
}
if ($block) {
$block->add_frame_to_line($frame);
}
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\ListBullet as ListBulletFrameDecorator;
/**
* Reflows list bullets
*
* @package dompdf
*/
class ListBullet extends AbstractFrameReflower
{
/**
* ListBullet constructor.
* @param ListBulletFrameDecorator $frame
*/
function __construct(ListBulletFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
if ($block === null) {
return;
}
/** @var ListBulletFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
$style->set_used("width", $frame->get_width());
$frame->position();
if ($style->list_style_position === "inside") {
$block->add_frame_to_line($frame);
} else {
$block->add_dangling_marker($frame);
}
}
}
@@ -0,0 +1,37 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
/**
* Dummy reflower
*
* @package dompdf
*/
class NullFrameReflower extends AbstractFrameReflower
{
/**
* NullFrameReflower constructor.
* @param Frame $frame
*/
function __construct(Frame $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
return;
}
}
@@ -0,0 +1,198 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Page as PageFrameDecorator;
/**
* Reflows pages
*
* @package dompdf
*/
class Page extends AbstractFrameReflower
{
/**
* Cache of the callbacks array
*
* @var array
*/
private $_callbacks;
/**
* Cache of the canvas
*
* @var \Dompdf\Canvas
*/
private $_canvas;
/**
* Page constructor.
* @param PageFrameDecorator $frame
*/
function __construct(PageFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param PageFrameDecorator $frame
* @param int $page_number
*/
function apply_page_style(Frame $frame, $page_number)
{
$style = $frame->get_style();
$page_styles = $style->get_stylesheet()->get_page_styles();
// http://www.w3.org/TR/CSS21/page.html#page-selectors
if (count($page_styles) > 1) {
$odd = $page_number % 2 == 1;
$first = $page_number == 1;
$style = clone $page_styles["base"];
// FIXME RTL
if ($odd && isset($page_styles[":right"])) {
$style->merge($page_styles[":right"]);
}
if ($odd && isset($page_styles[":odd"])) {
$style->merge($page_styles[":odd"]);
}
// FIXME RTL
if (!$odd && isset($page_styles[":left"])) {
$style->merge($page_styles[":left"]);
}
if (!$odd && isset($page_styles[":even"])) {
$style->merge($page_styles[":even"]);
}
if ($first && isset($page_styles[":first"])) {
$style->merge($page_styles[":first"]);
}
$frame->set_style($style);
}
$frame->calculate_bottom_page_edge();
}
/**
* Paged layout:
* http://www.w3.org/TR/CSS21/page.html
*
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
/** @var PageFrameDecorator $frame */
$frame = $this->_frame;
$child = $frame->get_first_child();
$fixed_children = [];
$prev_child = null;
$current_page = 0;
// Only if it's the first page, we save the nodes with a fixed position
if ($child) {
foreach ($child->get_children() as $onechild) {
if ($onechild->get_style()->position === "fixed") {
$fixed_children[] = $onechild->deep_copy();
$child->remove_child($onechild);
}
}
$fixed_children = array_reverse($fixed_children);
}
while ($child) {
$this->apply_page_style($frame, $current_page + 1);
$style = $frame->get_style();
// Pages are only concerned with margins
$cb = $frame->get_containing_block();
$left = (float)$style->length_in_pt($style->margin_left, $cb["w"]);
$right = (float)$style->length_in_pt($style->margin_right, $cb["w"]);
$top = (float)$style->length_in_pt($style->margin_top, $cb["h"]);
$bottom = (float)$style->length_in_pt($style->margin_bottom, $cb["h"]);
$content_x = $cb["x"] + $left;
$content_y = $cb["y"] + $top;
$content_width = $cb["w"] - $left - $right;
$content_height = $cb["h"] - $top - $bottom;
$child->set_containing_block($content_x, $content_y, $content_width, $content_height);
//Insert a copy of each node which have a fixed position
foreach ($fixed_children as $fixed_child) {
$child->prepend_child($fixed_child->deep_copy());
}
// Check for begin reflow callback
$this->_check_callbacks("begin_page_reflow", $child);
$child->reflow();
$next_child = $child->get_next_sibling();
// Check for begin render callback
$this->_check_callbacks("begin_page_render", $child);
// Render the page
$frame->get_renderer()->render($child);
// Check for end render callback
$this->_check_callbacks("end_page_render", $child);
if ($next_child) {
$frame->next_page();
}
// Wait to dispose of all frames on the previous page
// so callback will have access to them
if ($prev_child) {
$prev_child->dispose(true);
}
$prev_child = $child;
$child = $next_child;
$current_page++;
}
// Dispose of previous page if it still exists
if ($prev_child) {
$prev_child->dispose(true);
}
}
/**
* Check for callbacks that need to be performed when a given event
* gets triggered on a page
*
* @param string $event The type of event
* @param Frame $frame The frame that event is triggered on
*/
protected function _check_callbacks(string $event, Frame $frame): void
{
if (!isset($this->_callbacks)) {
$dompdf = $this->get_dompdf();
$this->_callbacks = $dompdf->getCallbacks();
$this->_canvas = $dompdf->getCanvas();
}
if (isset($this->_callbacks[$event])) {
$fs = $this->_callbacks[$event];
$canvas = $this->_canvas;
$fontMetrics = $this->get_dompdf()->getFontMetrics();
foreach ($fs as $f) {
$f($frame, $canvas, $fontMetrics);
}
}
}
}
@@ -0,0 +1,523 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
use Dompdf\Helpers;
/**
* Reflows tables
*
* @package dompdf
*/
class Table extends AbstractFrameReflower
{
/**
* Frame for this reflower
*
* @var TableFrameDecorator
*/
protected $_frame;
/**
* Cache of results between call to get_min_max_width and assign_widths
*
* @var array
*/
protected $_state;
/**
* Table constructor.
* @param TableFrameDecorator $frame
*/
function __construct(TableFrameDecorator $frame)
{
$this->_state = null;
parent::__construct($frame);
}
/**
* State is held here so it needs to be reset along with the decorator
*/
public function reset(): void
{
parent::reset();
$this->_state = null;
}
protected function _assign_widths()
{
$style = $this->_frame->get_style();
// Find the min/max width of the table and sort the columns into
// absolute/percent/auto arrays
$delta = $this->_state["width_delta"];
$min_width = $this->_state["min_width"];
$max_width = $this->_state["max_width"];
$percent_used = $this->_state["percent_used"];
$absolute_used = $this->_state["absolute_used"];
$auto_min = $this->_state["auto_min"];
$absolute =& $this->_state["absolute"];
$percent =& $this->_state["percent"];
$auto =& $this->_state["auto"];
// Determine the actual width of the table (excluding borders and
// padding)
$cb = $this->_frame->get_containing_block();
$columns =& $this->_frame->get_cellmap()->get_columns();
$width = $style->width;
$min_table_width = $this->resolve_min_width($cb["w"]) - $delta;
if ($width !== "auto") {
$preferred_width = (float) $style->length_in_pt($width, $cb["w"]) - $delta;
if ($preferred_width < $min_table_width) {
$preferred_width = $min_table_width;
}
if ($preferred_width > $min_width) {
$width = $preferred_width;
} else {
$width = $min_width;
}
} else {
if ($max_width + $delta < $cb["w"]) {
$width = $max_width;
} elseif ($cb["w"] - $delta > $min_width) {
$width = $cb["w"] - $delta;
} else {
$width = $min_width;
}
if ($width < $min_table_width) {
$width = $min_table_width;
}
}
// Store our resolved width
$style->set_used("width", $width);
$cellmap = $this->_frame->get_cellmap();
if ($cellmap->is_columns_locked()) {
return;
}
// If the whole table fits on the page, then assign each column it's max width
if ($width == $max_width) {
foreach ($columns as $i => $col) {
$cellmap->set_column_width($i, $col["max-width"]);
}
return;
}
// Determine leftover and assign it evenly to all columns
if ($width > $min_width) {
// We have three cases to deal with:
//
// 1. All columns are auto or absolute width. In this case we
// distribute extra space across all auto columns weighted by the
// difference between their max and min width, or by max width only
// if the width of the table is larger than the max width for all
// columns.
//
// 2. Only absolute widths have been specified, no auto columns. In
// this case we distribute extra space across all columns weighted
// by their absolute width.
//
// 3. Percentage widths have been specified. In this case we normalize
// the percentage values and try to assign widths as fractions of
// the table width. Absolute column widths are fully satisfied and
// any remaining space is evenly distributed among all auto columns.
// Case 1:
if ($percent_used == 0 && count($auto)) {
foreach ($absolute as $i) {
$w = $columns[$i]["min-width"];
$cellmap->set_column_width($i, $w);
}
if ($width < $max_width) {
$increment = $width - $min_width;
$table_delta = $max_width - $min_width;
foreach ($auto as $i) {
$min = $columns[$i]["min-width"];
$max = $columns[$i]["max-width"];
$col_delta = $max - $min;
$w = $min + $increment * ($col_delta / $table_delta);
$cellmap->set_column_width($i, $w);
}
} else {
$increment = $width - $max_width;
$auto_max = $max_width - $absolute_used;
foreach ($auto as $i) {
$max = $columns[$i]["max-width"];
$f = $auto_max > 0 ? $max / $auto_max : 1 / count($auto);
$w = $max + $increment * $f;
$cellmap->set_column_width($i, $w);
}
}
return;
}
// Case 2:
if ($percent_used == 0 && !count($auto)) {
$increment = $width - $absolute_used;
foreach ($absolute as $i) {
$abs = $columns[$i]["min-width"];
$f = $absolute_used > 0 ? $abs / $absolute_used : 1 / count($absolute);
$w = $abs + $increment * $f;
$cellmap->set_column_width($i, $w);
}
return;
}
// Case 3:
if ($percent_used > 0) {
// Scale percent values if the total percentage is > 100 or
// there are no auto values to take up slack
if ($percent_used > 100 || count($auto) == 0) {
$scale = 100 / $percent_used;
} else {
$scale = 1;
}
// Account for the minimum space used by the unassigned auto
// columns, by the columns with absolute widths, and the
// percentage columns following the current one
$used_width = $auto_min + $absolute_used;
foreach ($absolute as $i) {
$w = $columns[$i]["min-width"];
$cellmap->set_column_width($i, $w);
}
$percent_min = 0;
foreach ($percent as $i) {
$percent_min += $columns[$i]["min-width"];
}
// First-come, first served
foreach ($percent as $i) {
$min = $columns[$i]["min-width"];
$percent_min -= $min;
$slack = $width - $used_width - $percent_min;
$columns[$i]["percent"] *= $scale;
$w = min($columns[$i]["percent"] * $width / 100, $slack);
if ($w < $min) {
$w = $min;
}
$cellmap->set_column_width($i, $w);
$used_width += $w;
}
// This works because $used_width includes the min-width of each
// unassigned column
if (count($auto) > 0) {
$increment = ($width - $used_width) / count($auto);
foreach ($auto as $i) {
$w = $columns[$i]["min-width"] + $increment;
$cellmap->set_column_width($i, $w);
}
}
return;
}
} else {
// We are over-constrained:
// Each column gets its minimum width
foreach ($columns as $i => $col) {
$cellmap->set_column_width($i, $col["min-width"]);
}
}
}
/**
* Determine the frame's height based on min/max height
*
* @return float
*/
protected function _calculate_height()
{
$frame = $this->_frame;
$style = $frame->get_style();
$cb = $frame->get_containing_block();
$height = $style->length_in_pt($style->height, $cb["h"]);
$cellmap = $frame->get_cellmap();
$cellmap->assign_frame_heights();
$rows = $cellmap->get_rows();
// Determine our content height
$content_height = 0.0;
foreach ($rows as $r) {
$content_height += $r["height"];
}
if ($height === "auto") {
$height = $content_height;
}
// Handle min/max height
// https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
$min_height = $this->resolve_min_height($cb["h"]);
$max_height = $this->resolve_max_height($cb["h"]);
$height = Helpers::clamp($height, $min_height, $max_height);
// Use the content height or the height value, whichever is greater
if ($height <= $content_height) {
$height = $content_height;
} else {
// FIXME: Borders and row positions are not properly updated by this
// $cellmap->set_frame_heights($height, $content_height);
}
return $height;
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
/** @var TableFrameDecorator */
$frame = $this->_frame;
// Check if a page break is forced
$page = $frame->get_root();
$page->check_forced_page_break($frame);
// Bail if the page is full
if ($page->is_full()) {
return;
}
// Let the page know that we're reflowing a table so that splits
// are suppressed (simply setting page-break-inside: avoid won't
// work because we may have an arbitrary number of block elements
// inside tds.)
$page->table_reflow_start();
$this->determine_absolute_containing_block();
// Counters and generated content
$this->_set_content();
// Collapse vertical margins, if required
$this->_collapse_margins();
// Table layout algorithm:
// http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
if (is_null($this->_state)) {
$this->get_min_max_width();
}
$cb = $frame->get_containing_block();
$style = $frame->get_style();
// This is slightly inexact, but should be okay. Add half the
// border-spacing to the table as padding. The other half is added to
// the cells themselves.
if ($style->border_collapse === "separate") {
[$h, $v] = $style->border_spacing;
$v = $v / 2;
$h = $h / 2;
$style->set_used("padding_left", (float)$style->length_in_pt($style->padding_left, $cb["w"]) + $h);
$style->set_used("padding_right", (float)$style->length_in_pt($style->padding_right, $cb["w"]) + $h);
$style->set_used("padding_top", (float)$style->length_in_pt($style->padding_top, $cb["w"]) + $v);
$style->set_used("padding_bottom", (float)$style->length_in_pt($style->padding_bottom, $cb["w"]) + $v);
}
$this->_assign_widths();
// Adjust left & right margins, if they are auto
$delta = $this->_state["width_delta"];
$width = $style->width;
$left = $style->length_in_pt($style->margin_left, $cb["w"]);
$right = $style->length_in_pt($style->margin_right, $cb["w"]);
$diff = (float) $cb["w"] - (float) $width - $delta;
if ($left === "auto" && $right === "auto") {
if ($diff < 0) {
$left = 0;
$right = $diff;
} else {
$left = $right = $diff / 2;
}
} else {
if ($left === "auto") {
$left = max($diff - $right, 0);
}
if ($right === "auto") {
$right = max($diff - $left, 0);
}
}
$style->set_used("margin_left", $left);
$style->set_used("margin_right", $right);
$frame->position();
[$x, $y] = $frame->get_position();
// Determine the content edge
$offset_x = (float)$left + (float)$style->length_in_pt([
$style->padding_left,
$style->border_left_width
], $cb["w"]);
$offset_y = (float)$style->length_in_pt([
$style->margin_top,
$style->border_top_width,
$style->padding_top
], $cb["w"]);
$content_x = $x + $offset_x;
$content_y = $y + $offset_y;
if (isset($cb["h"])) {
$h = $cb["h"];
} else {
$h = null;
}
$cellmap = $frame->get_cellmap();
$col =& $cellmap->get_column(0);
$col["x"] = $offset_x;
$row =& $cellmap->get_row(0);
$row["y"] = $offset_y;
$cellmap->assign_x_positions();
// Set the containing block of each child & reflow
foreach ($frame->get_children() as $child) {
$child->set_containing_block($content_x, $content_y, $width, $h);
$child->reflow();
if (!$page->in_nested_table()) {
// Check if a split has occurred
$page->check_page_break($child);
if ($page->is_full()) {
break;
}
}
}
// Stop reflow if a page break has occurred before the frame, in which
// case it has been reset, including its position
if ($page->is_full() && $frame->get_position("x") === null) {
$page->table_reflow_end();
return;
}
// Assign heights to our cells:
$style->set_used("height", $this->_calculate_height());
$page->table_reflow_end();
if ($block && $frame->is_in_flow()) {
$block->add_frame_to_line($frame);
if ($frame->is_block_level()) {
$block->add_line();
}
}
}
public function get_min_max_width(): array
{
if (!is_null($this->_min_max_cache)) {
return $this->_min_max_cache;
}
$style = $this->_frame->get_style();
$cellmap = $this->_frame->get_cellmap();
$this->_frame->normalize();
// Add the cells to the cellmap (this will calculate column widths as
// frames are added)
$cellmap->add_frame($this->_frame);
// Find the min/max width of the table and sort the columns into
// absolute/percent/auto arrays
$this->_state = [];
$this->_state["min_width"] = 0;
$this->_state["max_width"] = 0;
$this->_state["percent_used"] = 0;
$this->_state["absolute_used"] = 0;
$this->_state["auto_min"] = 0;
$this->_state["absolute"] = [];
$this->_state["percent"] = [];
$this->_state["auto"] = [];
$columns =& $cellmap->get_columns();
foreach ($columns as $i => $col) {
$this->_state["min_width"] += $col["min-width"];
$this->_state["max_width"] += $col["max-width"];
if ($col["absolute"] > 0) {
$this->_state["absolute"][] = $i;
$this->_state["absolute_used"] += $col["min-width"];
} elseif ($col["percent"] > 0) {
$this->_state["percent"][] = $i;
$this->_state["percent_used"] += $col["percent"];
} else {
$this->_state["auto"][] = $i;
$this->_state["auto_min"] += $col["min-width"];
}
}
// Account for margins, borders, padding, and border spacing
$cb_w = $this->_frame->get_containing_block("w");
$lm = (float) $style->length_in_pt($style->margin_left, $cb_w);
$rm = (float) $style->length_in_pt($style->margin_right, $cb_w);
$dims = [
$style->border_left_width,
$style->border_right_width,
$style->padding_left,
$style->padding_right
];
if ($style->border_collapse !== "collapse") {
list($dims[]) = $style->border_spacing;
}
$delta = (float) $style->length_in_pt($dims, $cb_w);
$this->_state["width_delta"] = $delta;
$min_width = $this->_state["min_width"] + $delta + $lm + $rm;
$max_width = $this->_state["max_width"] + $delta + $lm + $rm;
return $this->_min_max_cache = [
$min_width,
$max_width,
"min" => $min_width,
"max" => $max_width
];
}
}
@@ -0,0 +1,165 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Exception;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
use Dompdf\FrameDecorator\TableCell as TableCellFrameDecorator;
use Dompdf\Helpers;
/**
* Reflows table cells
*
* @package dompdf
*/
class TableCell extends Block
{
/**
* TableCell constructor.
* @param BlockFrameDecorator $frame
*/
function __construct(BlockFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
/** @var TableCellFrameDecorator */
$frame = $this->_frame;
$table = TableFrameDecorator::find_parent_table($frame);
if ($table === null) {
throw new Exception("Parent table not found for table cell");
}
// Counters and generated content
$this->_set_content();
$style = $frame->get_style();
$cellmap = $table->get_cellmap();
[$x, $y] = $cellmap->get_frame_position($frame);
$frame->set_position($x, $y);
$cells = $cellmap->get_spanned_cells($frame);
$w = 0;
foreach ($cells["columns"] as $i) {
$col = $cellmap->get_column($i);
$w += $col["used-width"];
}
//FIXME?
$h = $frame->get_containing_block("h");
$left_space = (float)$style->length_in_pt([$style->margin_left,
$style->padding_left,
$style->border_left_width],
$w);
$right_space = (float)$style->length_in_pt([$style->padding_right,
$style->margin_right,
$style->border_right_width],
$w);
$top_space = (float)$style->length_in_pt([$style->margin_top,
$style->padding_top,
$style->border_top_width],
$h);
$bottom_space = (float)$style->length_in_pt([$style->margin_bottom,
$style->padding_bottom,
$style->border_bottom_width],
$h);
$cb_w = $w - $left_space - $right_space;
$style->set_used("width", $cb_w);
$content_x = $x + $left_space;
$content_y = $line_y = $y + $top_space;
// Adjust the first line based on the text-indent property
$indent = (float)$style->length_in_pt($style->text_indent, $w);
$frame->increase_line_width($indent);
$page = $frame->get_root();
// Set the y position of the first line in the cell
$line_box = $frame->get_current_line_box();
$line_box->y = $line_y;
// Set the containing blocks and reflow each child
foreach ($frame->get_children() as $child) {
$child->set_containing_block($content_x, $content_y, $cb_w, $h);
$this->process_clear($child);
$child->reflow($frame);
$this->process_float($child, $content_x, $cb_w);
if ($page->is_full()) {
break;
}
}
// Determine our height
$style_height = (float) $style->length_in_pt($style->height, $h);
$content_height = $this->_calculate_content_height();
$height = max($style_height, $content_height);
$frame->set_content_height($content_height);
// Let the cellmap know our height
$cell_height = $height / count($cells["rows"]);
if ($style_height <= $height) {
$cell_height += $top_space + $bottom_space;
}
foreach ($cells["rows"] as $i) {
$cellmap->set_row_height($i, $cell_height);
}
$style->set_used("height", $height);
$this->_text_align();
$this->vertical_align();
// Handle relative positioning
foreach ($frame->get_children() as $child) {
$this->position_relative($child);
}
}
public function get_min_max_content_width(): array
{
// Ignore percentage values for a specified width here, as they are
// relative to the table width, which is not determined yet
$style = $this->_frame->get_style();
$width = $style->width;
$fixed_width = $width !== "auto" && !Helpers::is_percent($width);
[$min, $max] = $this->get_min_max_child_width();
// For table cells: Use specified width if it is greater than the
// minimum defined by the content
if ($fixed_width) {
$width = (float) $style->length_in_pt($width, 0);
$min = max($width, $min);
$max = $min;
}
// Handle min/max width style properties
$min_width = $this->resolve_min_width(null);
$max_width = $this->resolve_max_width(null);
$min = Helpers::clamp($min, $min_width, $max_width);
$max = Helpers::clamp($max, $min_width, $max_width);
return [$min, $max];
}
}
@@ -0,0 +1,86 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
use Dompdf\FrameDecorator\TableRow as TableRowFrameDecorator;
use Dompdf\Exception;
/**
* Reflows table rows
*
* @package dompdf
*/
class TableRow extends AbstractFrameReflower
{
/**
* TableRow constructor.
* @param TableRowFrameDecorator $frame
*/
function __construct(TableRowFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
/** @var TableRowFrameDecorator */
$frame = $this->_frame;
// Check if a page break is forced
$page = $frame->get_root();
$page->check_forced_page_break($frame);
// Bail if the page is full
if ($page->is_full()) {
return;
}
// Counters and generated content
$this->_set_content();
$frame->position();
$style = $frame->get_style();
$cb = $frame->get_containing_block();
foreach ($frame->get_children() as $child) {
$child->set_containing_block($cb);
$child->reflow();
if ($page->is_full()) {
break;
}
}
if ($page->is_full()) {
return;
}
$table = TableFrameDecorator::find_parent_table($frame);
if ($table === null) {
throw new Exception("Parent table not found for table row");
}
$cellmap = $table->get_cellmap();
$style->set_used("width", $cellmap->get_frame_width($frame));
$style->set_used("height", $cellmap->get_frame_height($frame));
$frame->set_position($cellmap->get_frame_position($frame));
}
/**
* @throws Exception
*/
public function get_min_max_width(): array
{
throw new Exception("Min/max width is undefined for table rows");
}
}
@@ -0,0 +1,81 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Exception;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
use Dompdf\FrameDecorator\TableRowGroup as TableRowGroupFrameDecorator;
/**
* Reflows table row groups (e.g. tbody tags)
*
* @package dompdf
*/
class TableRowGroup extends AbstractFrameReflower
{
/**
* TableRowGroup constructor.
* @param TableRowGroupFrameDecorator $frame
*/
function __construct(TableRowGroupFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(?BlockFrameDecorator $block = null)
{
/** @var TableRowGroupFrameDecorator */
$frame = $this->_frame;
$page = $frame->get_root();
$parent = $frame->get_parent();
$dompdf_generated = $parent->get_frame()->get_node()->nodeName === "dompdf_generated";
// Counters and generated content
$this->_set_content();
$style = $frame->get_style();
$cb = $frame->get_containing_block();
foreach ($frame->get_children() as $child) {
$child->set_containing_block($cb["x"], $cb["y"], $cb["w"], $cb["h"]);
$child->reflow();
// Check if a split has occurred
$page->check_page_break($child);
if ($page->is_full()) {
break;
}
}
if ($page->is_full() && $dompdf_generated && $frame->get_parent() === null) {
return;
}
$table = TableFrameDecorator::find_parent_table($frame);
if ($table === null) {
throw new Exception("Parent table not found for table row group");
}
$cellmap = $table->get_cellmap();
// Stop reflow if a page break has occurred before the frame, in which
// case it is not part of its parent table's cell map yet
if ($page->is_full() && !$cellmap->frame_exists_in_cellmap($frame)) {
return;
}
$style->set_used("width", $cellmap->get_frame_width($frame));
$style->set_used("height", $cellmap->get_frame_height($frame));
$frame->set_position($cellmap->get_frame_position($frame));
}
}
@@ -0,0 +1,601 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Exception;
use Dompdf\FontMetrics;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Inline as InlineFrameDecorator;
use Dompdf\FrameDecorator\Text as TextFrameDecorator;
use Dompdf\Helpers;
/**
* Reflows text frames.
*
* @package dompdf
*/
class Text extends AbstractFrameReflower
{
/**
* PHP string representation of HTML entity <shy>
*/
const SOFT_HYPHEN = "\xC2\xAD";
/**
* The regex splits on everything that's a separator (^\S double negative),
* excluding the following non-breaking space characters:
* * nbsp (\xA0)
* * narrow nbsp (\x{202F})
* * figure space (\x{2007})
*/
public static $_whitespace_pattern = '/([^\S\xA0\x{202F}\x{2007}]+)/u';
/**
* The regex splits on everything that's a separator (^\S double negative)
* plus dashes, excluding the following non-breaking space characters:
* * nbsp (\xA0)
* * narrow nbsp (\x{202F})
* * figure space (\x{2007})
*/
public static $_wordbreak_pattern = '/([^\S\xA0\x{202F}\x{2007}\n]+|\R|\-+|\xAD+)/u';
/**
* Frame for this reflower
*
* @var TextFrameDecorator
*/
protected $_frame;
/**
* @var FontMetrics
*/
private $fontMetrics;
/**
* @param TextFrameDecorator $frame
* @param FontMetrics $fontMetrics
*/
public function __construct(TextFrameDecorator $frame, FontMetrics $fontMetrics)
{
parent::__construct($frame);
$this->setFontMetrics($fontMetrics);
}
/**
* Apply text transform and white-space collapse according to style.
*
* * http://www.w3.org/TR/CSS21/text.html#propdef-text-transform
* * http://www.w3.org/TR/CSS21/text.html#propdef-white-space
*
* @param string $text
* @return string
*/
protected function pre_process_text(string $text): string
{
$style = $this->_frame->get_style();
// Handle text transform
switch ($style->text_transform) {
case "capitalize":
$text = Helpers::mb_ucwords($text);
break;
case "uppercase":
$text = mb_convert_case($text, MB_CASE_UPPER, "UTF-8");
break;
case "lowercase":
$text = mb_convert_case($text, MB_CASE_LOWER, "UTF-8");
break;
default:
break;
}
// Handle white-space collapse
switch ($style->white_space) {
default:
case "normal":
case "nowrap":
$text = preg_replace(self::$_whitespace_pattern, " ", $text) ?? "";
break;
case "pre-line":
// Collapse white space except for line breaks
$text = preg_replace('/([^\S\xA0\x{202F}\x{2007}\n]+)/u', " ", $text) ?? "";
break;
case "pre":
case "pre-wrap":
break;
}
return $text;
}
/**
* @param string $text
* @param BlockFrameDecorator $block
* @param bool $nowrap
*
* @return int|false
*/
protected function line_break(string $text, BlockFrameDecorator $block, bool $nowrap = false)
{
$fontMetrics = $this->getFontMetrics();
$frame = $this->_frame;
$style = $frame->get_style();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
// Determine the available width
$current_line = $block->get_current_line_box();
$line_width = $frame->get_containing_block("w");
$current_line_width = $current_line->left + $current_line->w + $current_line->right;
$available_width = $line_width - $current_line_width;
// Determine the frame width including margin, padding & border
$visible_text = preg_replace('/\xAD/u', "", $text);
$text_width = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
$mbp_width = (float) $style->length_in_pt([
$style->margin_left,
$style->border_left_width,
$style->padding_left,
$style->padding_right,
$style->border_right_width,
$style->margin_right
], $line_width);
$frame_width = $text_width + $mbp_width;
if (Helpers::lengthLessOrEqual($frame_width, $available_width)) {
return false;
}
$force_first = $current_line->left == 0
&& $current_line->right == 0
&& $current_line->is_empty();
if ($nowrap) {
return $force_first ? false : 0;
}
// Split the text into words
$words = preg_split(self::$_wordbreak_pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$wc = count($words);
// Determine the split point
$width = 0.0;
$str = "";
$space_width = $fontMetrics->getTextWidth(" ", $font, $size, $word_spacing, $letter_spacing);
$shy_width = $fontMetrics->getTextWidth(self::SOFT_HYPHEN, $font, $size);
// @todo support <wbr>
for ($i = 0; $i < $wc; $i += 2) {
// Allow trailing white space to overflow. White space is always
// collapsed to the standard space character currently, so only
// handle that for now
$sep = $words[$i + 1] ?? "";
$word = $sep === " " ? $words[$i] : $words[$i] . $sep;
$word_width = $fontMetrics->getTextWidth($word, $font, $size, $word_spacing, $letter_spacing);
$used_width = $width + $word_width + $mbp_width;
if ($used_width > 0 && Helpers::lengthGreater($used_width, $available_width)) {
// If the previous split happened by soft hyphen, we have to
// append its width again because the last hyphen of a line
// won't be removed
if (isset($words[$i - 1]) && self::SOFT_HYPHEN === $words[$i - 1]) {
$width += $shy_width;
}
break;
}
// If the word is splitted by soft hyphen, but no line break is needed
// we have to reduce the width. But the str is not modified, otherwise
// the wrong offset is calculated at the end of this method.
if ($sep === self::SOFT_HYPHEN) {
$width += $word_width - $shy_width;
$str .= $word;
} elseif ($sep === " ") {
$width += $word_width + $space_width;
$str .= $word . $sep;
} else {
$width += $word_width;
$str .= $word;
}
}
// The first word has overflowed. Force it onto the line, or as many
// characters as fit if breaking words is allowed
if ($force_first && $width === 0.0) {
if ($sep === " ") {
$word .= $sep;
}
// https://www.w3.org/TR/css-text-3/#overflow-wrap-property
$wrap = $style->overflow_wrap;
$break_word = $wrap === "anywhere" || $wrap === "break-word";
if ($break_word) {
$s = "";
$len = mb_strlen($word, "UTF-8");
for ($j = 0; $j < $len; $j++) {
$c = mb_substr($word, $j, 1, "UTF-8");
$w = $fontMetrics->getTextWidth($s . $c, $font, $size, $word_spacing, $letter_spacing);
if (Helpers::lengthGreater($w, $available_width)) {
break;
}
$s .= $c;
}
// Always force the first character onto the line
$str = $j === 0 ? $s . $c : $s;
} else {
$str = $word;
}
}
$offset = mb_strlen($str, "UTF-8");
return $offset;
}
/**
* @param string $text
* @return int|false
*/
protected function newline_break(string $text)
{
if (($i = mb_strpos($text, "\n", 0, "UTF-8")) === false) {
return false;
}
return $i + 1;
}
/**
* @param BlockFrameDecorator $block
* @return bool|null Whether to add a new line at the end. `null` if reflow
* should be stopped.
*/
protected function layout_line(BlockFrameDecorator $block): ?bool
{
$frame = $this->_frame;
$style = $frame->get_style();
$current_line = $block->get_current_line_box();
$text = $frame->get_text();
// Trim leading white space if this is the first text on the line
if ($current_line->is_empty() && !$frame->is_pre()) {
$text = ltrim($text, " ");
}
if ($text === "") {
$frame->set_text("");
$style->set_used("width", 0.0);
return false;
}
// Determine the next line break
// http://www.w3.org/TR/CSS21/text.html#propdef-white-space
$white_space = $style->white_space;
$nowrap = $white_space === "nowrap" || $white_space === "pre";
switch ($white_space) {
default:
case "normal":
case "nowrap":
$split = $this->line_break($text, $block, $nowrap);
$add_line = false;
break;
case "pre":
case "pre-line":
case "pre-wrap":
$hard_split = $this->newline_break($text);
$first_line = $hard_split !== false
? mb_substr($text, 0, $hard_split, "UTF-8")
: $text;
$soft_split = $this->line_break($first_line, $block, $nowrap);
$split = $soft_split !== false ? $soft_split : $hard_split;
$add_line = $hard_split !== false;
break;
}
if ($split === 0) {
// Make sure to move text when floating frames leave no space to
// place anything onto the line
// TODO: Would probably be better to move just below the current
// floating frame instead of trying to place text in line-height
// increments
if ($current_line->h === 0.0) {
// Line height might be 0
$h = max($frame->get_margin_height(), 1.0);
$block->maximize_line_height($h, $frame);
}
// Break line and repeat layout
$block->add_line();
// Find the appropriate inline ancestor to split
$child = $frame;
$p = $child->get_parent();
while ($p instanceof InlineFrameDecorator && !$child->get_prev_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if ($p instanceof InlineFrameDecorator) {
// Split parent and stop current reflow. Reflow continues
// via child-reflow loop of split parent
$p->split($child);
return null;
}
return $this->layout_line($block);
}
// Final split point is determined
if ($split !== false && $split < mb_strlen($text, "UTF-8")) {
// Split the line
$frame->set_text($text);
$frame->split_text($split, true);
$add_line = true;
// Remove inner soft hyphens
$t = $frame->get_text();
$shyPosition = mb_strpos($t, self::SOFT_HYPHEN, 0, "UTF-8");
if (false !== $shyPosition && $shyPosition < mb_strlen($t, "UTF-8") - 1) {
$t = str_replace(self::SOFT_HYPHEN, "", mb_substr($t, 0, -1, "UTF-8")) . mb_substr($t, -1, null, "UTF-8");
$frame->set_text($t);
}
} else {
// No split required
// Remove soft hyphens
$text = str_replace(self::SOFT_HYPHEN, "", $text);
$frame->set_text($text);
}
// Set our new width
$frame->recalculate_width();
return $add_line;
}
/**
* @param BlockFrameDecorator|null $block
* @throws Exception
*/
function reflow(?BlockFrameDecorator $block = null)
{
$frame = $this->_frame;
$page = $frame->get_root();
$page->check_forced_page_break($frame);
if ($page->is_full()) {
return;
}
$style = $frame->get_style();
// Handle text transform and white space
$frame->set_text($this->pre_process_text($frame->get_text()));
// map text to fonts based on supported Unicode range
$frame->apply_font_mapping();
$text = $frame->get_text();
// Determine the text height
$size = $style->font_size;
$font = $style->font_family;
$font_height = $this->getFontMetrics()->getFontHeight($font, $size);
$style->set_used("height", $font_height);
if ($block === null) {
return;
}
$add_line = $this->layout_line($block);
if ($add_line === null) {
return;
}
$frame->position();
// Skip wrapped white space between block-level elements in case white
// space is collapsed
$text = $frame->get_text();
if ($text === "" && $frame->get_margin_width() === 0.0) {
return;
}
$line = $block->add_frame_to_line($frame);
$trimmed = trim($text);
// Split the text into words (used to determine spacing between
// words on justified lines)
if ($trimmed !== "") {
$words = preg_split(self::$_whitespace_pattern, $trimmed);
$line->wc += count($words);
}
if ($add_line) {
$block->add_line();
}
}
/**
* Trim trailing white space from the frame text.
*/
public function trim_trailing_ws(): void
{
$this->_frame->trim_trailing_ws();
}
public function reset(): void
{
parent::reset();
}
//........................................................................
public function get_min_max_width(): array
{
$fontMetrics = $this->getFontMetrics();
$frame = $this->_frame;
$style = $frame->get_style();
// Handle text transform and white space
$frame->set_text($this->pre_process_text($frame->get_text()));
// map text to fonts based on supported Unicode range
$frame->apply_font_mapping();
$text = $frame->get_text();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
if (!$frame->is_pre()) {
// Determine whether the frame is at the start of its parent block.
// Trim leading white space in that case
$child = $frame;
$p = $frame->get_parent();
while (!$p->is_block() && !$child->get_prev_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if (!$child->get_prev_sibling()) {
$text = ltrim($text, " ");
}
// Determine whether the frame is at the end of its parent block.
// Trim trailing white space in that case
$child = $frame;
$p = $frame->get_parent();
while (!$p->is_block() && !$child->get_next_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if (!$child->get_next_sibling()) {
$text = rtrim($text, " ");
}
}
// Strip soft hyphens for max-line-width calculations
$visible_text = preg_replace('/\xAD/u', "", $text);
// Determine minimum text width
switch ($style->white_space) {
default:
case "normal":
case "pre-line":
case "pre-wrap":
// The min width is the longest word or, if breaking words is
// allowed with the `anywhere` keyword, the widest character.
// For performance reasons, we only check the first character in
// the latter case.
// https://www.w3.org/TR/css-text-3/#overflow-wrap-property
if ($style->overflow_wrap === "anywhere") {
$char = mb_substr($visible_text, 0, 1, "UTF-8");
$min = $fontMetrics->getTextWidth($char, $font, $size, $word_spacing, $letter_spacing);
} else {
// Find the longest word
$words = preg_split(self::$_wordbreak_pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$lengths = array_map(function ($chunk) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
// Allow trailing white space to overflow. As in actual
// layout above, only handle a single space for now
$sep = $chunk[1] ?? "";
$word = $sep === " " ? $chunk[0] : $chunk[0] . $sep;
return $fontMetrics->getTextWidth($word, $font, $size, $word_spacing, $letter_spacing);
}, array_chunk($words, 2));
$min = max($lengths);
}
break;
case "pre":
// Find the longest line
$lines = array_flip(preg_split("/\R/u", $visible_text));
array_walk($lines, function (&$chunked_text_width, $chunked_text) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
$chunked_text_width = $fontMetrics->getTextWidth($chunked_text, $font, $size, $word_spacing, $letter_spacing);
});
arsort($lines);
$min = reset($lines);
break;
case "nowrap":
$min = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
break;
}
// Determine maximum text width
switch ($style->white_space) {
default:
case "normal":
$max = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
break;
case "pre-line":
case "pre-wrap":
// Find the longest line
$lines = array_flip(preg_split("/\R/u", $visible_text));
array_walk($lines, function (&$chunked_text_width, $chunked_text) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
$chunked_text_width = $fontMetrics->getTextWidth($chunked_text, $font, $size, $word_spacing, $letter_spacing);
});
arsort($lines);
$max = reset($lines);
break;
case "pre":
case "nowrap":
$max = $min;
break;
}
// Account for margins, borders, and padding
$dims = [
$style->padding_left,
$style->padding_right,
$style->border_left_width,
$style->border_right_width,
$style->margin_left,
$style->margin_right
];
// The containing block is not defined yet, treat percentages as 0
$delta = (float) $style->length_in_pt($dims, 0);
$min += $delta;
$max += $delta;
return [$min, $max, "min" => $min, "max" => $max];
}
/**
* @param FontMetrics $fontMetrics
* @return $this
*/
public function setFontMetrics(FontMetrics $fontMetrics)
{
$this->fontMetrics = $fontMetrics;
return $this;
}
/**
* @return FontMetrics
*/
public function getFontMetrics()
{
return $this->fontMetrics;
}
}