frame_reflower.cls.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. <?php
  2. /**
  3. * @package dompdf
  4. * @link http://dompdf.github.com/
  5. * @author Benj Carson <benjcarson@digitaljunkies.ca>
  6. * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
  7. */
  8. /**
  9. * Base reflower class
  10. *
  11. * Reflower objects are responsible for determining the width and height of
  12. * individual frames. They also create line and page breaks as necessary.
  13. *
  14. * @access private
  15. * @package dompdf
  16. */
  17. abstract class Frame_Reflower {
  18. /**
  19. * Frame for this reflower
  20. *
  21. * @var Frame
  22. */
  23. protected $_frame;
  24. /**
  25. * Cached min/max size
  26. *
  27. * @var array
  28. */
  29. protected $_min_max_cache;
  30. function __construct(Frame $frame) {
  31. $this->_frame = $frame;
  32. $this->_min_max_cache = null;
  33. }
  34. function dispose() {
  35. clear_object($this);
  36. }
  37. /**
  38. * @return DOMPDF
  39. */
  40. function get_dompdf() {
  41. return $this->_frame->get_dompdf();
  42. }
  43. /**
  44. * Collapse frames margins
  45. * http://www.w3.org/TR/CSS2/box.html#collapsing-margins
  46. */
  47. protected function _collapse_margins() {
  48. $frame = $this->_frame;
  49. $cb = $frame->get_containing_block();
  50. $style = $frame->get_style();
  51. if ( !$frame->is_in_flow() ) {
  52. return;
  53. }
  54. $t = $style->length_in_pt($style->margin_top, $cb["h"]);
  55. $b = $style->length_in_pt($style->margin_bottom, $cb["h"]);
  56. // Handle 'auto' values
  57. if ( $t === "auto" ) {
  58. $style->margin_top = "0pt";
  59. $t = 0;
  60. }
  61. if ( $b === "auto" ) {
  62. $style->margin_bottom = "0pt";
  63. $b = 0;
  64. }
  65. // Collapse vertical margins:
  66. $n = $frame->get_next_sibling();
  67. if ( $n && !$n->is_block() ) {
  68. while ( $n = $n->get_next_sibling() ) {
  69. if ( $n->is_block() ) {
  70. break;
  71. }
  72. if ( !$n->get_first_child() ) {
  73. $n = null;
  74. break;
  75. }
  76. }
  77. }
  78. if ( $n ) {
  79. $n_style = $n->get_style();
  80. $b = max($b, $n_style->length_in_pt($n_style->margin_top, $cb["h"]));
  81. $n_style->margin_top = "0pt";
  82. $style->margin_bottom = $b."pt";
  83. }
  84. // Collapse our first child's margin
  85. /*$f = $this->_frame->get_first_child();
  86. if ( $f && !$f->is_block() ) {
  87. while ( $f = $f->get_next_sibling() ) {
  88. if ( $f->is_block() ) {
  89. break;
  90. }
  91. if ( !$f->get_first_child() ) {
  92. $f = null;
  93. break;
  94. }
  95. }
  96. }
  97. // Margin are collapsed only between block elements
  98. if ( $f ) {
  99. $f_style = $f->get_style();
  100. $t = max($t, $f_style->length_in_pt($f_style->margin_top, $cb["h"]));
  101. $style->margin_top = $t."pt";
  102. $f_style->margin_bottom = "0pt";
  103. }*/
  104. }
  105. //........................................................................
  106. abstract function reflow(Block_Frame_Decorator $block = null);
  107. //........................................................................
  108. // Required for table layout: Returns an array(0 => min, 1 => max, "min"
  109. // => min, "max" => max) of the minimum and maximum widths of this frame.
  110. // This provides a basic implementation. Child classes should override
  111. // this if necessary.
  112. function get_min_max_width() {
  113. if ( !is_null($this->_min_max_cache) ) {
  114. return $this->_min_max_cache;
  115. }
  116. $style = $this->_frame->get_style();
  117. // Account for margins & padding
  118. $dims = array($style->padding_left,
  119. $style->padding_right,
  120. $style->border_left_width,
  121. $style->border_right_width,
  122. $style->margin_left,
  123. $style->margin_right);
  124. $cb_w = $this->_frame->get_containing_block("w");
  125. $delta = $style->length_in_pt($dims, $cb_w);
  126. // Handle degenerate case
  127. if ( !$this->_frame->get_first_child() ) {
  128. return $this->_min_max_cache = array(
  129. $delta, $delta,
  130. "min" => $delta,
  131. "max" => $delta,
  132. );
  133. }
  134. $low = array();
  135. $high = array();
  136. for ( $iter = $this->_frame->get_children()->getIterator();
  137. $iter->valid();
  138. $iter->next() ) {
  139. $inline_min = 0;
  140. $inline_max = 0;
  141. // Add all adjacent inline widths together to calculate max width
  142. while ( $iter->valid() && in_array( $iter->current()->get_style()->display, Style::$INLINE_TYPES ) ) {
  143. $child = $iter->current();
  144. $minmax = $child->get_min_max_width();
  145. if ( in_array( $iter->current()->get_style()->white_space, array("pre", "nowrap") ) ) {
  146. $inline_min += $minmax["min"];
  147. }
  148. else {
  149. $low[] = $minmax["min"];
  150. }
  151. $inline_max += $minmax["max"];
  152. $iter->next();
  153. }
  154. if ( $inline_max > 0 ) $high[] = $inline_max;
  155. if ( $inline_min > 0 ) $low[] = $inline_min;
  156. if ( $iter->valid() ) {
  157. list($low[], $high[]) = $iter->current()->get_min_max_width();
  158. continue;
  159. }
  160. }
  161. $min = count($low) ? max($low) : 0;
  162. $max = count($high) ? max($high) : 0;
  163. // Use specified width if it is greater than the minimum defined by the
  164. // content. If the width is a percentage ignore it for now.
  165. $width = $style->width;
  166. if ( $width !== "auto" && !is_percent($width) ) {
  167. $width = $style->length_in_pt($width, $cb_w);
  168. if ( $min < $width ) $min = $width;
  169. if ( $max < $width ) $max = $width;
  170. }
  171. $min += $delta;
  172. $max += $delta;
  173. return $this->_min_max_cache = array($min, $max, "min"=>$min, "max"=>$max);
  174. }
  175. /**
  176. * Parses a CSS string containing quotes and escaped hex characters
  177. *
  178. * @param $string string The CSS string to parse
  179. * @param $single_trim
  180. * @return string
  181. */
  182. protected function _parse_string($string, $single_trim = false) {
  183. if ( $single_trim ) {
  184. $string = preg_replace('/^[\"\']/', "", $string);
  185. $string = preg_replace('/[\"\']$/', "", $string);
  186. }
  187. else {
  188. $string = trim($string, "'\"");
  189. }
  190. $string = str_replace(array("\\\n",'\\"',"\\'"),
  191. array("",'"',"'"), $string);
  192. // Convert escaped hex characters into ascii characters (e.g. \A => newline)
  193. $string = preg_replace_callback("/\\\\([0-9a-fA-F]{0,6})/",
  194. create_function('$matches',
  195. 'return unichr(hexdec($matches[1]));'),
  196. $string);
  197. return $string;
  198. }
  199. /**
  200. * Parses a CSS "quotes" property
  201. *
  202. * @return array|null An array of pairs of quotes
  203. */
  204. protected function _parse_quotes() {
  205. // Matches quote types
  206. $re = '/(\'[^\']*\')|(\"[^\"]*\")/';
  207. $quotes = $this->_frame->get_style()->quotes;
  208. // split on spaces, except within quotes
  209. if ( !preg_match_all($re, "$quotes", $matches, PREG_SET_ORDER) ) {
  210. return null;
  211. }
  212. $quotes_array = array();
  213. foreach($matches as &$_quote){
  214. $quotes_array[] = $this->_parse_string($_quote[0], true);
  215. }
  216. if ( empty($quotes_array) ) {
  217. $quotes_array = array('"', '"');
  218. }
  219. return array_chunk($quotes_array, 2);
  220. }
  221. /**
  222. * Parses the CSS "content" property
  223. *
  224. * @return string|null The resulting string
  225. */
  226. protected function _parse_content() {
  227. // Matches generated content
  228. $re = "/\n".
  229. "\s(counters?\\([^)]*\\))|\n".
  230. "\A(counters?\\([^)]*\\))|\n".
  231. "\s([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\3|\n".
  232. "\A([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\5|\n" .
  233. "\s([^\s\"']+)|\n" .
  234. "\A([^\s\"']+)\n".
  235. "/xi";
  236. $content = $this->_frame->get_style()->content;
  237. $quotes = $this->_parse_quotes();
  238. // split on spaces, except within quotes
  239. if ( !preg_match_all($re, $content, $matches, PREG_SET_ORDER) ) {
  240. return null;
  241. }
  242. $text = "";
  243. foreach ($matches as $match) {
  244. if ( isset($match[2]) && $match[2] !== "" ) {
  245. $match[1] = $match[2];
  246. }
  247. if ( isset($match[6]) && $match[6] !== "" ) {
  248. $match[4] = $match[6];
  249. }
  250. if ( isset($match[8]) && $match[8] !== "" ) {
  251. $match[7] = $match[8];
  252. }
  253. if ( isset($match[1]) && $match[1] !== "" ) {
  254. // counters?(...)
  255. $match[1] = mb_strtolower(trim($match[1]));
  256. // Handle counter() references:
  257. // http://www.w3.org/TR/CSS21/generate.html#content
  258. $i = mb_strpos($match[1], ")");
  259. if ( $i === false ) {
  260. continue;
  261. }
  262. $args = explode(",", mb_substr($match[1], 8, $i - 8));
  263. $counter_id = $args[0];
  264. if ( $match[1][7] === "(" ) {
  265. // counter(name [,style])
  266. if ( isset($args[1]) ) {
  267. $type = trim($args[1]);
  268. }
  269. else {
  270. $type = null;
  271. }
  272. $p = $this->_frame->lookup_counter_frame($counter_id);
  273. $text .= $p->counter_value($counter_id, $type);
  274. }
  275. else if ( $match[1][7] === "s" ) {
  276. // counters(name, string [,style])
  277. if ( isset($args[1]) ) {
  278. $string = $this->_parse_string(trim($args[1]));
  279. }
  280. else {
  281. $string = "";
  282. }
  283. if ( isset($args[2]) ) {
  284. $type = $args[2];
  285. }
  286. else {
  287. $type = null;
  288. }
  289. $p = $this->_frame->lookup_counter_frame($counter_id);
  290. $tmp = "";
  291. while ($p) {
  292. $tmp = $p->counter_value($counter_id, $type) . $string . $tmp;
  293. $p = $p->lookup_counter_frame($counter_id);
  294. }
  295. $text .= $tmp;
  296. }
  297. else {
  298. // countertops?
  299. continue;
  300. }
  301. }
  302. else if ( isset($match[4]) && $match[4] !== "" ) {
  303. // String match
  304. $text .= $this->_parse_string($match[4]);
  305. }
  306. else if ( isset($match[7]) && $match[7] !== "" ) {
  307. // Directive match
  308. if ( $match[7] === "open-quote" ) {
  309. // FIXME: do something here
  310. $text .= $quotes[0][0];
  311. }
  312. else if ( $match[7] === "close-quote" ) {
  313. // FIXME: do something else here
  314. $text .= $quotes[0][1];
  315. }
  316. else if ( $match[7] === "no-open-quote" ) {
  317. // FIXME:
  318. }
  319. else if ( $match[7] === "no-close-quote" ) {
  320. // FIXME:
  321. }
  322. else if ( mb_strpos($match[7],"attr(") === 0 ) {
  323. $i = mb_strpos($match[7],")");
  324. if ( $i === false ) {
  325. continue;
  326. }
  327. $attr = mb_substr($match[7], 5, $i - 5);
  328. if ( $attr == "" ) {
  329. continue;
  330. }
  331. $text .= $this->_frame->get_parent()->get_node()->getAttribute($attr);
  332. }
  333. else {
  334. continue;
  335. }
  336. }
  337. }
  338. return $text;
  339. }
  340. /**
  341. * Sets the generated content of a generated frame
  342. */
  343. protected function _set_content(){
  344. $frame = $this->_frame;
  345. $style = $frame->get_style();
  346. if ( $style->counter_reset && ($reset = $style->counter_reset) !== "none" ) {
  347. $vars = preg_split('/\s+/', trim($reset), 2);
  348. $frame->reset_counter($vars[0], isset($vars[1]) ? $vars[1] : 0);
  349. }
  350. if ( $style->counter_increment && ($increment = $style->counter_increment) !== "none" ) {
  351. $frame->increment_counters($increment);
  352. }
  353. if ( $style->content && !$frame->get_first_child() && $frame->get_node()->nodeName === "dompdf_generated" ) {
  354. $content = $this->_parse_content();
  355. $node = $frame->get_node()->ownerDocument->createTextNode($content);
  356. $new_style = $style->get_stylesheet()->create_style();
  357. $new_style->inherit($style);
  358. $new_frame = new Frame($node);
  359. $new_frame->set_style($new_style);
  360. Frame_Factory::decorate_frame($new_frame, $frame->get_dompdf(), $frame->get_root());
  361. $frame->append_child($new_frame);
  362. }
  363. }
  364. }