Initial commit
This commit is contained in:
+607
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user