ok
Direktori : /home2/selectio/www/myclassicfit.com/vendor/dompdf/dompdf/src/FrameReflower/ |
Current File : /home2/selectio/www/myclassicfit.com/vendor/dompdf/dompdf/src/FrameReflower/Text.php |
<?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; } }