Viewing file: Text.php (20.17 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?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; } }
|