composer package updates
[openemr.git] / vendor / dompdf / dompdf / src / FrameReflower / Table.php
blob3ff262b2de1b0d7c9cb3902410e4faae6f1a877d
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\FrameDecorator\Block as BlockFrameDecorator;
11 use Dompdf\FrameDecorator\Table as TableFrameDecorator;
13 /**
14 * Reflows tables
16 * @access private
17 * @package dompdf
19 class Table extends AbstractFrameReflower
21 /**
22 * Frame for this reflower
24 * @var TableFrameDecorator
26 protected $_frame;
28 /**
29 * Cache of results between call to get_min_max_width and assign_widths
31 * @var array
33 protected $_state;
35 /**
36 * Table constructor.
37 * @param TableFrameDecorator $frame
39 function __construct(TableFrameDecorator $frame)
41 $this->_state = null;
42 parent::__construct($frame);
45 /**
46 * State is held here so it needs to be reset along with the decorator
48 function reset()
50 $this->_state = null;
51 $this->_min_max_cache = null;
54 protected function _assign_widths()
56 $style = $this->_frame->get_style();
58 // Find the min/max width of the table and sort the columns into
59 // absolute/percent/auto arrays
60 $min_width = $this->_state["min_width"];
61 $max_width = $this->_state["max_width"];
62 $percent_used = $this->_state["percent_used"];
63 $absolute_used = $this->_state["absolute_used"];
64 $auto_min = $this->_state["auto_min"];
66 $absolute =& $this->_state["absolute"];
67 $percent =& $this->_state["percent"];
68 $auto =& $this->_state["auto"];
70 // Determine the actual width of the table
71 $cb = $this->_frame->get_containing_block();
72 $columns =& $this->_frame->get_cellmap()->get_columns();
74 $width = $style->width;
76 // Calculate padding & border fudge factor
77 $left = $style->margin_left;
78 $right = $style->margin_right;
80 $centered = ($left === "auto" && $right === "auto");
82 $left = (float)($left === "auto" ? 0 : $style->length_in_pt($left, $cb["w"]));
83 $right = (float)($right === "auto" ? 0 : $style->length_in_pt($right, $cb["w"]));
85 $delta = $left + $right;
87 if (!$centered) {
88 $delta += (float)$style->length_in_pt(array(
89 $style->padding_left,
90 $style->border_left_width,
91 $style->border_right_width,
92 $style->padding_right),
93 $cb["w"]);
96 $min_table_width = (float)$style->length_in_pt($style->min_width, $cb["w"] - $delta);
98 // min & max widths already include borders & padding
99 $min_width -= $delta;
100 $max_width -= $delta;
102 if ($width !== "auto") {
103 $preferred_width = (float)$style->length_in_pt($width, $cb["w"]) - $delta;
105 if ($preferred_width < $min_table_width) {
106 $preferred_width = $min_table_width;
109 if ($preferred_width > $min_width) {
110 $width = $preferred_width;
111 } else {
112 $width = $min_width;
115 } else {
116 if ($max_width + $delta < $cb["w"]) {
117 $width = $max_width;
118 } else if ($cb["w"] - $delta > $min_width) {
119 $width = $cb["w"] - $delta;
120 } else {
121 $width = $min_width;
124 if ($width < $min_table_width) {
125 $width = $min_table_width;
130 // Store our resolved width
131 $style->width = $width;
133 $cellmap = $this->_frame->get_cellmap();
135 if ($cellmap->is_columns_locked()) {
136 return;
139 // If the whole table fits on the page, then assign each column it's max width
140 if ($width == $max_width) {
141 foreach (array_keys($columns) as $i) {
142 $cellmap->set_column_width($i, $columns[$i]["max-width"]);
145 return;
148 // Determine leftover and assign it evenly to all columns
149 if ($width > $min_width) {
150 // We have four cases to deal with:
152 // 1. All columns are auto--no widths have been specified. In this
153 // case we distribute extra space across all columns weighted by max-width.
155 // 2. Only absolute widths have been specified. In this case we
156 // distribute any extra space equally among 'width: auto' columns, or all
157 // columns if no auto columns have been specified.
159 // 3. Only percentage widths have been specified. In this case we
160 // normalize the percentage values and distribute any remaining % to
161 // width: auto columns. We then proceed to assign widths as fractions
162 // of the table width.
164 // 4. Both absolute and percentage widths have been specified.
166 $increment = 0;
168 // Case 1:
169 if ($absolute_used == 0 && $percent_used == 0) {
170 $increment = $width - $min_width;
172 foreach (array_keys($columns) as $i) {
173 $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment * ($columns[$i]["max-width"] / $max_width));
175 return;
178 // Case 2
179 if ($absolute_used > 0 && $percent_used == 0) {
180 if (count($auto) > 0) {
181 $increment = ($width - $auto_min - $absolute_used) / count($auto);
184 // Use the absolutely specified width or the increment
185 foreach (array_keys($columns) as $i) {
186 if ($columns[$i]["absolute"] > 0 && count($auto)) {
187 $cellmap->set_column_width($i, $columns[$i]["min-width"]);
188 } else if (count($auto)) {
189 $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
190 } else {
191 // All absolute columns
192 $increment = ($width - $absolute_used) * $columns[$i]["absolute"] / $absolute_used;
194 $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
198 return;
201 // Case 3:
202 if ($absolute_used == 0 && $percent_used > 0) {
203 $scale = null;
204 $remaining = null;
206 // Scale percent values if the total percentage is > 100, or if all
207 // values are specified as percentages.
208 if ($percent_used > 100 || count($auto) == 0) {
209 $scale = 100 / $percent_used;
210 } else {
211 $scale = 1;
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"];
228 $cellmap->set_column_width($i, $w);
229 $used_width += $w;
233 // This works because $used_width includes the min-width of each
234 // unassigned column
235 if (count($auto) > 0) {
236 $increment = ($width - $used_width) / count($auto);
238 foreach ($auto as $i) {
239 $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
243 return;
246 // Case 4:
248 // First-come, first served
249 if ($absolute_used > 0 && $percent_used > 0) {
250 $used_width = $auto_min;
252 foreach ($absolute as $i) {
253 $cellmap->set_column_width($i, $columns[$i]["min-width"]);
254 $used_width += $columns[$i]["min-width"];
257 // Scale percent values if the total percentage is > 100 or there
258 // are no auto values to take up slack
259 if ($percent_used > 100 || count($auto) == 0) {
260 $scale = 100 / $percent_used;
261 } else {
262 $scale = 1;
265 $remaining_width = $width - $used_width;
267 foreach ($percent as $i) {
268 $slack = $remaining_width - $used_width;
270 $columns[$i]["percent"] *= $scale;
271 $w = min($columns[$i]["percent"] * $remaining_width / 100, $slack);
273 if ($w < $columns[$i]["min-width"]) {
274 $w = $columns[$i]["min-width"];
277 $columns[$i]["used-width"] = $w;
278 $used_width += $w;
281 if (count($auto) > 0) {
282 $increment = ($width - $used_width) / count($auto);
284 foreach ($auto as $i) {
285 $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
289 return;
291 } 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"]);
300 * Determine the frame's height based on min/max height
302 * @return float|int|mixed|string
304 protected function _calculate_height()
306 $style = $this->_frame->get_style();
307 $height = $style->height;
309 $cellmap = $this->_frame->get_cellmap();
310 $cellmap->assign_frame_heights();
311 $rows = $cellmap->get_rows();
313 // Determine our content height
314 $content_height = 0;
315 foreach ($rows as $r) {
316 $content_height += $r["height"];
319 $cb = $this->_frame->get_containing_block();
321 if (!($style->overflow === "visible" ||
322 ($style->overflow === "hidden" && $height === "auto"))
324 // Only handle min/max height if the height is independent of the frame's content
326 $min_height = $style->min_height;
327 $max_height = $style->max_height;
329 if (isset($cb["h"])) {
330 $min_height = $style->length_in_pt($min_height, $cb["h"]);
331 $max_height = $style->length_in_pt($max_height, $cb["h"]);
333 } else if (isset($cb["w"])) {
334 if (mb_strpos($min_height, "%") !== false) {
335 $min_height = 0;
336 } else {
337 $min_height = $style->length_in_pt($min_height, $cb["w"]);
339 if (mb_strpos($max_height, "%") !== false) {
340 $max_height = "none";
341 } else {
342 $max_height = $style->length_in_pt($max_height, $cb["w"]);
346 if ($max_height !== "none" && $min_height > $max_height) {
347 // Swap 'em
348 list($max_height, $min_height) = array($min_height, $max_height);
351 if ($max_height !== "none" && $height > $max_height) {
352 $height = $max_height;
355 if ($height < $min_height) {
356 $height = $min_height;
358 } else {
359 // Use the content height or the height value, whichever is greater
360 if ($height !== "auto") {
361 $height = $style->length_in_pt($height, $cb["h"]);
363 if ($height <= $content_height) {
364 $height = $content_height;
365 } else {
366 $cellmap->set_frame_heights($height, $content_height);
368 } else {
369 $height = $content_height;
373 return $height;
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()) {
390 return;
393 // Let the page know that we're reflowing a table so that splits
394 // are suppressed (simply setting page-break-inside: avoid won't
395 // work because we may have an arbitrary number of block elements
396 // inside tds.)
397 $page->table_reflow_start();
399 // Collapse vertical margins, if required
400 $this->_collapse_margins();
402 $frame->position();
404 // Table layout algorithm:
405 // http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
407 if (is_null($this->_state)) {
408 $this->get_min_max_width();
411 $cb = $frame->get_containing_block();
412 $style = $frame->get_style();
414 // This is slightly inexact, but should be okay. Add half the
415 // border-spacing to the table as padding. The other half is added to
416 // the cells themselves.
417 if ($style->border_collapse === "separate") {
418 list($h, $v) = $style->border_spacing;
420 $v = (float)$style->length_in_pt($v) / 2;
421 $h = (float)$style->length_in_pt($h) / 2;
423 $style->padding_left = (float)$style->length_in_pt($style->padding_left, $cb["w"]) + $h;
424 $style->padding_right = (float)$style->length_in_pt($style->padding_right, $cb["w"]) + $h;
425 $style->padding_top = (float)$style->length_in_pt($style->padding_top, $cb["h"]) + $v;
426 $style->padding_bottom = (float)$style->length_in_pt($style->padding_bottom, $cb["h"]) + $v;
429 $this->_assign_widths();
431 // Adjust left & right margins, if they are auto
432 $width = $style->width;
433 $left = $style->margin_left;
434 $right = $style->margin_right;
436 $diff = $cb["w"] - $width;
438 if ($left === "auto" && $right === "auto") {
439 if ($diff < 0) {
440 $left = 0;
441 $right = $diff;
442 } else {
443 $left = $right = $diff / 2;
446 $style->margin_left = sprintf("%Fpt", $left);
447 $style->margin_right = sprintf("%Fpt", $right);;
448 } else {
449 if ($left === "auto") {
450 $left = (float)$style->length_in_pt($cb["w"] - $right - $width, $cb["w"]);
452 if ($right === "auto") {
453 $left = (float)$style->length_in_pt($left, $cb["w"]);
457 list($x, $y) = $frame->get_position();
459 // Determine the content edge
460 $content_x = $x + (float)$left + (float)$style->length_in_pt(array($style->padding_left,
461 $style->border_left_width), $cb["w"]);
462 $content_y = $y + (float)$style->length_in_pt(array($style->margin_top,
463 $style->border_top_width,
464 $style->padding_top), $cb["h"]);
466 if (isset($cb["h"])) {
467 $h = $cb["h"];
468 } else {
469 $h = null;
472 $cellmap = $frame->get_cellmap();
473 $col =& $cellmap->get_column(0);
474 $col["x"] = $content_x;
476 $row =& $cellmap->get_row(0);
477 $row["y"] = $content_y;
479 $cellmap->assign_x_positions();
481 // Set the containing block of each child & reflow
482 foreach ($frame->get_children() as $child) {
483 // Bail if the page is full
484 if (!$page->in_nested_table() && $page->is_full()) {
485 break;
488 $child->set_containing_block($content_x, $content_y, $width, $h);
489 $child->reflow();
491 if (!$page->in_nested_table()) {
492 // Check if a split has occured
493 $page->check_page_break($child);
498 // Assign heights to our cells:
499 $style->height = $this->_calculate_height();
501 if ($style->border_collapse === "collapse") {
502 // Unset our borders because our cells are now using them
503 $style->border_style = "none";
506 $page->table_reflow_end();
508 // Debugging:
509 //echo ($this->_frame->get_cellmap());
511 if ($block && $style->float === "none" && $frame->is_in_flow()) {
512 $block->add_frame_to_line($frame);
513 $block->add_line();
518 * @return array|null
520 function get_min_max_width()
522 if (!is_null($this->_min_max_cache)) {
523 return $this->_min_max_cache;
526 $style = $this->_frame->get_style();
528 $this->_frame->normalise();
530 // Add the cells to the cellmap (this will calcluate column widths as
531 // frames are added)
532 $this->_frame->get_cellmap()->add_frame($this->_frame);
534 // Find the min/max width of the table and sort the columns into
535 // absolute/percent/auto arrays
536 $this->_state = array();
537 $this->_state["min_width"] = 0;
538 $this->_state["max_width"] = 0;
540 $this->_state["percent_used"] = 0;
541 $this->_state["absolute_used"] = 0;
542 $this->_state["auto_min"] = 0;
544 $this->_state["absolute"] = array();
545 $this->_state["percent"] = array();
546 $this->_state["auto"] = array();
548 $columns =& $this->_frame->get_cellmap()->get_columns();
549 foreach (array_keys($columns) as $i) {
550 $this->_state["min_width"] += $columns[$i]["min-width"];
551 $this->_state["max_width"] += $columns[$i]["max-width"];
553 if ($columns[$i]["absolute"] > 0) {
554 $this->_state["absolute"][] = $i;
555 $this->_state["absolute_used"] += $columns[$i]["absolute"];
556 } else if ($columns[$i]["percent"] > 0) {
557 $this->_state["percent"][] = $i;
558 $this->_state["percent_used"] += $columns[$i]["percent"];
559 } else {
560 $this->_state["auto"][] = $i;
561 $this->_state["auto_min"] += $columns[$i]["min-width"];
565 // Account for margins & padding
566 $dims = array($style->border_left_width,
567 $style->border_right_width,
568 $style->padding_left,
569 $style->padding_right,
570 $style->margin_left,
571 $style->margin_right);
573 if ($style->border_collapse !== "collapse") {
574 list($dims[]) = $style->border_spacing;
577 $delta = (float)$style->length_in_pt($dims, $this->_frame->get_containing_block("w"));
579 $this->_state["min_width"] += $delta;
580 $this->_state["max_width"] += $delta;
582 return $this->_min_max_cache = array(
583 $this->_state["min_width"],
584 $this->_state["max_width"],
585 "min" => $this->_state["min_width"],
586 "max" => $this->_state["max_width"],