PluginProbe
MBE eShip / trunk
MBE eShip vtrunk
2.8.1 trunk 1.0.0 1.1.0 1.1.3 1.2.1 1.2.2 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.6.0 1.7.0 1.7.1 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1.0 2.1.1 2.1.2 2.2.1 All 37 releases
mail-boxes-etc / lib / dompdf / src / FrameReflower / Table.php

Table.php in MBE eShip trunk, at lib/dompdf/src/FrameReflower/Table.php

524 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package dompdf
4 * @link https://github.com/dompdf/dompdf
5 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
6 */
7 namespace Dompdf\FrameReflower;
8
9 use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
10 use Dompdf\FrameDecorator\Table as TableFrameDecorator;
11 use Dompdf\Helpers;
12
13 /**
14 * Reflows tables
15 *
16 * @package dompdf
17 */
18 class Table extends AbstractFrameReflower
19 {
20 /**
21 * Frame for this reflower
22 *
23 * @var TableFrameDecorator
24 */
25 protected $_frame;
26
27 /**
28 * Cache of results between call to get_min_max_width and assign_widths
29 *
30 * @var array
31 */
32 protected $_state;
33
34 /**
35 * Table constructor.
36 * @param TableFrameDecorator $frame
37 */
38 function __construct(TableFrameDecorator $frame)
39 {
40 $this->_state = null;
41 parent::__construct($frame);
42 }
43
44 /**
45 * State is held here so it needs to be reset along with the decorator
46 */
47 public function reset(): void
48 {
49 parent::reset();
50 $this->_state = null;
51 }
52
53 protected function _assign_widths()
54 {
55 $style = $this->_frame->get_style();
56
57 // Find the min/max width of the table and sort the columns into
58 // absolute/percent/auto arrays
59 $delta = $this->_state["width_delta"];
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"];
65
66 $absolute =& $this->_state["absolute"];
67 $percent =& $this->_state["percent"];
68 $auto =& $this->_state["auto"];
69
70 // Determine the actual width of the table (excluding borders and
71 // padding)
72 $cb = $this->_frame->get_containing_block();
73 $columns =& $this->_frame->get_cellmap()->get_columns();
74
75 $width = $style->width;
76 $min_table_width = $this->resolve_min_width($cb["w"]) - $delta;
77
78 if ($width !== "auto") {
79 $preferred_width = (float) $style->length_in_pt($width, $cb["w"]) - $delta;
80
81 if ($preferred_width < $min_table_width) {
82 $preferred_width = $min_table_width;
83 }
84
85 if ($preferred_width > $min_width) {
86 $width = $preferred_width;
87 } else {
88 $width = $min_width;
89 }
90
91 } else {
92 if ($max_width + $delta < $cb["w"]) {
93 $width = $max_width;
94 } elseif ($cb["w"] - $delta > $min_width) {
95 $width = $cb["w"] - $delta;
96 } else {
97 $width = $min_width;
98 }
99
100 if ($width < $min_table_width) {
101 $width = $min_table_width;
102 }
103
104 }
105
106 // Store our resolved width
107 $style->set_used("width", $width);
108
109 $cellmap = $this->_frame->get_cellmap();
110
111 if ($cellmap->is_columns_locked()) {
112 return;
113 }
114
115 // If the whole table fits on the page, then assign each column it's max width
116 if ($width == $max_width) {
117 foreach ($columns as $i => $col) {
118 $cellmap->set_column_width($i, $col["max-width"]);
119 }
120
121 return;
122 }
123
124 // Determine leftover and assign it evenly to all columns
125 if ($width > $min_width) {
126 // We have three cases to deal with:
127 //
128 // 1. All columns are auto or absolute width. In this case we
129 // distribute extra space across all auto columns weighted by the
130 // difference between their max and min width, or by max width only
131 // if the width of the table is larger than the max width for all
132 // columns.
133 //
134 // 2. Only absolute widths have been specified, no auto columns. In
135 // this case we distribute extra space across all columns weighted
136 // by their absolute width.
137 //
138 // 3. Percentage widths have been specified. In this case we normalize
139 // the percentage values and try to assign widths as fractions of
140 // the table width. Absolute column widths are fully satisfied and
141 // any remaining space is evenly distributed among all auto columns.
142
143 // Case 1:
144 if ($percent_used == 0 && count($auto)) {
145 foreach ($absolute as $i) {
146 $w = $columns[$i]["min-width"];
147 $cellmap->set_column_width($i, $w);
148 }
149
150 if ($width < $max_width) {
151 $increment = $width - $min_width;
152 $table_delta = $max_width - $min_width;
153
154 foreach ($auto as $i) {
155 $min = $columns[$i]["min-width"];
156 $max = $columns[$i]["max-width"];
157 $col_delta = $max - $min;
158 $w = $min + $increment * ($col_delta / $table_delta);
159 $cellmap->set_column_width($i, $w);
160 }
161 } else {
162 $increment = $width - $max_width;
163 $auto_max = $max_width - $absolute_used;
164
165 foreach ($auto as $i) {
166 $max = $columns[$i]["max-width"];
167 $f = $auto_max > 0 ? $max / $auto_max : 1 / count($auto);
168 $w = $max + $increment * $f;
169 $cellmap->set_column_width($i, $w);
170 }
171 }
172 return;
173 }
174
175 // Case 2:
176 if ($percent_used == 0 && !count($auto)) {
177 $increment = $width - $absolute_used;
178
179 foreach ($absolute as $i) {
180 $abs = $columns[$i]["min-width"];
181 $f = $absolute_used > 0 ? $abs / $absolute_used : 1 / count($absolute);
182 $w = $abs + $increment * $f;
183 $cellmap->set_column_width($i, $w);
184 }
185 return;
186 }
187
188 // Case 3:
189 if ($percent_used > 0) {
190 // Scale percent values if the total percentage is > 100 or
191 // there are no auto values to take up slack
192 if ($percent_used > 100 || count($auto) == 0) {
193 $scale = 100 / $percent_used;
194 } else {
195 $scale = 1;
196 }
197
198 // Account for the minimum space used by the unassigned auto
199 // columns, by the columns with absolute widths, and the
200 // percentage columns following the current one
201 $used_width = $auto_min + $absolute_used;
202
203 foreach ($absolute as $i) {
204 $w = $columns[$i]["min-width"];
205 $cellmap->set_column_width($i, $w);
206 }
207
208 $percent_min = 0;
209
210 foreach ($percent as $i) {
211 $percent_min += $columns[$i]["min-width"];
212 }
213
214 // First-come, first served
215 foreach ($percent as $i) {
216 $min = $columns[$i]["min-width"];
217 $percent_min -= $min;
218 $slack = $width - $used_width - $percent_min;
219
220 $columns[$i]["percent"] *= $scale;
221 $w = min($columns[$i]["percent"] * $width / 100, $slack);
222
223 if ($w < $min) {
224 $w = $min;
225 }
226
227 $cellmap->set_column_width($i, $w);
228 $used_width += $w;
229 }
230
231 // This works because $used_width includes the min-width of each
232 // unassigned column
233 if (count($auto) > 0) {
234 $increment = ($width - $used_width) / count($auto);
235
236 foreach ($auto as $i) {
237 $w = $columns[$i]["min-width"] + $increment;
238 $cellmap->set_column_width($i, $w);
239 }
240 }
241 return;
242 }
243 } else {
244 // We are over-constrained:
245 // Each column gets its minimum width
246 foreach ($columns as $i => $col) {
247 $cellmap->set_column_width($i, $col["min-width"]);
248 }
249 }
250 }
251
252 /**
253 * Determine the frame's height based on min/max height
254 *
255 * @return float
256 */
257 protected function _calculate_height()
258 {
259 $frame = $this->_frame;
260 $style = $frame->get_style();
261 $cb = $frame->get_containing_block();
262
263 $height = $style->length_in_pt($style->height, $cb["h"]);
264
265 $cellmap = $frame->get_cellmap();
266 $cellmap->assign_frame_heights();
267 $rows = $cellmap->get_rows();
268
269 // Determine our content height
270 $content_height = 0.0;
271 foreach ($rows as $r) {
272 $content_height += $r["height"];
273 }
274
275 if ($height === "auto") {
276 $height = $content_height;
277 }
278
279 // Handle min/max height
280 // https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
281 $min_height = $this->resolve_min_height($cb["h"]);
282 $max_height = $this->resolve_max_height($cb["h"]);
283 $height = Helpers::clamp($height, $min_height, $max_height);
284
285 // Use the content height or the height value, whichever is greater
286 if ($height <= $content_height) {
287 $height = $content_height;
288 } else {
289 // FIXME: Borders and row positions are not properly updated by this
290 // $cellmap->set_frame_heights($height, $content_height);
291 }
292
293 return $height;
294 }
295
296 /**
297 * @param BlockFrameDecorator|null $block
298 */
299 function reflow(BlockFrameDecorator $block = null)
300 {
301 /** @var TableFrameDecorator */
302 $frame = $this->_frame;
303
304 // Check if a page break is forced
305 $page = $frame->get_root();
306 $page->check_forced_page_break($frame);
307
308 // Bail if the page is full
309 if ($page->is_full()) {
310 return;
311 }
312
313 // Let the page know that we're reflowing a table so that splits
314 // are suppressed (simply setting page-break-inside: avoid won't
315 // work because we may have an arbitrary number of block elements
316 // inside tds.)
317 $page->table_reflow_start();
318
319 $this->determine_absolute_containing_block();
320
321 // Counters and generated content
322 $this->_set_content();
323
324 // Collapse vertical margins, if required
325 $this->_collapse_margins();
326
327 // Table layout algorithm:
328 // http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
329
330 if (is_null($this->_state)) {
331 $this->get_min_max_width();
332 }
333
334 $cb = $frame->get_containing_block();
335 $style = $frame->get_style();
336
337 // This is slightly inexact, but should be okay. Add half the
338 // border-spacing to the table as padding. The other half is added to
339 // the cells themselves.
340 if ($style->border_collapse === "separate") {
341 [$h, $v] = $style->border_spacing;
342 $v = $v / 2;
343 $h = $h / 2;
344
345 $style->set_used("padding_left", (float)$style->length_in_pt($style->padding_left, $cb["w"]) + $h);
346 $style->set_used("padding_right", (float)$style->length_in_pt($style->padding_right, $cb["w"]) + $h);
347 $style->set_used("padding_top", (float)$style->length_in_pt($style->padding_top, $cb["w"]) + $v);
348 $style->set_used("padding_bottom", (float)$style->length_in_pt($style->padding_bottom, $cb["w"]) + $v);
349 }
350
351 $this->_assign_widths();
352
353 // Adjust left & right margins, if they are auto
354 $delta = $this->_state["width_delta"];
355 $width = $style->width;
356 $left = $style->length_in_pt($style->margin_left, $cb["w"]);
357 $right = $style->length_in_pt($style->margin_right, $cb["w"]);
358
359 $diff = (float) $cb["w"] - (float) $width - $delta;
360
361 if ($left === "auto" && $right === "auto") {
362 if ($diff < 0) {
363 $left = 0;
364 $right = $diff;
365 } else {
366 $left = $right = $diff / 2;
367 }
368 } else {
369 if ($left === "auto") {
370 $left = max($diff - $right, 0);
371 }
372 if ($right === "auto") {
373 $right = max($diff - $left, 0);
374 }
375 }
376
377 $style->set_used("margin_left", $left);
378 $style->set_used("margin_right", $right);
379
380 $frame->position();
381 [$x, $y] = $frame->get_position();
382
383 // Determine the content edge
384 $offset_x = (float)$left + (float)$style->length_in_pt([
385 $style->padding_left,
386 $style->border_left_width
387 ], $cb["w"]);
388 $offset_y = (float)$style->length_in_pt([
389 $style->margin_top,
390 $style->border_top_width,
391 $style->padding_top
392 ], $cb["w"]);
393 $content_x = $x + $offset_x;
394 $content_y = $y + $offset_y;
395
396 if (isset($cb["h"])) {
397 $h = $cb["h"];
398 } else {
399 $h = null;
400 }
401
402 $cellmap = $frame->get_cellmap();
403 $col =& $cellmap->get_column(0);
404 $col["x"] = $offset_x;
405
406 $row =& $cellmap->get_row(0);
407 $row["y"] = $offset_y;
408
409 $cellmap->assign_x_positions();
410
411 // Set the containing block of each child & reflow
412 foreach ($frame->get_children() as $child) {
413 $child->set_containing_block($content_x, $content_y, $width, $h);
414 $child->reflow();
415
416 if (!$page->in_nested_table()) {
417 // Check if a split has occurred
418 $page->check_page_break($child);
419
420 if ($page->is_full()) {
421 break;
422 }
423 }
424 }
425
426 // Stop reflow if a page break has occurred before the frame, in which
427 // case it has been reset, including its position
428 if ($page->is_full() && $frame->get_position("x") === null) {
429 $page->table_reflow_end();
430 return;
431 }
432
433 // Assign heights to our cells:
434 $style->set_used("height", $this->_calculate_height());
435
436 $page->table_reflow_end();
437
438 if ($block && $frame->is_in_flow()) {
439 $block->add_frame_to_line($frame);
440
441 if ($frame->is_block_level()) {
442 $block->add_line();
443 }
444 }
445 }
446
447 public function get_min_max_width(): array
448 {
449 if (!is_null($this->_min_max_cache)) {
450 return $this->_min_max_cache;
451 }
452
453 $style = $this->_frame->get_style();
454 $cellmap = $this->_frame->get_cellmap();
455
456 $this->_frame->normalize();
457
458 // Add the cells to the cellmap (this will calculate column widths as
459 // frames are added)
460 $cellmap->add_frame($this->_frame);
461
462 // Find the min/max width of the table and sort the columns into
463 // absolute/percent/auto arrays
464 $this->_state = [];
465 $this->_state["min_width"] = 0;
466 $this->_state["max_width"] = 0;
467
468 $this->_state["percent_used"] = 0;
469 $this->_state["absolute_used"] = 0;
470 $this->_state["auto_min"] = 0;
471
472 $this->_state["absolute"] = [];
473 $this->_state["percent"] = [];
474 $this->_state["auto"] = [];
475
476 $columns =& $cellmap->get_columns();
477 foreach ($columns as $i => $col) {
478 $this->_state["min_width"] += $col["min-width"];
479 $this->_state["max_width"] += $col["max-width"];
480
481 if ($col["absolute"] > 0) {
482 $this->_state["absolute"][] = $i;
483 $this->_state["absolute_used"] += $col["min-width"];
484 } elseif ($col["percent"] > 0) {
485 $this->_state["percent"][] = $i;
486 $this->_state["percent_used"] += $col["percent"];
487 } else {
488 $this->_state["auto"][] = $i;
489 $this->_state["auto_min"] += $col["min-width"];
490 }
491 }
492
493 // Account for margins, borders, padding, and border spacing
494 $cb_w = $this->_frame->get_containing_block("w");
495 $lm = (float) $style->length_in_pt($style->margin_left, $cb_w);
496 $rm = (float) $style->length_in_pt($style->margin_right, $cb_w);
497
498 $dims = [
499 $style->border_left_width,
500 $style->border_right_width,
501 $style->padding_left,
502 $style->padding_right
503 ];
504
505 if ($style->border_collapse !== "collapse") {
506 list($dims[]) = $style->border_spacing;
507 }
508
509 $delta = (float) $style->length_in_pt($dims, $cb_w);
510
511 $this->_state["width_delta"] = $delta;
512
513 $min_width = $this->_state["min_width"] + $delta + $lm + $rm;
514 $max_width = $this->_state["max_width"] + $delta + $lm + $rm;
515
516 return $this->_min_max_cache = [
517 $min_width,
518 $max_width,
519 "min" => $min_width,
520 "max" => $max_width
521 ];
522 }
523 }
524