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
8 namespace Dompdf\FrameReflower
;
10 use Dompdf\Adapter\CPDF
;
15 use Dompdf\FrameDecorator\Block
;
16 use Dompdf\Frame\Factory
;
21 * Reflower objects are responsible for determining the width and height of
22 * individual frames. They also create line and page breaks as necessary.
26 abstract class AbstractFrameReflower
30 * Frame for this reflower
41 protected $_min_max_cache;
44 * AbstractFrameReflower constructor.
47 function __construct(Frame
$frame)
49 $this->_frame
= $frame;
50 $this->_min_max_cache
= null;
62 return $this->_frame
->get_dompdf();
66 * Collapse frames margins
67 * http://www.w3.org/TR/CSS2/box.html#collapsing-margins
69 protected function _collapse_margins()
71 $frame = $this->_frame
;
72 $cb = $frame->get_containing_block();
73 $style = $frame->get_style();
75 // Margins of float/absolutely positioned/inline-block elements do not collapse.
76 if (!$frame->is_in_flow() ||
$frame->is_inline_block()) {
80 $t = $style->length_in_pt($style->margin_top
, $cb["h"]);
81 $b = $style->length_in_pt($style->margin_bottom
, $cb["h"]);
83 // Handle 'auto' values
85 $style->margin_top
= "0pt";
90 $style->margin_bottom
= "0pt";
94 // Collapse vertical margins:
95 $n = $frame->get_next_sibling();
96 if ( $n && !$n->is_block() & !$n->is_table() ) {
97 while ($n = $n->get_next_sibling()) {
98 if ($n->is_block() ||
$n->is_table()) {
102 if (!$n->get_first_child()) {
110 $n_style = $n->get_style();
111 $n_t = (float)$n_style->length_in_pt($n_style->margin_top
, $cb["h"]);
113 $b = $this->_get_collapsed_margin_length($b, $n_t);
114 $style->margin_bottom
= $b . "pt";
115 $n_style->margin_top
= "0pt";
118 // Collapse our first child's margin, if there is no border or padding
119 if ($style->get_border_top_width() == 0 && $style->length_in_pt($style->padding_top
) == 0) {
120 $f = $this->_frame
->get_first_child();
121 if ( $f && !$f->is_block() && !$f->is_table() ) {
122 while ( $f = $f->get_next_sibling() ) {
123 if ( $f->is_block() ||
$f->is_table() ) {
127 if ( !$f->get_first_child() ) {
134 // Margin are collapsed only between block-level boxes
136 $f_style = $f->get_style();
137 $f_t = (float)$f_style->length_in_pt($f_style->margin_top
, $cb["h"]);
139 $t = $this->_get_collapsed_margin_length($t, $f_t);
140 $style->margin_top
= $t."pt";
141 $f_style->margin_top
= "0pt";
145 // Collapse our last child's margin, if there is no border or padding
146 if ($style->get_border_bottom_width() == 0 && $style->length_in_pt($style->padding_bottom
) == 0) {
147 $l = $this->_frame
->get_last_child();
148 if ( $l && !$l->is_block() && !$l->is_table() ) {
149 while ( $l = $l->get_prev_sibling() ) {
150 if ( $l->is_block() ||
$l->is_table() ) {
154 if ( !$l->get_last_child() ) {
161 // Margin are collapsed only between block-level boxes
163 $l_style = $l->get_style();
164 $l_b = (float)$l_style->length_in_pt($l_style->margin_bottom
, $cb["h"]);
166 $b = $this->_get_collapsed_margin_length($b, $l_b);
167 $style->margin_bottom
= $b."pt";
168 $l_style->margin_bottom
= "0pt";
174 * Get the combined (collapsed) length of two adjoining margins.
176 * See http://www.w3.org/TR/CSS2/box.html#collapsing-margins.
178 * @param number $length1
179 * @param number $length2
182 private function _get_collapsed_margin_length($length1, $length2)
184 if ($length1 < 0 && $length2 < 0) {
185 return min($length1, $length2); // min(x, y) = - max(abs(x), abs(y)), if x < 0 && y < 0
188 if ($length1 < 0 ||
$length2 < 0) {
189 return $length1 +
$length2; // x + y = x - abs(y), if y < 0
192 return max($length1, $length2);
196 * @param Block|null $block
199 abstract function reflow(Block
$block = null);
202 * Required for table layout: Returns an array(0 => min, 1 => max, "min"
203 * => min, "max" => max) of the minimum and maximum widths of this frame.
204 * This provides a basic implementation. Child classes should override
209 function get_min_max_width()
211 if (!is_null($this->_min_max_cache
)) {
212 return $this->_min_max_cache
;
215 $style = $this->_frame
->get_style();
217 // Account for margins & padding
218 $dims = array($style->padding_left
,
219 $style->padding_right
,
220 $style->border_left_width
,
221 $style->border_right_width
,
223 $style->margin_right
);
225 $cb_w = $this->_frame
->get_containing_block("w");
226 $delta = (float)$style->length_in_pt($dims, $cb_w);
228 // Handle degenerate case
229 if (!$this->_frame
->get_first_child()) {
230 return $this->_min_max_cache
= array(
240 for ($iter = $this->_frame
->get_children()->getIterator(); $iter->valid(); $iter->next()) {
244 // Add all adjacent inline widths together to calculate max width
245 while ($iter->valid() && in_array($iter->current()->get_style()->display
, Style
::$INLINE_TYPES)) {
246 $child = $iter->current();
248 $minmax = $child->get_min_max_width();
250 if (in_array($iter->current()->get_style()->white_space
, array("pre", "nowrap"))) {
251 $inline_min +
= $minmax["min"];
253 $low[] = $minmax["min"];
256 $inline_max +
= $minmax["max"];
260 if ($inline_max > 0) {
261 $high[] = $inline_max;
263 if ($inline_min > 0) {
264 $low[] = $inline_min;
267 if ($iter->valid()) {
268 list($low[], $high[]) = $iter->current()->get_min_max_width();
272 $min = count($low) ?
max($low) : 0;
273 $max = count($high) ?
max($high) : 0;
275 // Use specified width if it is greater than the minimum defined by the
276 // content. If the width is a percentage ignore it for now.
277 $width = $style->width
;
278 if ($width !== "auto" && !Helpers
::is_percent($width)) {
279 $width = (float)$style->length_in_pt($width, $cb_w);
290 return $this->_min_max_cache
= array($min, $max, "min" => $min, "max" => $max);
294 * Parses a CSS string containing quotes and escaped hex characters
296 * @param $string string The CSS string to parse
297 * @param $single_trim
300 protected function _parse_string($string, $single_trim = false)
303 $string = preg_replace('/^[\"\']/', "", $string);
304 $string = preg_replace('/[\"\']$/', "", $string);
306 $string = trim($string, "'\"");
309 $string = str_replace(array("\\\n", '\\"', "\\'"),
310 array("", '"', "'"), $string);
312 // Convert escaped hex characters into ascii characters (e.g. \A => newline)
313 $string = preg_replace_callback("/\\\\([0-9a-fA-F]{0,6})/",
314 function ($matches) { return \Dompdf\Helpers
::unichr(hexdec($matches[1])); },
320 * Parses a CSS "quotes" property
322 * @return array|null An array of pairs of quotes
324 protected function _parse_quotes()
326 // Matches quote types
327 $re = '/(\'[^\']*\')|(\"[^\"]*\")/';
329 $quotes = $this->_frame
->get_style()->quotes
;
331 // split on spaces, except within quotes
332 if (!preg_match_all($re, "$quotes", $matches, PREG_SET_ORDER
)) {
336 $quotes_array = array();
337 foreach ($matches as $_quote) {
338 $quotes_array[] = $this->_parse_string($_quote[0], true);
341 if (empty($quotes_array)) {
342 $quotes_array = array('"', '"');
345 return array_chunk($quotes_array, 2);
349 * Parses the CSS "content" property
351 * @return string|null The resulting string
353 protected function _parse_content()
355 // Matches generated content
357 "\s(counters?\\([^)]*\\))|\n" .
358 "\A(counters?\\([^)]*\\))|\n" .
359 "\s([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\3|\n" .
360 "\A([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\5|\n" .
365 $content = $this->_frame
->get_style()->content
;
367 $quotes = $this->_parse_quotes();
369 // split on spaces, except within quotes
370 if (!preg_match_all($re, $content, $matches, PREG_SET_ORDER
)) {
376 foreach ($matches as $match) {
377 if (isset($match[2]) && $match[2] !== "") {
378 $match[1] = $match[2];
381 if (isset($match[6]) && $match[6] !== "") {
382 $match[4] = $match[6];
385 if (isset($match[8]) && $match[8] !== "") {
386 $match[7] = $match[8];
389 if (isset($match[1]) && $match[1] !== "") {
391 $match[1] = mb_strtolower(trim($match[1]));
393 // Handle counter() references:
394 // http://www.w3.org/TR/CSS21/generate.html#content
396 $i = mb_strpos($match[1], ")");
401 preg_match('/(counters?)(^\()*?\(\s*([^\s,]+)\s*(,\s*["\']?([^"\'\)]+)["\']?\s*(,\s*([^\s)]+)\s*)?)?\)/i', $match[1], $args);
402 $counter_id = $args[3];
403 if (strtolower($args[1]) == 'counter') {
404 // counter(name [,style])
405 if (isset($args[5])) {
406 $type = trim($args[5]);
410 $p = $this->_frame
->lookup_counter_frame($counter_id);
412 $text .= $p->counter_value($counter_id, $type);
414 } else if (strtolower($args[1]) == 'counters') {
415 // counters(name, string [,style])
416 if (isset($args[5])) {
417 $string = $this->_parse_string($args[5]);
422 if (isset($args[7])) {
423 $type = trim($args[7]);
428 $p = $this->_frame
->lookup_counter_frame($counter_id);
431 // We only want to use the counter values when they actually increment the counter
432 if (array_key_exists($counter_id, $p->_counters
)) {
433 array_unshift($tmp, $p->counter_value($counter_id, $type));
435 $p = $p->lookup_counter_frame($counter_id);
437 $text .= implode($string, $tmp);
443 } else if (isset($match[4]) && $match[4] !== "") {
445 $text .= $this->_parse_string($match[4]);
446 } else if (isset($match[7]) && $match[7] !== "") {
449 if ($match[7] === "open-quote") {
450 // FIXME: do something here
451 $text .= $quotes[0][0];
452 } else if ($match[7] === "close-quote") {
453 // FIXME: do something else here
454 $text .= $quotes[0][1];
455 } else if ($match[7] === "no-open-quote") {
457 } else if ($match[7] === "no-close-quote") {
459 } else if (mb_strpos($match[7], "attr(") === 0) {
460 $i = mb_strpos($match[7], ")");
465 $attr = mb_substr($match[7], 5, $i - 5);
470 $text .= $this->_frame
->get_parent()->get_node()->getAttribute($attr);
481 * Sets the generated content of a generated frame
483 protected function _set_content()
485 $frame = $this->_frame
;
486 $style = $frame->get_style();
488 // if the element was pushed to a new page use the saved counter value, otherwise use the CSS reset value
489 if ($style->counter_reset
&& ($reset = $style->counter_reset
) !== "none") {
490 $vars = preg_split('/\s+/', trim($reset), 2);
491 $frame->reset_counter($vars[0], (isset($frame->_counters
['__' . $vars[0]]) ?
$frame->_counters
['__' . $vars[0]] : (isset($vars[1]) ?
$vars[1] : 0)));
494 if ($style->counter_increment
&& ($increment = $style->counter_increment
) !== "none") {
495 $frame->increment_counters($increment);
498 if ($style->content
&& $frame->get_node()->nodeName
=== "dompdf_generated") {
499 $content = $this->_parse_content();
500 // add generated content to the font subset
501 // FIXME: This is currently too late because the font subset has already been generated.
502 // See notes in issue #750.
503 if ($frame->get_dompdf()->getOptions()->getIsFontSubsettingEnabled() && $frame->get_dompdf()->get_canvas() instanceof CPDF
) {
504 $frame->get_dompdf()->get_canvas()->register_string_subset($style->font_family
, $content);
507 $node = $frame->get_node()->ownerDocument
->createTextNode($content);
509 $new_style = $style->get_stylesheet()->create_style();
510 $new_style->inherit($style);
512 $new_frame = new Frame($node);
513 $new_frame->set_style($new_style);
515 Factory
::decorate_frame($new_frame, $frame->get_dompdf(), $frame->get_root());
516 $frame->append_child($new_frame);
521 * Determine current frame width based on contents
525 public function calculate_auto_width()
527 return $this->_frame
->get_margin_width();