PluginProbe
MBE eShip / trunk
MBE eShip vtrunk
2.8.1 trunk 1.0.0 1.1.0 1.1.3 1.2.1 1.2.2 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.6.0 1.7.0 1.7.1 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1.0 2.1.1 2.1.2 2.2.1 All 37 releases
mail-boxes-etc / lib / dompdf / src / FrameReflower / Text.php

Text.php in MBE eShip trunk, at lib/dompdf/src/FrameReflower/Text.php

606 lines 20.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package dompdf
4 * @link https://github.com/dompdf/dompdf
5 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
6 */
7 namespace Dompdf\FrameReflower;
8
9 use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
10 use Dompdf\FrameDecorator\Inline as InlineFrameDecorator;
11 use Dompdf\FrameDecorator\Text as TextFrameDecorator;
12 use Dompdf\FontMetrics;
13 use Dompdf\Helpers;
14
15 /**
16 * Reflows text frames.
17 *
18 * @package dompdf
19 */
20 class Text extends AbstractFrameReflower
21 {
22 /**
23 * PHP string representation of HTML entity <shy>
24 */
25 const SOFT_HYPHEN = "\xC2\xAD";
26
27 /**
28 * The regex splits on everything that's a separator (^\S double negative),
29 * excluding the following non-breaking space characters:
30 * * nbsp (\xA0)
31 * * narrow nbsp (\x{202F})
32 * * figure space (\x{2007})
33 */
34 public static $_whitespace_pattern = '/([^\S\xA0\x{202F}\x{2007}]+)/u';
35
36 /**
37 * The regex splits on everything that's a separator (^\S double negative)
38 * plus dashes, excluding the following non-breaking space characters:
39 * * nbsp (\xA0)
40 * * narrow nbsp (\x{202F})
41 * * figure space (\x{2007})
42 */
43 public static $_wordbreak_pattern = '/([^\S\xA0\x{202F}\x{2007}\n]+|\R|\-+|\xAD+)/u';
44
45 /**
46 * Frame for this reflower
47 *
48 * @var TextFrameDecorator
49 */
50 protected $_frame;
51
52 /**
53 * Saves trailing whitespace trimmed after a line break, so it can be
54 * restored when needed.
55 *
56 * @var string|null
57 */
58 protected $trailingWs = null;
59
60 /**
61 * @var FontMetrics
62 */
63 private $fontMetrics;
64
65 /**
66 * @param TextFrameDecorator $frame
67 * @param FontMetrics $fontMetrics
68 */
69 public function __construct(TextFrameDecorator $frame, FontMetrics $fontMetrics)
70 {
71 parent::__construct($frame);
72 $this->setFontMetrics($fontMetrics);
73 }
74
75 /**
76 * Apply text transform and white-space collapse according to style.
77 *
78 * * http://www.w3.org/TR/CSS21/text.html#propdef-text-transform
79 * * http://www.w3.org/TR/CSS21/text.html#propdef-white-space
80 *
81 * @param string $text
82 * @return string
83 */
84 protected function pre_process_text(string $text): string
85 {
86 $style = $this->_frame->get_style();
87
88 // Handle text transform
89 switch ($style->text_transform) {
90 case "capitalize":
91 $text = Helpers::mb_ucwords($text);
92 break;
93 case "uppercase":
94 $text = mb_convert_case($text, MB_CASE_UPPER);
95 break;
96 case "lowercase":
97 $text = mb_convert_case($text, MB_CASE_LOWER);
98 break;
99 default:
100 break;
101 }
102
103 // Handle white-space collapse
104 switch ($style->white_space) {
105 default:
106 case "normal":
107 case "nowrap":
108 $text = preg_replace(self::$_whitespace_pattern, " ", $text) ?? "";
109 break;
110
111 case "pre-line":
112 // Collapse white space except for line breaks
113 $text = preg_replace('/([^\S\xA0\x{202F}\x{2007}\n]+)/u', " ", $text) ?? "";
114 break;
115
116 case "pre":
117 case "pre-wrap":
118 break;
119
120 }
121
122 return $text;
123 }
124
125 /**
126 * @param string $text
127 * @param BlockFrameDecorator $block
128 * @param bool $nowrap
129 *
130 * @return bool|int
131 */
132 protected function line_break(string $text, BlockFrameDecorator $block, bool $nowrap = false)
133 {
134 $fontMetrics = $this->getFontMetrics();
135 $frame = $this->_frame;
136 $style = $frame->get_style();
137 $font = $style->font_family;
138 $size = $style->font_size;
139 $word_spacing = $style->word_spacing;
140 $letter_spacing = $style->letter_spacing;
141
142 // Determine the available width
143 $current_line = $block->get_current_line_box();
144 $line_width = $frame->get_containing_block("w");
145 $current_line_width = $current_line->left + $current_line->w + $current_line->right;
146 $available_width = $line_width - $current_line_width;
147
148 // Determine the frame width including margin, padding & border
149 $visible_text = preg_replace('/\xAD/u', "", $text);
150 $text_width = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
151 $mbp_width = (float) $style->length_in_pt([
152 $style->margin_left,
153 $style->border_left_width,
154 $style->padding_left,
155 $style->padding_right,
156 $style->border_right_width,
157 $style->margin_right
158 ], $line_width);
159 $frame_width = $text_width + $mbp_width;
160
161 if (Helpers::lengthLessOrEqual($frame_width, $available_width)) {
162 return false;
163 }
164
165 if ($nowrap) {
166 return $current_line_width == 0 ? false : 0;
167 }
168
169 // Split the text into words
170 $words = preg_split(self::$_wordbreak_pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE);
171 $wc = count($words);
172
173 // Determine the split point
174 $width = 0.0;
175 $str = "";
176
177 $space_width = $fontMetrics->getTextWidth(" ", $font, $size, $word_spacing, $letter_spacing);
178 $shy_width = $fontMetrics->getTextWidth(self::SOFT_HYPHEN, $font, $size);
179
180 // @todo support <wbr>
181 for ($i = 0; $i < $wc; $i += 2) {
182 // Allow trailing white space to overflow. White space is always
183 // collapsed to the standard space character currently, so only
184 // handle that for now
185 $sep = $words[$i + 1] ?? "";
186 $word = $sep === " " ? $words[$i] : $words[$i] . $sep;
187 $word_width = $fontMetrics->getTextWidth($word, $font, $size, $word_spacing, $letter_spacing);
188 $used_width = $width + $word_width + $mbp_width;
189
190 if (Helpers::lengthGreater($used_width, $available_width)) {
191 // If the previous split happened by soft hyphen, we have to
192 // append its width again because the last hyphen of a line
193 // won't be removed
194 if (isset($words[$i - 1]) && self::SOFT_HYPHEN === $words[$i - 1]) {
195 $width += $shy_width;
196 }
197 break;
198 }
199
200 // If the word is splitted by soft hyphen, but no line break is needed
201 // we have to reduce the width. But the str is not modified, otherwise
202 // the wrong offset is calculated at the end of this method.
203 if ($sep === self::SOFT_HYPHEN) {
204 $width += $word_width - $shy_width;
205 $str .= $word;
206 } elseif ($sep === " ") {
207 $width += $word_width + $space_width;
208 $str .= $word . $sep;
209 } else {
210 $width += $word_width;
211 $str .= $word;
212 }
213 }
214
215 // The first word has overflowed. Force it onto the line, or as many
216 // characters as fit if breaking words is allowed
217 if ($current_line_width == 0 && $width === 0.0) {
218 if ($sep === " ") {
219 $word .= $sep;
220 }
221
222 // https://www.w3.org/TR/css-text-3/#overflow-wrap-property
223 $wrap = $style->overflow_wrap;
224 $break_word = $wrap === "anywhere" || $wrap === "break-word";
225
226 if ($break_word) {
227 $s = "";
228
229 for ($j = 0; $j < mb_strlen($word); $j++) {
230 $c = mb_substr($word, $j, 1);
231 $w = $fontMetrics->getTextWidth($s . $c, $font, $size, $word_spacing, $letter_spacing);
232
233 if (Helpers::lengthGreater($w, $available_width)) {
234 break;
235 }
236
237 $s .= $c;
238 }
239
240 // Always force the first character onto the line
241 $str = $j === 0 ? $s . $c : $s;
242 } else {
243 $str = $word;
244 }
245 }
246
247 $offset = mb_strlen($str);
248 return $offset;
249 }
250
251 /**
252 * @param string $text
253 * @return bool|int
254 */
255 protected function newline_break(string $text)
256 {
257 if (($i = mb_strpos($text, "\n")) === false) {
258 return false;
259 }
260
261 return $i + 1;
262 }
263
264 /**
265 * @param BlockFrameDecorator $block
266 * @return bool|null Whether to add a new line at the end. `null` if reflow
267 * should be stopped.
268 */
269 protected function layout_line(BlockFrameDecorator $block): ?bool
270 {
271 $frame = $this->_frame;
272 $style = $frame->get_style();
273 $current_line = $block->get_current_line_box();
274 $text = $frame->get_text();
275
276 // Trim leading white space if this is the first text on the line
277 if ($current_line->w === 0.0 && !$frame->is_pre()) {
278 $text = ltrim($text, " ");
279 }
280
281 // Exclude wrapped white space. This handles white space between block
282 // elements in case white space is collapsed
283 if ($text === "") {
284 $frame->set_text("");
285 $style->set_used("width", 0.0);
286 return null;
287 }
288
289 // Determine the next line break
290 // http://www.w3.org/TR/CSS21/text.html#propdef-white-space
291 $white_space = $style->white_space;
292 $nowrap = $white_space === "nowrap" || $white_space === "pre";
293
294 switch ($white_space) {
295 default:
296 case "normal":
297 case "nowrap":
298 $split = $this->line_break($text, $block, $nowrap);
299 $add_line = false;
300 break;
301
302 case "pre":
303 case "pre-line":
304 case "pre-wrap":
305 $hard_split = $this->newline_break($text);
306 $first_line = $hard_split !== false
307 ? mb_substr($text, 0, $hard_split)
308 : $text;
309 $soft_split = $this->line_break($first_line, $block, $nowrap);
310
311 $split = $soft_split !== false ? $soft_split : $hard_split;
312 $add_line = $hard_split !== false;
313 break;
314 }
315
316 if ($split === 0) {
317 // Make sure to move text when floating frames leave no space to
318 // place anything onto the line
319 // TODO: Would probably be better to move just below the current
320 // floating frame instead of trying to place text in line-height
321 // increments
322 if ($current_line->h === 0.0) {
323 // Line height might be 0
324 $h = max($frame->get_margin_height(), 1.0);
325 $block->maximize_line_height($h, $frame);
326 }
327
328 // Break line and repeat layout
329 $block->add_line();
330
331 // Find the appropriate inline ancestor to split
332 $child = $frame;
333 $p = $child->get_parent();
334 while ($p instanceof InlineFrameDecorator && !$child->get_prev_sibling()) {
335 $child = $p;
336 $p = $p->get_parent();
337 }
338
339 if ($p instanceof InlineFrameDecorator) {
340 // Split parent and stop current reflow. Reflow continues
341 // via child-reflow loop of split parent
342 $p->split($child);
343 return null;
344 }
345
346 return $this->layout_line($block);
347 }
348
349 // Final split point is determined
350 if ($split !== false && $split < mb_strlen($text)) {
351 // Split the line
352 $frame->set_text($text);
353 $frame->split_text($split);
354 $add_line = true;
355
356 // Remove inner soft hyphens
357 $t = $frame->get_text();
358 $shyPosition = mb_strpos($t, self::SOFT_HYPHEN);
359 if (false !== $shyPosition && $shyPosition < mb_strlen($t) - 1) {
360 $t = str_replace(self::SOFT_HYPHEN, "", mb_substr($t, 0, -1)) . mb_substr($t, -1);
361 $frame->set_text($t);
362 }
363 } else {
364 // No split required
365 // Remove soft hyphens
366 $text = str_replace(self::SOFT_HYPHEN, "", $text);
367 $frame->set_text($text);
368 }
369
370 // Set our new width
371 $frame->recalculate_width();
372
373 return $add_line;
374 }
375
376 /**
377 * @param BlockFrameDecorator|null $block
378 */
379 function reflow(BlockFrameDecorator $block = null)
380 {
381 $frame = $this->_frame;
382 $page = $frame->get_root();
383 $page->check_forced_page_break($frame);
384
385 if ($page->is_full()) {
386 return;
387 }
388
389 // Determine the text height
390 $style = $frame->get_style();
391 $size = $style->font_size;
392 $font = $style->font_family;
393 $font_height = $this->getFontMetrics()->getFontHeight($font, $size);
394 $style->set_used("height", $font_height);
395
396 // Handle text transform and white space
397 $text = $this->pre_process_text($frame->get_text());
398 $frame->set_text($text);
399
400 $add_line = $this->layout_line($block);
401
402 if ($add_line === null) {
403 return;
404 }
405
406 $frame->position();
407
408 if ($block) {
409 $line = $block->add_frame_to_line($frame);
410 $trimmed = trim($frame->get_text());
411
412 // Split the text into words (used to determine spacing between
413 // words on justified lines)
414 if ($trimmed !== "") {
415 $words = preg_split(self::$_whitespace_pattern, $trimmed);
416 $line->wc += count($words);
417 }
418
419 if ($add_line) {
420 $block->add_line();
421 }
422 }
423 }
424
425 /**
426 * Trim trailing white space from the frame text.
427 */
428 public function trim_trailing_ws(): void
429 {
430 $frame = $this->_frame;
431 $text = $frame->get_text();
432 $trailing = mb_substr($text, -1);
433
434 // White space is always collapsed to the standard space character
435 // currently, so only handle that for now
436 if ($trailing === " ") {
437 $this->trailingWs = $trailing;
438 $frame->set_text(mb_substr($text, 0, -1));
439 $frame->recalculate_width();
440 }
441 }
442
443 public function reset(): void
444 {
445 parent::reset();
446
447 // Restore trimmed trailing white space, as the frame will go through
448 // another reflow and line breaks might be different after a split
449 if ($this->trailingWs !== null) {
450 $text = $this->_frame->get_text();
451 $this->_frame->set_text($text . $this->trailingWs);
452 $this->trailingWs = null;
453 }
454 }
455
456 //........................................................................
457
458 public function get_min_max_width(): array
459 {
460 $fontMetrics = $this->getFontMetrics();
461 $frame = $this->_frame;
462 $style = $frame->get_style();
463 $text = $frame->get_text();
464 $font = $style->font_family;
465 $size = $style->font_size;
466 $word_spacing = $style->word_spacing;
467 $letter_spacing = $style->letter_spacing;
468
469 // Handle text transform and white space
470 $text = $this->pre_process_text($frame->get_text());
471
472 if (!$frame->is_pre()) {
473 // Determine whether the frame is at the start of its parent block.
474 // Trim leading white space in that case
475 $child = $frame;
476 $p = $frame->get_parent();
477 while (!$p->is_block() && !$child->get_prev_sibling()) {
478 $child = $p;
479 $p = $p->get_parent();
480 }
481
482 if (!$child->get_prev_sibling()) {
483 $text = ltrim($text, " ");
484 }
485
486 // Determine whether the frame is at the end of its parent block.
487 // Trim trailing white space in that case
488 $child = $frame;
489 $p = $frame->get_parent();
490 while (!$p->is_block() && !$child->get_next_sibling()) {
491 $child = $p;
492 $p = $p->get_parent();
493 }
494
495 if (!$child->get_next_sibling()) {
496 $text = rtrim($text, " ");
497 }
498 }
499
500 // Strip soft hyphens for max-line-width calculations
501 $visible_text = preg_replace('/\xAD/u', "", $text);
502
503 // Determine minimum text width
504 switch ($style->white_space) {
505 default:
506 case "normal":
507 case "pre-line":
508 case "pre-wrap":
509 // The min width is the longest word or, if breaking words is
510 // allowed with the `anywhere` keyword, the widest character.
511 // For performance reasons, we only check the first character in
512 // the latter case.
513 // https://www.w3.org/TR/css-text-3/#overflow-wrap-property
514 if ($style->overflow_wrap === "anywhere") {
515 $char = mb_substr($visible_text, 0, 1);
516 $min = $fontMetrics->getTextWidth($char, $font, $size, $word_spacing, $letter_spacing);
517 } else {
518 // Find the longest word
519 $words = preg_split(self::$_wordbreak_pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE);
520 $lengths = array_map(function ($chunk) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
521 // Allow trailing white space to overflow. As in actual
522 // layout above, only handle a single space for now
523 $sep = $chunk[1] ?? "";
524 $word = $sep === " " ? $chunk[0] : $chunk[0] . $sep;
525 return $fontMetrics->getTextWidth($word, $font, $size, $word_spacing, $letter_spacing);
526 }, array_chunk($words, 2));
527 $min = max($lengths);
528 }
529 break;
530
531 case "pre":
532 // Find the longest line
533 $lines = array_flip(preg_split("/\R/u", $visible_text));
534 array_walk($lines, function (&$chunked_text_width, $chunked_text) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
535 $chunked_text_width = $fontMetrics->getTextWidth($chunked_text, $font, $size, $word_spacing, $letter_spacing);
536 });
537 arsort($lines);
538 $min = reset($lines);
539 break;
540
541 case "nowrap":
542 $min = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
543 break;
544 }
545
546 // Determine maximum text width
547 switch ($style->white_space) {
548 default:
549 case "normal":
550 $max = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
551 break;
552
553 case "pre-line":
554 case "pre-wrap":
555 // Find the longest line
556 $lines = array_flip(preg_split("/\R/u", $visible_text));
557 array_walk($lines, function (&$chunked_text_width, $chunked_text) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
558 $chunked_text_width = $fontMetrics->getTextWidth($chunked_text, $font, $size, $word_spacing, $letter_spacing);
559 });
560 arsort($lines);
561 $max = reset($lines);
562 break;
563
564 case "pre":
565 case "nowrap":
566 $max = $min;
567 break;
568 }
569
570 // Account for margins, borders, and padding
571 $dims = [
572 $style->padding_left,
573 $style->padding_right,
574 $style->border_left_width,
575 $style->border_right_width,
576 $style->margin_left,
577 $style->margin_right
578 ];
579
580 // The containing block is not defined yet, treat percentages as 0
581 $delta = (float) $style->length_in_pt($dims, 0);
582 $min += $delta;
583 $max += $delta;
584
585 return [$min, $max, "min" => $min, "max" => $max];
586 }
587
588 /**
589 * @param FontMetrics $fontMetrics
590 * @return $this
591 */
592 public function setFontMetrics(FontMetrics $fontMetrics)
593 {
594 $this->fontMetrics = $fontMetrics;
595 return $this;
596 }
597
598 /**
599 * @return FontMetrics
600 */
601 public function getFontMetrics()
602 {
603 return $this->fontMetrics;
604 }
605 }
606