6 use Dompdf\Frame\FrameList
;
10 * @link http://dompdf.github.com/
11 * @author Benj Carson <benjcarson@digitaljunkies.ca>
12 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
16 * The main Frame class
18 * This class represents a single HTML element. This class stores
19 * positioning information as well as containing block location and
20 * dimensions. Style information for the element is stored in a {@link
21 * Style} object. Tree structure is maintained via the parent & children
32 * The DOMElement or DOMText object this frame represents
34 * @var \DOMElement|\DOMText
39 * Unique identifier for this frame. Used to reference this frame
49 public static $ID_COUNTER = 0; /*protected*/
52 * This frame's calculated style
59 * This frame's original style. Needed for cases where frames are
64 protected $_original_style;
67 * This frame's parent in the document tree.
74 * This frame's children
78 protected $_frame_list;
81 * This frame's first child. All children are handled as a
86 protected $_first_child;
89 * This frame's last child.
93 protected $_last_child;
96 * This frame's previous sibling in the document tree.
100 protected $_prev_sibling;
103 * This frame's next sibling in the document tree.
107 protected $_next_sibling;
110 * This frame's containing block (used in layout): array(x, y, w, h)
114 protected $_containing_block;
117 * Position on the page of the top-left corner of the margin box of
118 * this frame: array(x,y)
122 protected $_position;
125 * Absolute opacity of this frame
132 * This frame's decorator
134 * @var \Dompdf\FrameDecorator\AbstractFrameDecorator
136 protected $_decorator;
139 * This frame's containing line box
143 protected $_containing_line;
148 protected $_is_cache = array();
151 * Tells wether the frame was already pushed to the next page
155 public $_already_pushed = false;
160 public $_float_next_line = false;
163 * Tells wether the frame was split
172 public static $_ws_state = self
::WS_SPACE
;
177 * @param \DOMNode $node the DOMNode this frame represents
179 public function __construct(\DOMNode
$node)
181 $this->_node
= $node;
183 $this->_parent
= null;
184 $this->_first_child
= null;
185 $this->_last_child
= null;
186 $this->_prev_sibling
= $this->_next_sibling
= null;
188 $this->_style
= null;
189 $this->_original_style
= null;
191 $this->_containing_block
= array(
198 $this->_containing_block
[0] =& $this->_containing_block
["x"];
199 $this->_containing_block
[1] =& $this->_containing_block
["y"];
200 $this->_containing_block
[2] =& $this->_containing_block
["w"];
201 $this->_containing_block
[3] =& $this->_containing_block
["h"];
203 $this->_position
= array(
208 $this->_position
[0] =& $this->_position
["x"];
209 $this->_position
[1] =& $this->_position
["y"];
211 $this->_opacity
= 1.0;
212 $this->_decorator
= null;
214 $this->set_id(self
::$ID_COUNTER++
);
218 * WIP : preprocessing to remove all the unused whitespace
220 protected function ws_trim()
222 if ($this->ws_keep()) {
226 if (self
::$_ws_state === self
::WS_SPACE
) {
227 $node = $this->_node
;
229 if ($node->nodeName
=== "#text" && !empty($node->nodeValue
)) {
230 $node->nodeValue
= preg_replace("/[ \t\r\n\f]+/u", " ", trim($node->nodeValue
));
231 self
::$_ws_state = self
::WS_TEXT
;
239 protected function ws_keep()
241 $whitespace = $this->get_style()->white_space
;
243 return in_array($whitespace, array("pre", "pre-wrap", "pre-line"));
249 protected function ws_is_text()
251 $node = $this->get_node();
253 if ($node->nodeName
=== "img") {
257 if (!$this->is_in_flow()) {
261 if ($this->is_text_node()) {
262 return trim($node->nodeValue
) !== "";
269 * "Destructor": forcibly free all references held by this frame
271 * @param bool $recursive if true, call dispose on all children
273 public function dispose($recursive = false)
276 while ($child = $this->_first_child
) {
277 $child->dispose(true);
281 // Remove this frame from the tree
282 if ($this->_prev_sibling
) {
283 $this->_prev_sibling
->_next_sibling
= $this->_next_sibling
;
286 if ($this->_next_sibling
) {
287 $this->_next_sibling
->_prev_sibling
= $this->_prev_sibling
;
290 if ($this->_parent
&& $this->_parent
->_first_child
=== $this) {
291 $this->_parent
->_first_child
= $this->_next_sibling
;
294 if ($this->_parent
&& $this->_parent
->_last_child
=== $this) {
295 $this->_parent
->_last_child
= $this->_prev_sibling
;
298 if ($this->_parent
) {
299 $this->_parent
->get_node()->removeChild($this->_node
);
302 $this->_style
->dispose();
303 $this->_style
= null;
304 unset($this->_style
);
306 $this->_original_style
->dispose();
307 $this->_original_style
= null;
308 unset($this->_original_style
);
313 * Re-initialize the frame
315 public function reset()
317 $this->_position
["x"] = null;
318 $this->_position
["y"] = null;
320 $this->_containing_block
["x"] = null;
321 $this->_containing_block
["y"] = null;
322 $this->_containing_block
["w"] = null;
323 $this->_containing_block
["h"] = null;
325 $this->_style
= null;
326 unset($this->_style
);
327 $this->_style
= clone $this->_original_style
;
329 // If this represents a generated node then child nodes represent generated content.
330 // Remove the children since the content will be generated next time this frame is reflowed.
331 if ($this->_node
->nodeName
=== "dompdf_generated" && $this->_style
->content
!= "normal") {
332 foreach ($this->get_children() as $child) {
333 $this->remove_child($child);
339 * @return \DOMElement|\DOMText
341 public function get_node()
349 public function get_id()
357 public function get_style()
359 return $this->_style
;
365 public function get_original_style()
367 return $this->_original_style
;
373 public function get_parent()
375 return $this->_parent
;
379 * @return \Dompdf\FrameDecorator\AbstractFrameDecorator
381 public function get_decorator()
383 return $this->_decorator
;
389 public function get_first_child()
391 return $this->_first_child
;
397 public function get_last_child()
399 return $this->_last_child
;
405 public function get_prev_sibling()
407 return $this->_prev_sibling
;
413 public function get_next_sibling()
415 return $this->_next_sibling
;
419 * @return FrameList|Frame[]
421 public function get_children()
423 if (isset($this->_frame_list
)) {
424 return $this->_frame_list
;
427 $this->_frame_list
= new FrameList($this);
429 return $this->_frame_list
;
432 // Layout property accessors
435 * Containing block dimensions
437 * @param $i string The key of the wanted containing block's dimension (x, y, w, h)
439 * @return float[]|float
441 public function get_containing_block($i = null)
444 return $this->_containing_block
[$i];
447 return $this->_containing_block
;
453 * @param $i string The key of the wanted position value (x, y)
455 * @return array|float
457 public function get_position($i = null)
460 return $this->_position
[$i];
463 return $this->_position
;
466 //........................................................................
469 * Return the height of the margin box of the frame, in pt. Meaningless
470 * unless the height has been calculated properly.
474 public function get_margin_height()
476 $style = $this->_style
;
478 return (float)$style->length_in_pt(array(
481 $style->margin_bottom
,
482 $style->border_top_width
,
483 $style->border_bottom_width
,
485 $style->padding_bottom
486 ), $this->_containing_block
["h"]);
490 * Return the width of the margin box of the frame, in pt. Meaningless
491 * unless the width has been calculated properly.
495 public function get_margin_width()
497 $style = $this->_style
;
499 return (float)$style->length_in_pt(array(
502 $style->margin_right
,
503 $style->border_left_width
,
504 $style->border_right_width
,
505 $style->padding_left
,
506 $style->padding_right
507 ), $this->_containing_block
["w"]);
513 public function get_break_margins()
515 $style = $this->_style
;
517 return (float)$style->length_in_pt(array(
520 $style->margin_bottom
,
521 $style->border_top_width
,
522 $style->border_bottom_width
,
524 $style->padding_bottom
525 ), $this->_containing_block
["h"]);
529 * Return the content box (x,y,w,h) of the frame
533 public function get_content_box()
535 $style = $this->_style
;
536 $cb = $this->_containing_block
;
538 $x = $this->_position
["x"] +
539 (float)$style->length_in_pt(array($style->margin_left
,
540 $style->border_left_width
,
541 $style->padding_left
),
544 $y = $this->_position
["y"] +
545 (float)$style->length_in_pt(array($style->margin_top
,
546 $style->border_top_width
,
547 $style->padding_top
),
550 $w = $style->length_in_pt($style->width
, $cb["w"]);
552 $h = $style->length_in_pt($style->height
, $cb["h"]);
554 return array(0 => $x, "x" => $x,
561 * Return the padding box (x,y,w,h) of the frame
565 public function get_padding_box()
567 $style = $this->_style
;
568 $cb = $this->_containing_block
;
570 $x = $this->_position
["x"] +
571 (float)$style->length_in_pt(array($style->margin_left
,
572 $style->border_left_width
),
575 $y = $this->_position
["y"] +
576 (float)$style->length_in_pt(array($style->margin_top
,
577 $style->border_top_width
),
580 $w = $style->length_in_pt(array($style->padding_left
,
582 $style->padding_right
),
585 $h = $style->length_in_pt(array($style->padding_top
,
587 $style->padding_bottom
),
590 return array(0 => $x, "x" => $x,
597 * Return the border box of the frame
601 public function get_border_box()
603 $style = $this->_style
;
604 $cb = $this->_containing_block
;
606 $x = $this->_position
["x"] +
(float)$style->length_in_pt($style->margin_left
, $cb["w"]);
608 $y = $this->_position
["y"] +
(float)$style->length_in_pt($style->margin_top
, $cb["h"]);
610 $w = $style->length_in_pt(array($style->border_left_width
,
611 $style->padding_left
,
613 $style->padding_right
,
614 $style->border_right_width
),
617 $h = $style->length_in_pt(array($style->border_top_width
,
620 $style->padding_bottom
,
621 $style->border_bottom_width
),
624 return array(0 => $x, "x" => $x,
631 * @param null $opacity
635 public function get_opacity($opacity = null)
637 if ($opacity !== null) {
638 $this->set_opacity($opacity);
641 return $this->_opacity
;
647 public function &get_containing_line()
649 return $this->_containing_line
;
652 //........................................................................
658 public function set_id($id)
662 // We can only set attributes of DOMElement objects (nodeType == 1).
663 // Since these are the only objects that we can assign CSS rules to,
664 // this shortcoming is okay.
665 if ($this->_node
->nodeType
== XML_ELEMENT_NODE
) {
666 $this->_node
->setAttribute("frame_id", $id);
671 * @param Style $style
673 public function set_style(Style
$style)
675 if (is_null($this->_style
)) {
676 $this->_original_style
= clone $style;
679 //$style->set_frame($this);
680 $this->_style
= $style;
684 * @param \Dompdf\FrameDecorator\AbstractFrameDecorator $decorator
686 public function set_decorator(FrameDecorator\AbstractFrameDecorator
$decorator)
688 $this->_decorator
= $decorator;
697 public function set_containing_block($x = null, $y = null, $w = null, $h = null)
700 foreach ($x as $key => $val) {
705 if (is_numeric($x)) {
706 $this->_containing_block
["x"] = $x;
709 if (is_numeric($y)) {
710 $this->_containing_block
["y"] = $y;
713 if (is_numeric($w)) {
714 $this->_containing_block
["w"] = $w;
717 if (is_numeric($h)) {
718 $this->_containing_block
["h"] = $h;
726 public function set_position($x = null, $y = null)
729 list($x, $y) = array($x["x"], $x["y"]);
732 if (is_numeric($x)) {
733 $this->_position
["x"] = $x;
736 if (is_numeric($y)) {
737 $this->_position
["y"] = $y;
744 public function set_opacity($opacity)
746 $parent = $this->get_parent();
747 $base_opacity = (($parent && $parent->_opacity
!== null) ?
$parent->_opacity
: 1.0);
748 $this->_opacity
= $base_opacity * $opacity;
752 * @param LineBox $line
754 public function set_containing_line(LineBox
$line)
756 $this->_containing_line
= $line;
760 * Indicates if the margin height is auto sized
764 public function is_auto_height()
766 $style = $this->_style
;
773 $style->margin_bottom
,
774 $style->border_top_width
,
775 $style->border_bottom_width
,
777 $style->padding_bottom
,
778 $this->_containing_block
["h"]
785 * Indicates if the margin width is auto sized
789 public function is_auto_width()
791 $style = $this->_style
;
798 $style->margin_right
,
799 $style->border_left_width
,
800 $style->border_right_width
,
801 $style->padding_left
,
802 $style->padding_right
,
803 $this->_containing_block
["w"]
810 * Tells if the frame is a text node
814 public function is_text_node()
816 if (isset($this->_is_cache
["text_node"])) {
817 return $this->_is_cache
["text_node"];
820 return $this->_is_cache
["text_node"] = ($this->get_node()->nodeName
=== "#text");
826 public function is_positionned()
828 if (isset($this->_is_cache
["positionned"])) {
829 return $this->_is_cache
["positionned"];
832 $position = $this->get_style()->position
;
834 return $this->_is_cache
["positionned"] = in_array($position, Style
::$POSITIONNED_TYPES);
840 public function is_absolute()
842 if (isset($this->_is_cache
["absolute"])) {
843 return $this->_is_cache
["absolute"];
846 $position = $this->get_style()->position
;
848 return $this->_is_cache
["absolute"] = ($position === "absolute" ||
$position === "fixed");
854 public function is_block()
856 if (isset($this->_is_cache
["block"])) {
857 return $this->_is_cache
["block"];
860 return $this->_is_cache
["block"] = in_array($this->get_style()->display
, Style
::$BLOCK_TYPES);
866 public function is_inline_block()
868 if (isset($this->_is_cache
["inline_block"])) {
869 return $this->_is_cache
["inline_block"];
872 return $this->_is_cache
["inline_block"] = ($this->get_style()->display
=== 'inline-block');
878 public function is_in_flow()
880 if (isset($this->_is_cache
["in_flow"])) {
881 return $this->_is_cache
["in_flow"];
883 return $this->_is_cache
["in_flow"] = !($this->get_style()->float !== "none" ||
$this->is_absolute());
889 public function is_pre()
891 if (isset($this->_is_cache
["pre"])) {
892 return $this->_is_cache
["pre"];
895 $white_space = $this->get_style()->white_space
;
897 return $this->_is_cache
["pre"] = in_array($white_space, array("pre", "pre-wrap"));
903 public function is_table()
905 if (isset($this->_is_cache
["table"])) {
906 return $this->_is_cache
["table"];
909 $display = $this->get_style()->display
;
911 return $this->_is_cache
["table"] = in_array($display, Style
::$TABLE_TYPES);
916 * Inserts a new child at the beginning of the Frame
918 * @param $child Frame The new Frame to insert
919 * @param $update_node boolean Whether or not to update the DOM
921 public function prepend_child(Frame
$child, $update_node = true)
924 $this->_node
->insertBefore($child->_node
, $this->_first_child ?
$this->_first_child
->_node
: null);
927 // Remove the child from its parent
928 if ($child->_parent
) {
929 $child->_parent
->remove_child($child, false);
932 $child->_parent
= $this;
933 $child->_prev_sibling
= null;
935 // Handle the first child
936 if (!$this->_first_child
) {
937 $this->_first_child
= $child;
938 $this->_last_child
= $child;
939 $child->_next_sibling
= null;
941 $this->_first_child
->_prev_sibling
= $child;
942 $child->_next_sibling
= $this->_first_child
;
943 $this->_first_child
= $child;
948 * Inserts a new child at the end of the Frame
950 * @param $child Frame The new Frame to insert
951 * @param $update_node boolean Whether or not to update the DOM
953 public function append_child(Frame
$child, $update_node = true)
956 $this->_node
->appendChild($child->_node
);
959 // Remove the child from its parent
960 if ($child->_parent
) {
961 $child->_parent
->remove_child($child, false);
964 $child->_parent
= $this;
965 $decorator = $child->get_decorator();
966 // force an update to the cached parent
967 if ($decorator !== null) {
968 $decorator->get_parent(false);
970 $child->_next_sibling
= null;
972 // Handle the first child
973 if (!$this->_last_child
) {
974 $this->_first_child
= $child;
975 $this->_last_child
= $child;
976 $child->_prev_sibling
= null;
978 $this->_last_child
->_next_sibling
= $child;
979 $child->_prev_sibling
= $this->_last_child
;
980 $this->_last_child
= $child;
985 * Inserts a new child immediately before the specified frame
987 * @param $new_child Frame The new Frame to insert
988 * @param $ref Frame The Frame after the new Frame
989 * @param $update_node boolean Whether or not to update the DOM
993 public function insert_child_before(Frame
$new_child, Frame
$ref, $update_node = true)
995 if ($ref === $this->_first_child
) {
996 $this->prepend_child($new_child, $update_node);
1001 if (is_null($ref)) {
1002 $this->append_child($new_child, $update_node);
1007 if ($ref->_parent
!== $this) {
1008 throw new Exception("Reference child is not a child of this node.");
1013 $this->_node
->insertBefore($new_child->_node
, $ref->_node
);
1016 // Remove the child from its parent
1017 if ($new_child->_parent
) {
1018 $new_child->_parent
->remove_child($new_child, false);
1021 $new_child->_parent
= $this;
1022 $new_child->_next_sibling
= $ref;
1023 $new_child->_prev_sibling
= $ref->_prev_sibling
;
1025 if ($ref->_prev_sibling
) {
1026 $ref->_prev_sibling
->_next_sibling
= $new_child;
1029 $ref->_prev_sibling
= $new_child;
1033 * Inserts a new child immediately after the specified frame
1035 * @param $new_child Frame The new Frame to insert
1036 * @param $ref Frame The Frame before the new Frame
1037 * @param $update_node boolean Whether or not to update the DOM
1041 public function insert_child_after(Frame
$new_child, Frame
$ref, $update_node = true)
1043 if ($ref === $this->_last_child
) {
1044 $this->append_child($new_child, $update_node);
1049 if (is_null($ref)) {
1050 $this->prepend_child($new_child, $update_node);
1055 if ($ref->_parent
!== $this) {
1056 throw new Exception("Reference child is not a child of this node.");
1061 if ($ref->_next_sibling
) {
1062 $next_node = $ref->_next_sibling
->_node
;
1063 $this->_node
->insertBefore($new_child->_node
, $next_node);
1065 $new_child->_node
= $this->_node
->appendChild($new_child->_node
);
1069 // Remove the child from its parent
1070 if ($new_child->_parent
) {
1071 $new_child->_parent
->remove_child($new_child, false);
1074 $new_child->_parent
= $this;
1075 $new_child->_prev_sibling
= $ref;
1076 $new_child->_next_sibling
= $ref->_next_sibling
;
1078 if ($ref->_next_sibling
) {
1079 $ref->_next_sibling
->_prev_sibling
= $new_child;
1082 $ref->_next_sibling
= $new_child;
1086 * Remove a child frame
1088 * @param Frame $child
1089 * @param boolean $update_node Whether or not to remove the DOM node
1092 * @return Frame The removed child frame
1094 public function remove_child(Frame
$child, $update_node = true)
1096 if ($child->_parent
!== $this) {
1097 throw new Exception("Child not found in this frame");
1101 $this->_node
->removeChild($child->_node
);
1104 if ($child === $this->_first_child
) {
1105 $this->_first_child
= $child->_next_sibling
;
1108 if ($child === $this->_last_child
) {
1109 $this->_last_child
= $child->_prev_sibling
;
1112 if ($child->_prev_sibling
) {
1113 $child->_prev_sibling
->_next_sibling
= $child->_next_sibling
;
1116 if ($child->_next_sibling
) {
1117 $child->_next_sibling
->_prev_sibling
= $child->_prev_sibling
;
1120 $child->_next_sibling
= null;
1121 $child->_prev_sibling
= null;
1122 $child->_parent
= null;
1127 //........................................................................
1129 // Debugging function:
1133 public function __toString()
1135 // Skip empty text frames
1136 // if ( $this->is_text_node() &&
1137 // preg_replace("/\s/", "", $this->_node->data) === "" )
1141 $str = "<b>" . $this->_node
->nodeName
. ":</b><br/>";
1142 //$str .= spl_object_hash($this->_node) . "<br/>";
1143 $str .= "Id: " . $this->get_id() . "<br/>";
1144 $str .= "Class: " . get_class($this) . "<br/>";
1146 if ($this->is_text_node()) {
1147 $tmp = htmlspecialchars($this->_node
->nodeValue
);
1148 $str .= "<pre>'" . mb_substr($tmp, 0, 70) .
1149 (mb_strlen($tmp) > 70 ?
"..." : "") . "'</pre>";
1150 } elseif ($css_class = $this->_node
->getAttribute("class")) {
1151 $str .= "CSS class: '$css_class'<br/>";
1154 if ($this->_parent
) {
1155 $str .= "\nParent:" . $this->_parent
->_node
->nodeName
.
1156 " (" . spl_object_hash($this->_parent
->_node
) . ") " .
1160 if ($this->_prev_sibling
) {
1161 $str .= "Prev: " . $this->_prev_sibling
->_node
->nodeName
.
1162 " (" . spl_object_hash($this->_prev_sibling
->_node
) . ") " .
1166 if ($this->_next_sibling
) {
1167 $str .= "Next: " . $this->_next_sibling
->_node
->nodeName
.
1168 " (" . spl_object_hash($this->_next_sibling
->_node
) . ") " .
1172 $d = $this->get_decorator();
1173 while ($d && $d != $d->get_decorator()) {
1174 $str .= "Decorator: " . get_class($d) . "<br/>";
1175 $d = $d->get_decorator();
1178 $str .= "Position: " . Helpers
::pre_r($this->_position
, true);
1179 $str .= "\nContaining block: " . Helpers
::pre_r($this->_containing_block
, true);
1180 $str .= "\nMargin width: " . Helpers
::pre_r($this->get_margin_width(), true);
1181 $str .= "\nMargin height: " . Helpers
::pre_r($this->get_margin_height(), true);
1183 $str .= "\nStyle: <pre>" . $this->_style
->__toString() . "</pre>";
1185 if ($this->_decorator
instanceof FrameDecorator\Block
) {
1186 $str .= "Lines:<pre>";
1187 foreach ($this->_decorator
->get_line_boxes() as $line) {
1188 foreach ($line->get_frames() as $frame) {
1189 if ($frame instanceof FrameDecorator\Text
) {
1191 $str .= "'" . htmlspecialchars($frame->get_text()) . "'";
1193 $str .= "\nBlock: " . $frame->get_node()->nodeName
. " (" . spl_object_hash($frame->get_node()) . ")";
1198 "\ny => " . $line->y
. "\n" .
1199 "w => " . $line->w
. "\n" .
1200 "h => " . $line->h
. "\n" .
1201 "left => " . $line->left
. "\n" .
1202 "right => " . $line->right
. "\n";
1208 if (php_sapi_name() === "cli") {
1209 $str = strip_tags(str_replace(array("<br/>", "<b>", "</b>"),
1210 array("\n", "", ""),