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

/include/text_frame_reflower.cls.php

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