PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/src/FrameReflower/Text.php

https://gitlab.com/oritadeu/dompdf
PHP | 487 lines | 274 code | 108 blank | 105 comment | 44 complexity | a374e4f9b99d1b59eced259fc48f0646 MD5 | raw file
  1. <?php
  2. /**
  3. * @package dompdf
  4. * @link http://dompdf.github.com/
  5. * @author Benj Carson <benjcarson@digitaljunkies.ca>
  6. * @author Fabien Ménager <fabien.menager@gmail.com>
  7. * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
  8. */
  9. namespace Dompdf\FrameReflower;
  10. use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
  11. use Dompdf\FrameDecorator\Text as TextFrameDecorator;
  12. use Dompdf\FontMetrics;
  13. /**
  14. * Reflows text frames.
  15. *
  16. * @package dompdf
  17. */
  18. class Text extends AbstractFrameReflower
  19. {
  20. /**
  21. * @var BlockFrameDecorator
  22. */
  23. protected $_block_parent; // Nearest block-level ancestor
  24. /**
  25. * @var TextFrameDecorator
  26. */
  27. protected $_frame;
  28. public static $_whitespace_pattern = "/[ \t\r\n\f]+/u";
  29. /**
  30. * @var FontMetrics
  31. */
  32. private $fontMetrics;
  33. /**
  34. * @param TextFrameDecorator $frame
  35. * @param FontMetrics $fontMetrics
  36. */
  37. public function __construct(TextFrameDecorator $frame, FontMetrics $fontMetrics)
  38. {
  39. parent::__construct($frame);
  40. $this->setFontMetrics($fontMetrics);
  41. }
  42. //........................................................................
  43. protected function _collapse_white_space($text)
  44. {
  45. //$text = $this->_frame->get_text();
  46. // if ( $this->_block_parent->get_current_line_box->w == 0 )
  47. // $text = ltrim($text, " \n\r\t");
  48. return preg_replace(self::$_whitespace_pattern, " ", $text);
  49. }
  50. //........................................................................
  51. protected function _line_break($text)
  52. {
  53. $style = $this->_frame->get_style();
  54. $size = $style->font_size;
  55. $font = $style->font_family;
  56. $current_line = $this->_block_parent->get_current_line_box();
  57. // Determine the available width
  58. $line_width = $this->_frame->get_containing_block("w");
  59. $current_line_width = $current_line->left + $current_line->w + $current_line->right;
  60. $available_width = $line_width - $current_line_width;
  61. // Account for word-spacing
  62. $word_spacing = $style->length_in_pt($style->word_spacing);
  63. $char_spacing = $style->length_in_pt($style->letter_spacing);
  64. // Determine the frame width including margin, padding & border
  65. $text_width = $this->getFontMetrics()->getTextWidth($text, $font, $size, $word_spacing, $char_spacing);
  66. $mbp_width =
  67. $style->length_in_pt(array($style->margin_left,
  68. $style->border_left_width,
  69. $style->padding_left,
  70. $style->padding_right,
  71. $style->border_right_width,
  72. $style->margin_right), $line_width);
  73. $frame_width = $text_width + $mbp_width;
  74. // Debugging:
  75. // Helpers::pre_r("Text: '" . htmlspecialchars($text). "'");
  76. // Helpers::pre_r("width: " .$frame_width);
  77. // Helpers::pre_r("textwidth + delta: $text_width + $mbp_width");
  78. // Helpers::pre_r("font-size: $size");
  79. // Helpers::pre_r("cb[w]: " .$line_width);
  80. // Helpers::pre_r("available width: " . $available_width);
  81. // Helpers::pre_r("current line width: " . $current_line_width);
  82. // Helpers::pre_r($words);
  83. if ($frame_width <= $available_width)
  84. return false;
  85. // split the text into words
  86. $words = preg_split('/([\s-]+)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
  87. $wc = count($words);
  88. // Determine the split point
  89. $width = 0;
  90. $str = "";
  91. reset($words);
  92. // @todo support <shy>, <wbr>
  93. for ($i = 0; $i < $wc; $i += 2) {
  94. $word = $words[$i] . (isset($words[$i + 1]) ? $words[$i + 1] : "");
  95. $word_width = $this->getFontMetrics()->getTextWidth($word, $font, $size, $word_spacing, $char_spacing);
  96. if ($width + $word_width + $mbp_width > $available_width)
  97. break;
  98. $width += $word_width;
  99. $str .= $word;
  100. }
  101. $break_word = ($style->word_wrap === "break-word");
  102. // The first word has overflowed. Force it onto the line
  103. if ($current_line_width == 0 && $width == 0) {
  104. $s = "";
  105. $last_width = 0;
  106. if ($break_word) {
  107. for ($j = 0; $j < strlen($word); $j++) {
  108. $s .= $word[$j];
  109. $_width = $this->getFontMetrics()->getTextWidth($s, $font, $size, $word_spacing, $char_spacing);
  110. if ($_width > $available_width) {
  111. break;
  112. }
  113. $last_width = $_width;
  114. }
  115. }
  116. if ($break_word && $last_width > 0) {
  117. $width += $last_width;
  118. $str .= substr($s, 0, -1);
  119. } else {
  120. $width += $word_width;
  121. $str .= $word;
  122. }
  123. }
  124. $offset = mb_strlen($str);
  125. // More debugging:
  126. // var_dump($str);
  127. // print_r("Width: ". $width);
  128. // print_r("Offset: " . $offset);
  129. return $offset;
  130. }
  131. //........................................................................
  132. protected function _newline_break($text)
  133. {
  134. if (($i = mb_strpos($text, "\n")) === false)
  135. return false;
  136. return $i + 1;
  137. }
  138. //........................................................................
  139. protected function _layout_line()
  140. {
  141. $frame = $this->_frame;
  142. $style = $frame->get_style();
  143. $text = $frame->get_text();
  144. $size = $style->font_size;
  145. $font = $style->font_family;
  146. // Determine the text height
  147. $style->height = $this->getFontMetrics()->getFontHeight($font, $size);
  148. $split = false;
  149. $add_line = false;
  150. // Handle text transform:
  151. // http://www.w3.org/TR/CSS21/text.html#propdef-text-transform
  152. switch (strtolower($style->text_transform)) {
  153. default:
  154. break;
  155. case "capitalize":
  156. $text = mb_convert_case($text, MB_CASE_TITLE);
  157. break;
  158. case "uppercase":
  159. $text = mb_convert_case($text, MB_CASE_UPPER);
  160. break;
  161. case "lowercase":
  162. $text = mb_convert_case($text, MB_CASE_LOWER);
  163. break;
  164. }
  165. // Handle white-space property:
  166. // http://www.w3.org/TR/CSS21/text.html#propdef-white-space
  167. switch ($style->white_space) {
  168. default:
  169. case "normal":
  170. $frame->set_text($text = $this->_collapse_white_space($text));
  171. if ($text == "")
  172. break;
  173. $split = $this->_line_break($text);
  174. break;
  175. case "pre":
  176. $split = $this->_newline_break($text);
  177. $add_line = $split !== false;
  178. break;
  179. case "nowrap":
  180. $frame->set_text($text = $this->_collapse_white_space($text));
  181. break;
  182. case "pre-wrap":
  183. $split = $this->_newline_break($text);
  184. if (($tmp = $this->_line_break($text)) !== false) {
  185. $add_line = $split < $tmp;
  186. $split = min($tmp, $split);
  187. } else
  188. $add_line = true;
  189. break;
  190. case "pre-line":
  191. // Collapse white-space except for \n
  192. $frame->set_text($text = preg_replace("/[ \t]+/u", " ", $text));
  193. if ($text == "")
  194. break;
  195. $split = $this->_newline_break($text);
  196. if (($tmp = $this->_line_break($text)) !== false) {
  197. $add_line = $split < $tmp;
  198. $split = min($tmp, $split);
  199. } else
  200. $add_line = true;
  201. break;
  202. }
  203. // Handle degenerate case
  204. if ($text === "")
  205. return;
  206. if ($split !== false) {
  207. // Handle edge cases
  208. if ($split == 0 && $text === " ") {
  209. $frame->set_text("");
  210. return;
  211. }
  212. if ($split == 0) {
  213. // Trim newlines from the beginning of the line
  214. //$this->_frame->set_text(ltrim($text, "\n\r"));
  215. $this->_block_parent->add_line();
  216. $frame->position();
  217. // Layout the new line
  218. $this->_layout_line();
  219. } else if ($split < mb_strlen($frame->get_text())) {
  220. // split the line if required
  221. $frame->split_text($split);
  222. $t = $frame->get_text();
  223. // Remove any trailing newlines
  224. if ($split > 1 && $t[$split - 1] === "\n" && !$frame->is_pre())
  225. $frame->set_text(mb_substr($t, 0, -1));
  226. // Do we need to trim spaces on wrapped lines? This might be desired, however, we
  227. // can't trim the lines here or the layout will be affected if trimming the line
  228. // leaves enough space to fit the next word in the text stream (because pdf layout
  229. // is performed elsewhere).
  230. /*if (!$this->_frame->get_prev_sibling() && !$this->_frame->get_next_sibling()) {
  231. $t = $this->_frame->get_text();
  232. $this->_frame->set_text( trim($t) );
  233. }*/
  234. }
  235. if ($add_line) {
  236. $this->_block_parent->add_line();
  237. $frame->position();
  238. }
  239. } else {
  240. // Remove empty space from start and end of line, but only where there isn't an inline sibling
  241. // and the parent node isn't an inline element with siblings
  242. // FIXME: Include non-breaking spaces?
  243. $t = $frame->get_text();
  244. $parent = $frame->get_parent();
  245. $is_inline_frame = get_class($parent) === 'Inline_Frame_Decorator';
  246. if ((!$is_inline_frame && !$frame->get_next_sibling()) /* ||
  247. ( $is_inline_frame && !$parent->get_next_sibling())*/
  248. ) { // fails <b>BOLD <u>UNDERLINED</u></b> becomes <b>BOLD<u>UNDERLINED</u></b>
  249. $t = rtrim($t);
  250. }
  251. if ((!$is_inline_frame && !$frame->get_prev_sibling()) /* ||
  252. ( $is_inline_frame && !$parent->get_prev_sibling())*/
  253. ) { // <span><span>A<span>B</span> C</span></span> fails (the whitespace is removed)
  254. $t = ltrim($t);
  255. }
  256. $frame->set_text($t);
  257. }
  258. // Set our new width
  259. $width = $frame->recalculate_width();
  260. }
  261. //........................................................................
  262. function reflow(BlockFrameDecorator $block = null)
  263. {
  264. $frame = $this->_frame;
  265. $page = $frame->get_root();
  266. $page->check_forced_page_break($this->_frame);
  267. if ($page->is_full())
  268. return;
  269. $this->_block_parent = /*isset($block) ? $block : */
  270. $frame->find_block_parent();
  271. // Left trim the text if this is the first text on the line and we're
  272. // collapsing white space
  273. // if ( $this->_block_parent->get_current_line()->w == 0 &&
  274. // ($frame->get_style()->white_space !== "pre" ||
  275. // $frame->get_style()->white_space !== "pre-wrap") ) {
  276. // $frame->set_text( ltrim( $frame->get_text() ) );
  277. // }
  278. $frame->position();
  279. $this->_layout_line();
  280. if ($block) {
  281. $block->add_frame_to_line($frame);
  282. }
  283. }
  284. //........................................................................
  285. // Returns an array(0 => min, 1 => max, "min" => min, "max" => max) of the
  286. // minimum and maximum widths of this frame
  287. function get_min_max_width()
  288. {
  289. /*if ( !is_null($this->_min_max_cache) )
  290. return $this->_min_max_cache;*/
  291. $frame = $this->_frame;
  292. $style = $frame->get_style();
  293. $this->_block_parent = $frame->find_block_parent();
  294. $line_width = $frame->get_containing_block("w");
  295. $str = $text = $frame->get_text();
  296. $size = $style->font_size;
  297. $font = $style->font_family;
  298. $word_spacing = $style->length_in_pt($style->word_spacing);
  299. $char_spacing = $style->length_in_pt($style->letter_spacing);
  300. switch ($style->white_space) {
  301. default:
  302. case "normal":
  303. $str = preg_replace(self::$_whitespace_pattern, " ", $str);
  304. case "pre-wrap":
  305. case "pre-line":
  306. // Find the longest word (i.e. minimum length)
  307. // This technique (using arrays & an anonymous function) is actually
  308. // faster than doing a single-pass character by character scan. Heh,
  309. // yes I took the time to bench it ;)
  310. $words = array_flip(preg_split("/[\s-]+/u", $str, -1, PREG_SPLIT_DELIM_CAPTURE));
  311. $root = $this;
  312. array_walk($words, function(&$val, $str) use ($font, $size, $word_spacing, $char_spacing, $root) {
  313. $val = $root->getFontMetrics()->getTextWidth($str, $font, $size, $word_spacing, $char_spacing);
  314. });
  315. arsort($words);
  316. $min = reset($words);
  317. break;
  318. case "pre":
  319. $lines = array_flip(preg_split("/\n/u", $str));
  320. $root = $this;
  321. array_walk($lines, function(&$val, $str) use ($font, $size, $word_spacing, $char_spacing, $root) {
  322. $val = $root->getFontMetrics()->getTextWidth($str, $font, $size, $word_spacing, $char_spacing);
  323. });
  324. arsort($lines);
  325. $min = reset($lines);
  326. break;
  327. case "nowrap":
  328. $min = $this->getFontMetrics()->getTextWidth($this->_collapse_white_space($str), $font, $size, $word_spacing, $char_spacing);
  329. break;
  330. }
  331. switch ($style->white_space) {
  332. default:
  333. case "normal":
  334. case "nowrap":
  335. $str = preg_replace(self::$_whitespace_pattern, " ", $text);
  336. break;
  337. case "pre-line":
  338. //XXX: Is this correct?
  339. $str = preg_replace("/[ \t]+/u", " ", $text);
  340. case "pre-wrap":
  341. // Find the longest word (i.e. minimum length)
  342. $lines = array_flip(preg_split("/\n/", $text));
  343. $root = $this;
  344. array_walk($lines, function(&$val, $str) use ($font, $size, $word_spacing, $char_spacing, $root) {
  345. $val = $root->getFontMetrics()->getTextWidth($str, $font, $size, $word_spacing, $char_spacing);
  346. });
  347. arsort($lines);
  348. reset($lines);
  349. $str = key($lines);
  350. break;
  351. }
  352. $max = $this->getFontMetrics()->getTextWidth($str, $font, $size, $word_spacing, $char_spacing);
  353. $delta = $style->length_in_pt(array($style->margin_left,
  354. $style->border_left_width,
  355. $style->padding_left,
  356. $style->padding_right,
  357. $style->border_right_width,
  358. $style->margin_right), $line_width);
  359. $min += $delta;
  360. $max += $delta;
  361. return $this->_min_max_cache = array($min, $max, "min" => $min, "max" => $max);
  362. }
  363. /**
  364. * @param FontMetrics $fontMetrics
  365. * @return $this
  366. */
  367. public function setFontMetrics(FontMetrics $fontMetrics)
  368. {
  369. $this->fontMetrics = $fontMetrics;
  370. return $this;
  371. }
  372. /**
  373. * @return FontMetrics
  374. */
  375. public function getFontMetrics()
  376. {
  377. return $this->fontMetrics;
  378. }
  379. }