File "Text.php"
Full Path: /home/pulsehostuk9/public_html/invoicer.pulsehost.co.uk/vendor/dompdf/Text.php
File size: 20.17 KB
MIME-type: text/x-php
Charset: utf-8
<?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;
use Dompdf\FontMetrics;
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;
/**
* Saves trailing whitespace trimmed after a line break, so it can be
* restored when needed.
*
* @var string|null
*/
protected $trailingWs = null;
/**
* @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);
break;
case "lowercase":
$text = mb_convert_case($text, MB_CASE_LOWER);
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 bool|int
*/
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;
}
if ($nowrap) {
return $current_line_width == 0 ? 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 (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 ($current_line_width == 0 && $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 = "";
for ($j = 0; $j < mb_strlen($word); $j++) {
$c = mb_substr($word, $j, 1);
$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);
return $offset;
}
/**
* @param string $text
* @return bool|int
*/
protected function newline_break(string $text)
{
if (($i = mb_strpos($text, "\n")) === 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->w === 0.0 && !$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)
: $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)) {
// Split the line
$frame->set_text($text);
$frame->split_text($split);
$add_line = true;
// Remove inner soft hyphens
$t = $frame->get_text();
$shyPosition = mb_strpos($t, self::SOFT_HYPHEN);
if (false !== $shyPosition && $shyPosition < mb_strlen($t) - 1) {
$t = str_replace(self::SOFT_HYPHEN, "", mb_substr($t, 0, -1)) . mb_substr($t, -1);
$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
*/
function reflow(BlockFrameDecorator $block = null)
{
$frame = $this->_frame;
$page = $frame->get_root();
$page->check_forced_page_break($frame);
if ($page->is_full()) {
return;
}
// Determine the text height
$style = $frame->get_style();
$size = $style->font_size;
$font = $style->font_family;
$font_height = $this->getFontMetrics()->getFontHeight($font, $size);
$style->set_used("height", $font_height);
// Handle text transform and white space
$text = $this->pre_process_text($frame->get_text());
$frame->set_text($text);
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
if ($frame->get_text() === "" && $frame->get_margin_width() === 0.0) {
return;
}
$line = $block->add_frame_to_line($frame);
$trimmed = trim($frame->get_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
{
$frame = $this->_frame;
$text = $frame->get_text();
$trailing = mb_substr($text, -1);
// White space is always collapsed to the standard space character
// currently, so only handle that for now
if ($trailing === " ") {
$this->trailingWs = $trailing;
$frame->set_text(mb_substr($text, 0, -1));
$frame->recalculate_width();
}
}
public function reset(): void
{
parent::reset();
// Restore trimmed trailing white space, as the frame will go through
// another reflow and line breaks might be different after a split
if ($this->trailingWs !== null) {
$text = $this->_frame->get_text();
$this->_frame->set_text($text . $this->trailingWs);
$this->trailingWs = null;
}
}
//........................................................................
public function get_min_max_width(): array
{
$fontMetrics = $this->getFontMetrics();
$frame = $this->_frame;
$style = $frame->get_style();
$text = $frame->get_text();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
// Handle text transform and white space
$text = $this->pre_process_text($frame->get_text());
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);
$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;
}
}