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\FrameDecorator\Block
as BlockFrameDecorator
;
11 use Dompdf\FrameDecorator\Table
as TableFrameDecorator
;
19 class Table
extends AbstractFrameReflower
22 * Frame for this reflower
24 * @var TableFrameDecorator
29 * Cache of results between call to get_min_max_width and assign_widths
35 function __construct(TableFrameDecorator
$frame)
38 parent
::__construct($frame);
42 * State is held here so it needs to be reset along with the decorator
47 $this->_min_max_cache
= null;
50 //........................................................................
52 protected function _assign_widths()
54 $style = $this->_frame
->get_style();
56 // Find the min/max width of the table and sort the columns into
57 // absolute/percent/auto arrays
58 $min_width = $this->_state
["min_width"];
59 $max_width = $this->_state
["max_width"];
60 $percent_used = $this->_state
["percent_used"];
61 $absolute_used = $this->_state
["absolute_used"];
62 $auto_min = $this->_state
["auto_min"];
64 $absolute =& $this->_state
["absolute"];
65 $percent =& $this->_state
["percent"];
66 $auto =& $this->_state
["auto"];
68 // Determine the actual width of the table
69 $cb = $this->_frame
->get_containing_block();
70 $columns =& $this->_frame
->get_cellmap()->get_columns();
72 $width = $style->width
;
74 // Calculate padding & border fudge factor
75 $left = $style->margin_left
;
76 $right = $style->margin_right
;
78 $centered = ($left === "auto" && $right === "auto");
80 $left = $left === "auto" ?
0 : $style->length_in_pt($left, $cb["w"]);
81 $right = $right === "auto" ?
0 : $style->length_in_pt($right, $cb["w"]);
83 $delta = $left +
$right;
86 $delta +
= $style->length_in_pt(array(
88 $style->border_left_width
,
89 $style->border_right_width
,
90 $style->padding_right
),
94 $min_table_width = $style->length_in_pt($style->min_width
, $cb["w"] - $delta);
96 // min & max widths already include borders & padding
100 if ($width !== "auto") {
102 $preferred_width = $style->length_in_pt($width, $cb["w"]) - $delta;
104 if ($preferred_width < $min_table_width)
105 $preferred_width = $min_table_width;
107 if ($preferred_width > $min_width)
108 $width = $preferred_width;
114 if ($max_width +
$delta < $cb["w"])
116 else if ($cb["w"] - $delta > $min_width)
117 $width = $cb["w"] - $delta;
121 if ($width < $min_table_width)
122 $width = $min_table_width;
126 // Store our resolved width
127 $style->width
= $width;
129 $cellmap = $this->_frame
->get_cellmap();
131 if ($cellmap->is_columns_locked()) {
135 // If the whole table fits on the page, then assign each column it's max width
136 if ($width == $max_width) {
138 foreach (array_keys($columns) as $i)
139 $cellmap->set_column_width($i, $columns[$i]["max-width"]);
144 // Determine leftover and assign it evenly to all columns
145 if ($width > $min_width) {
147 // We have four cases to deal with:
149 // 1. All columns are auto--no widths have been specified. In this
150 // case we distribute extra space across all columns weighted by max-width.
152 // 2. Only absolute widths have been specified. In this case we
153 // distribute any extra space equally among 'width: auto' columns, or all
154 // columns if no auto columns have been specified.
156 // 3. Only percentage widths have been specified. In this case we
157 // normalize the percentage values and distribute any remaining % to
158 // width: auto columns. We then proceed to assign widths as fractions
159 // of the table width.
161 // 4. Both absolute and percentage widths have been specified.
166 if ($absolute_used == 0 && $percent_used == 0) {
167 $increment = $width - $min_width;
169 foreach (array_keys($columns) as $i) {
170 $cellmap->set_column_width($i, $columns[$i]["min-width"] +
$increment * ($columns[$i]["max-width"] / $max_width));
177 if ($absolute_used > 0 && $percent_used == 0) {
179 if (count($auto) > 0)
180 $increment = ($width - $auto_min - $absolute_used) / count($auto);
182 // Use the absolutely specified width or the increment
183 foreach (array_keys($columns) as $i) {
185 if ($columns[$i]["absolute"] > 0 && count($auto))
186 $cellmap->set_column_width($i, $columns[$i]["min-width"]);
187 else if (count($auto))
188 $cellmap->set_column_width($i, $columns[$i]["min-width"] +
$increment);
190 // All absolute columns
191 $increment = ($width - $absolute_used) * $columns[$i]["absolute"] / $absolute_used;
193 $cellmap->set_column_width($i, $columns[$i]["min-width"] +
$increment);
202 if ($absolute_used == 0 && $percent_used > 0) {
207 // Scale percent values if the total percentage is > 100, or if all
208 // values are specified as percentages.
209 if ($percent_used > 100 ||
count($auto) == 0)
210 $scale = 100 / $percent_used;
214 // Account for the minimum space used by the unassigned auto columns
215 $used_width = $auto_min;
217 foreach ($percent as $i) {
218 $columns[$i]["percent"] *= $scale;
220 $slack = $width - $used_width;
222 $w = min($columns[$i]["percent"] * $width / 100, $slack);
224 if ($w < $columns[$i]["min-width"])
225 $w = $columns[$i]["min-width"];
227 $cellmap->set_column_width($i, $w);
232 // This works because $used_width includes the min-width of each
234 if (count($auto) > 0) {
235 $increment = ($width - $used_width) / count($auto);
237 foreach ($auto as $i)
238 $cellmap->set_column_width($i, $columns[$i]["min-width"] +
$increment);
246 // First-come, first served
247 if ($absolute_used > 0 && $percent_used > 0) {
249 $used_width = $auto_min;
251 foreach ($absolute as $i) {
252 $cellmap->set_column_width($i, $columns[$i]["min-width"]);
253 $used_width +
= $columns[$i]["min-width"];
256 // Scale percent values if the total percentage is > 100 or there
257 // are no auto values to take up slack
258 if ($percent_used > 100 ||
count($auto) == 0)
259 $scale = 100 / $percent_used;
263 $remaining_width = $width - $used_width;
265 foreach ($percent as $i) {
266 $slack = $remaining_width - $used_width;
268 $columns[$i]["percent"] *= $scale;
269 $w = min($columns[$i]["percent"] * $remaining_width / 100, $slack);
271 if ($w < $columns[$i]["min-width"])
272 $w = $columns[$i]["min-width"];
274 $columns[$i]["used-width"] = $w;
278 if (count($auto) > 0) {
279 $increment = ($width - $used_width) / count($auto);
281 foreach ($auto as $i)
282 $cellmap->set_column_width($i, $columns[$i]["min-width"] +
$increment);
290 } else { // we are over constrained
292 // Each column gets its minimum width
293 foreach (array_keys($columns) as $i)
294 $cellmap->set_column_width($i, $columns[$i]["min-width"]);
299 //........................................................................
301 // Determine the frame's height based on min/max height
302 protected function _calculate_height()
305 $style = $this->_frame
->get_style();
306 $height = $style->height
;
308 $cellmap = $this->_frame
->get_cellmap();
309 $cellmap->assign_frame_heights();
310 $rows = $cellmap->get_rows();
312 // Determine our content height
314 foreach ($rows as $r)
315 $content_height +
= $r["height"];
317 $cb = $this->_frame
->get_containing_block();
319 if (!($style->overflow
=== "visible" ||
320 ($style->overflow
=== "hidden" && $height === "auto"))
323 // Only handle min/max height if the height is independent of the frame's content
325 $min_height = $style->min_height
;
326 $max_height = $style->max_height
;
328 if (isset($cb["h"])) {
329 $min_height = $style->length_in_pt($min_height, $cb["h"]);
330 $max_height = $style->length_in_pt($max_height, $cb["h"]);
332 } else if (isset($cb["w"])) {
334 if (mb_strpos($min_height, "%") !== false)
337 $min_height = $style->length_in_pt($min_height, $cb["w"]);
339 if (mb_strpos($max_height, "%") !== false)
340 $max_height = "none";
342 $max_height = $style->length_in_pt($max_height, $cb["w"]);
345 if ($max_height !== "none" && $min_height > $max_height)
347 list($max_height, $min_height) = array($min_height, $max_height);
349 if ($max_height !== "none" && $height > $max_height)
350 $height = $max_height;
352 if ($height < $min_height)
353 $height = $min_height;
357 // Use the content height or the height value, whichever is greater
358 if ($height !== "auto") {
359 $height = $style->length_in_pt($height, $cb["h"]);
361 if ($height <= $content_height)
362 $height = $content_height;
364 $cellmap->set_frame_heights($height, $content_height);
367 $height = $content_height;
374 //........................................................................
377 * @param BlockFrameDecorator $block
379 function reflow(BlockFrameDecorator
$block = null)
381 /** @var TableFrameDecorator */
382 $frame = $this->_frame
;
384 // Check if a page break is forced
385 $page = $frame->get_root();
386 $page->check_forced_page_break($frame);
388 // Bail if the page is full
389 if ($page->is_full())
392 // Let the page know that we're reflowing a table so that splits
393 // are suppressed (simply setting page-break-inside: avoid won't
394 // work because we may have an arbitrary number of block elements
396 $page->table_reflow_start();
398 // Collapse vertical margins, if required
399 $this->_collapse_margins();
403 // Table layout algorithm:
404 // http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
406 if (is_null($this->_state
))
407 $this->get_min_max_width();
409 $cb = $frame->get_containing_block();
410 $style = $frame->get_style();
412 // This is slightly inexact, but should be okay. Add half the
413 // border-spacing to the table as padding. The other half is added to
414 // the cells themselves.
415 if ($style->border_collapse
=== "separate") {
416 list($h, $v) = $style->border_spacing
;
418 $v = $style->length_in_pt($v) / 2;
419 $h = $style->length_in_pt($h) / 2;
421 $style->padding_left
= $style->length_in_pt($style->padding_left
, $cb["w"]) +
$h;
422 $style->padding_right
= $style->length_in_pt($style->padding_right
, $cb["w"]) +
$h;
423 $style->padding_top
= $style->length_in_pt($style->padding_top
, $cb["h"]) +
$v;
424 $style->padding_bottom
= $style->length_in_pt($style->padding_bottom
, $cb["h"]) +
$v;
428 $this->_assign_widths();
430 // Adjust left & right margins, if they are auto
431 $width = $style->width
;
432 $left = $style->margin_left
;
433 $right = $style->margin_right
;
435 $diff = $cb["w"] - $width;
437 if ($left === "auto" && $right === "auto") {
442 $left = $right = $diff / 2;
445 $style->margin_left
= "$left pt";
446 $style->margin_right
= "$right pt";
449 if ($left === "auto") {
450 $left = $style->length_in_pt($cb["w"] - $right - $width, $cb["w"]);
452 if ($right === "auto") {
453 $left = $style->length_in_pt($left, $cb["w"]);
457 list($x, $y) = $frame->get_position();
459 // Determine the content edge
460 $content_x = $x +
$left +
$style->length_in_pt(array($style->padding_left
,
461 $style->border_left_width
), $cb["w"]);
462 $content_y = $y +
$style->length_in_pt(array($style->margin_top
,
463 $style->border_top_width
,
464 $style->padding_top
), $cb["h"]);
471 $cellmap = $frame->get_cellmap();
472 $col =& $cellmap->get_column(0);
473 $col["x"] = $content_x;
475 $row =& $cellmap->get_row(0);
476 $row["y"] = $content_y;
478 $cellmap->assign_x_positions();
480 // Set the containing block of each child & reflow
481 foreach ($frame->get_children() as $child) {
483 // Bail if the page is full
484 if (!$page->in_nested_table() && $page->is_full())
487 $child->set_containing_block($content_x, $content_y, $width, $h);
490 if (!$page->in_nested_table())
491 // Check if a split has occured
492 $page->check_page_break($child);
496 // Assign heights to our cells:
497 $style->height
= $this->_calculate_height();
499 if ($style->border_collapse
=== "collapse") {
500 // Unset our borders because our cells are now using them
501 $style->border_style
= "none";
504 $page->table_reflow_end();
507 //echo ($this->_frame->get_cellmap());
509 if ($block && $style->float === "none" && $frame->is_in_flow()) {
510 $block->add_frame_to_line($frame);
515 //........................................................................
517 function get_min_max_width()
520 if (!is_null($this->_min_max_cache
))
521 return $this->_min_max_cache
;
523 $style = $this->_frame
->get_style();
525 $this->_frame
->normalise();
527 // Add the cells to the cellmap (this will calcluate column widths as
529 $this->_frame
->get_cellmap()->add_frame($this->_frame
);
531 // Find the min/max width of the table and sort the columns into
532 // absolute/percent/auto arrays
533 $this->_state
= array();
534 $this->_state
["min_width"] = 0;
535 $this->_state
["max_width"] = 0;
537 $this->_state
["percent_used"] = 0;
538 $this->_state
["absolute_used"] = 0;
539 $this->_state
["auto_min"] = 0;
541 $this->_state
["absolute"] = array();
542 $this->_state
["percent"] = array();
543 $this->_state
["auto"] = array();
545 $columns =& $this->_frame
->get_cellmap()->get_columns();
546 foreach (array_keys($columns) as $i) {
547 $this->_state
["min_width"] +
= $columns[$i]["min-width"];
548 $this->_state
["max_width"] +
= $columns[$i]["max-width"];
550 if ($columns[$i]["absolute"] > 0) {
551 $this->_state
["absolute"][] = $i;
552 $this->_state
["absolute_used"] +
= $columns[$i]["absolute"];
554 } else if ($columns[$i]["percent"] > 0) {
555 $this->_state
["percent"][] = $i;
556 $this->_state
["percent_used"] +
= $columns[$i]["percent"];
559 $this->_state
["auto"][] = $i;
560 $this->_state
["auto_min"] +
= $columns[$i]["min-width"];
564 // Account for margins & padding
565 $dims = array($style->border_left_width
,
566 $style->border_right_width
,
567 $style->padding_left
,
568 $style->padding_right
,
570 $style->margin_right
);
572 if ($style->border_collapse
!== "collapse")
573 list($dims[]) = $style->border_spacing
;
575 $delta = $style->length_in_pt($dims, $this->_frame
->get_containing_block("w"));
577 $this->_state
["min_width"] +
= $delta;
578 $this->_state
["max_width"] +
= $delta;
580 return $this->_min_max_cache
= array(
581 $this->_state
["min_width"],
582 $this->_state
["max_width"],
583 "min" => $this->_state
["min_width"],
584 "max" => $this->_state
["max_width"],