composer package updates
[openemr.git] / vendor / dompdf / dompdf / src / FrameReflower / AbstractFrameReflower.php
blob48e66a74c0980dc5eb5ee08c99128448fb8c998e
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 namespace Dompdf\FrameReflower;
10 use Dompdf\Adapter\CPDF;
11 use Dompdf\Css\Style;
12 use Dompdf\Dompdf;
13 use Dompdf\Helpers;
14 use Dompdf\Frame;
15 use Dompdf\FrameDecorator\Block;
16 use Dompdf\Frame\Factory;
18 /**
19 * Base reflower class
21 * Reflower objects are responsible for determining the width and height of
22 * individual frames. They also create line and page breaks as necessary.
24 * @package dompdf
26 abstract class AbstractFrameReflower
29 /**
30 * Frame for this reflower
32 * @var Frame
34 protected $_frame;
36 /**
37 * Cached min/max size
39 * @var array
41 protected $_min_max_cache;
43 /**
44 * AbstractFrameReflower constructor.
45 * @param Frame $frame
47 function __construct(Frame $frame)
49 $this->_frame = $frame;
50 $this->_min_max_cache = null;
53 function dispose()
57 /**
58 * @return Dompdf
60 function get_dompdf()
62 return $this->_frame->get_dompdf();
65 /**
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()) {
77 return;
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
84 if ($t === "auto") {
85 $style->margin_top = "0pt";
86 $t = 0;
89 if ($b === "auto") {
90 $style->margin_bottom = "0pt";
91 $b = 0;
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()) {
99 break;
102 if (!$n->get_first_child()) {
103 $n = null;
104 break;
109 if ($n) {
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() ) {
124 break;
127 if ( !$f->get_first_child() ) {
128 $f = null;
129 break;
134 // Margin are collapsed only between block-level boxes
135 if ($f) {
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() ) {
151 break;
154 if ( !$l->get_last_child() ) {
155 $l = null;
156 break;
161 // Margin are collapsed only between block-level boxes
162 if ($l) {
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
180 * @return number
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
197 * @return mixed
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
205 * this if necessary.
207 * @return array|null
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,
222 $style->margin_left,
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(
231 $delta, $delta,
232 "min" => $delta,
233 "max" => $delta,
237 $low = array();
238 $high = array();
240 for ($iter = $this->_frame->get_children()->getIterator(); $iter->valid(); $iter->next()) {
241 $inline_min = 0;
242 $inline_max = 0;
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"];
252 } else {
253 $low[] = $minmax["min"];
256 $inline_max += $minmax["max"];
257 $iter->next();
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();
269 continue;
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);
280 if ($min < $width) {
281 $min = $width;
283 if ($max < $width) {
284 $max = $width;
288 $min += $delta;
289 $max += $delta;
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
298 * @return string
300 protected function _parse_string($string, $single_trim = false)
302 if ($single_trim) {
303 $string = preg_replace('/^[\"\']/', "", $string);
304 $string = preg_replace('/[\"\']$/', "", $string);
305 } else {
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])); },
315 $string);
316 return $string;
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)) {
333 return null;
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
356 $re = "/\n" .
357 "\s(counters?\\([^)]*\\))|\n" .
358 "\A(counters?\\([^)]*\\))|\n" .
359 "\s([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\3|\n" .
360 "\A([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\5|\n" .
361 "\s([^\s\"']+)|\n" .
362 "\A([^\s\"']+)\n" .
363 "/xi";
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)) {
371 return null;
374 $text = "";
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] !== "") {
390 // counters?(...)
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], ")");
397 if ($i === false) {
398 continue;
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]);
407 } else {
408 $type = null;
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]);
418 } else {
419 $string = "";
422 if (isset($args[7])) {
423 $type = trim($args[7]);
424 } else {
425 $type = null;
428 $p = $this->_frame->lookup_counter_frame($counter_id);
429 $tmp = array();
430 while ($p) {
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);
438 } else {
439 // countertops?
440 continue;
443 } else if (isset($match[4]) && $match[4] !== "") {
444 // String match
445 $text .= $this->_parse_string($match[4]);
446 } else if (isset($match[7]) && $match[7] !== "") {
447 // Directive match
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") {
456 // FIXME:
457 } else if ($match[7] === "no-close-quote") {
458 // FIXME:
459 } else if (mb_strpos($match[7], "attr(") === 0) {
460 $i = mb_strpos($match[7], ")");
461 if ($i === false) {
462 continue;
465 $attr = mb_substr($match[7], 5, $i - 5);
466 if ($attr == "") {
467 continue;
470 $text .= $this->_frame->get_parent()->get_node()->getAttribute($attr);
471 } else {
472 continue;
477 return $text;
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
523 * @return float
525 public function calculate_auto_width()
527 return $this->_frame->get_margin_width();