table_frame_reflower.cls.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  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. /**
  9. * Reflows tables
  10. *
  11. * @access private
  12. * @package dompdf
  13. */
  14. class Table_Frame_Reflower extends Frame_Reflower {
  15. /**
  16. * Frame for this reflower
  17. *
  18. * @var Table_Frame_Decorator
  19. */
  20. protected $_frame;
  21. /**
  22. * Cache of results between call to get_min_max_width and assign_widths
  23. *
  24. * @var array
  25. */
  26. protected $_state;
  27. function __construct(Table_Frame_Decorator $frame) {
  28. $this->_state = null;
  29. parent::__construct($frame);
  30. }
  31. /**
  32. * State is held here so it needs to be reset along with the decorator
  33. */
  34. function reset() {
  35. $this->_state = null;
  36. $this->_min_max_cache = null;
  37. }
  38. //........................................................................
  39. protected function _assign_widths() {
  40. $style = $this->_frame->get_style();
  41. // Find the min/max width of the table and sort the columns into
  42. // absolute/percent/auto arrays
  43. $min_width = $this->_state["min_width"];
  44. $max_width = $this->_state["max_width"];
  45. $percent_used = $this->_state["percent_used"];
  46. $absolute_used = $this->_state["absolute_used"];
  47. $auto_min = $this->_state["auto_min"];
  48. $absolute =& $this->_state["absolute"];
  49. $percent =& $this->_state["percent"];
  50. $auto =& $this->_state["auto"];
  51. // Determine the actual width of the table
  52. $cb = $this->_frame->get_containing_block();
  53. $columns =& $this->_frame->get_cellmap()->get_columns();
  54. $width = $style->width;
  55. // Calculate padding & border fudge factor
  56. $left = $style->margin_left;
  57. $right = $style->margin_right;
  58. $centered = ( $left === "auto" && $right === "auto" );
  59. $left = $left === "auto" ? 0 : $style->length_in_pt($left, $cb["w"]);
  60. $right = $right === "auto" ? 0 : $style->length_in_pt($right, $cb["w"]);
  61. $delta = $left + $right;
  62. if ( !$centered ) {
  63. $delta += $style->length_in_pt(array(
  64. $style->padding_left,
  65. $style->border_left_width,
  66. $style->border_right_width,
  67. $style->padding_right),
  68. $cb["w"]);
  69. }
  70. $min_table_width = $style->length_in_pt( $style->min_width, $cb["w"] - $delta );
  71. // min & max widths already include borders & padding
  72. $min_width -= $delta;
  73. $max_width -= $delta;
  74. if ( $width !== "auto" ) {
  75. $preferred_width = $style->length_in_pt($width, $cb["w"]) - $delta;
  76. if ( $preferred_width < $min_table_width )
  77. $preferred_width = $min_table_width;
  78. if ( $preferred_width > $min_width )
  79. $width = $preferred_width;
  80. else
  81. $width = $min_width;
  82. } else {
  83. if ( $max_width + $delta < $cb["w"] )
  84. $width = $max_width;
  85. else if ( $cb["w"] - $delta > $min_width )
  86. $width = $cb["w"] - $delta;
  87. else
  88. $width = $min_width;
  89. if ( $width < $min_table_width )
  90. $width = $min_table_width;
  91. }
  92. // Store our resolved width
  93. $style->width = $width;
  94. $cellmap = $this->_frame->get_cellmap();
  95. if ( $cellmap->is_columns_locked() ) {
  96. return;
  97. }
  98. // If the whole table fits on the page, then assign each column it's max width
  99. if ( $width == $max_width ) {
  100. foreach (array_keys($columns) as $i)
  101. $cellmap->set_column_width($i, $columns[$i]["max-width"]);
  102. return;
  103. }
  104. // Determine leftover and assign it evenly to all columns
  105. if ( $width > $min_width ) {
  106. // We have four cases to deal with:
  107. //
  108. // 1. All columns are auto--no widths have been specified. In this
  109. // case we distribute extra space across all columns weighted by max-width.
  110. //
  111. // 2. Only absolute widths have been specified. In this case we
  112. // distribute any extra space equally among 'width: auto' columns, or all
  113. // columns if no auto columns have been specified.
  114. //
  115. // 3. Only percentage widths have been specified. In this case we
  116. // normalize the percentage values and distribute any remaining % to
  117. // width: auto columns. We then proceed to assign widths as fractions
  118. // of the table width.
  119. //
  120. // 4. Both absolute and percentage widths have been specified.
  121. $increment = 0;
  122. // Case 1:
  123. if ( $absolute_used == 0 && $percent_used == 0 ) {
  124. $increment = $width - $min_width;
  125. foreach (array_keys($columns) as $i) {
  126. $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment * ($columns[$i]["max-width"] / $max_width));
  127. }
  128. return;
  129. }
  130. // Case 2
  131. if ( $absolute_used > 0 && $percent_used == 0 ) {
  132. if ( count($auto) > 0 )
  133. $increment = ($width - $auto_min - $absolute_used) / count($auto);
  134. // Use the absolutely specified width or the increment
  135. foreach (array_keys($columns) as $i) {
  136. if ( $columns[$i]["absolute"] > 0 && count($auto) )
  137. $cellmap->set_column_width($i, $columns[$i]["min-width"]);
  138. else if ( count($auto) )
  139. $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
  140. else {
  141. // All absolute columns
  142. $increment = ($width - $absolute_used) * $columns[$i]["absolute"] / $absolute_used;
  143. $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
  144. }
  145. }
  146. return;
  147. }
  148. // Case 3:
  149. if ( $absolute_used == 0 && $percent_used > 0 ) {
  150. $scale = null;
  151. $remaining = null;
  152. // Scale percent values if the total percentage is > 100, or if all
  153. // values are specified as percentages.
  154. if ( $percent_used > 100 || count($auto) == 0)
  155. $scale = 100 / $percent_used;
  156. else
  157. $scale = 1;
  158. // Account for the minimum space used by the unassigned auto columns
  159. $used_width = $auto_min;
  160. foreach ($percent as $i) {
  161. $columns[$i]["percent"] *= $scale;
  162. $slack = $width - $used_width;
  163. $w = min($columns[$i]["percent"] * $width/100, $slack);
  164. if ( $w < $columns[$i]["min-width"] )
  165. $w = $columns[$i]["min-width"];
  166. $cellmap->set_column_width($i, $w);
  167. $used_width += $w;
  168. }
  169. // This works because $used_width includes the min-width of each
  170. // unassigned column
  171. if ( count($auto) > 0 ) {
  172. $increment = ($width - $used_width) / count($auto);
  173. foreach ($auto as $i)
  174. $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
  175. }
  176. return;
  177. }
  178. // Case 4:
  179. // First-come, first served
  180. if ( $absolute_used > 0 && $percent_used > 0 ) {
  181. $used_width = $auto_min;
  182. foreach ($absolute as $i) {
  183. $cellmap->set_column_width($i, $columns[$i]["min-width"]);
  184. $used_width += $columns[$i]["min-width"];
  185. }
  186. // Scale percent values if the total percentage is > 100 or there
  187. // are no auto values to take up slack
  188. if ( $percent_used > 100 || count($auto) == 0 )
  189. $scale = 100 / $percent_used;
  190. else
  191. $scale = 1;
  192. $remaining_width = $width - $used_width;
  193. foreach ($percent as $i) {
  194. $slack = $remaining_width - $used_width;
  195. $columns[$i]["percent"] *= $scale;
  196. $w = min($columns[$i]["percent"] * $remaining_width / 100, $slack);
  197. if ( $w < $columns[$i]["min-width"] )
  198. $w = $columns[$i]["min-width"];
  199. $columns[$i]["used-width"] = $w;
  200. $used_width += $w;
  201. }
  202. if ( count($auto) > 0 ) {
  203. $increment = ($width - $used_width) / count($auto);
  204. foreach ($auto as $i)
  205. $cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
  206. }
  207. return;
  208. }
  209. } else { // we are over constrained
  210. // Each column gets its minimum width
  211. foreach (array_keys($columns) as $i)
  212. $cellmap->set_column_width($i, $columns[$i]["min-width"]);
  213. }
  214. }
  215. //........................................................................
  216. // Determine the frame's height based on min/max height
  217. protected function _calculate_height() {
  218. $style = $this->_frame->get_style();
  219. $height = $style->height;
  220. $cellmap = $this->_frame->get_cellmap();
  221. $cellmap->assign_frame_heights();
  222. $rows = $cellmap->get_rows();
  223. // Determine our content height
  224. $content_height = 0;
  225. foreach ( $rows as $r )
  226. $content_height += $r["height"];
  227. $cb = $this->_frame->get_containing_block();
  228. if ( !($style->overflow === "visible" ||
  229. ($style->overflow === "hidden" && $height === "auto")) ) {
  230. // Only handle min/max height if the height is independent of the frame's content
  231. $min_height = $style->min_height;
  232. $max_height = $style->max_height;
  233. if ( isset($cb["h"]) ) {
  234. $min_height = $style->length_in_pt($min_height, $cb["h"]);
  235. $max_height = $style->length_in_pt($max_height, $cb["h"]);
  236. } else if ( isset($cb["w"]) ) {
  237. if ( mb_strpos($min_height, "%") !== false )
  238. $min_height = 0;
  239. else
  240. $min_height = $style->length_in_pt($min_height, $cb["w"]);
  241. if ( mb_strpos($max_height, "%") !== false )
  242. $max_height = "none";
  243. else
  244. $max_height = $style->length_in_pt($max_height, $cb["w"]);
  245. }
  246. if ( $max_height !== "none" && $min_height > $max_height )
  247. // Swap 'em
  248. list($max_height, $min_height) = array($min_height, $max_height);
  249. if ( $max_height !== "none" && $height > $max_height )
  250. $height = $max_height;
  251. if ( $height < $min_height )
  252. $height = $min_height;
  253. } else {
  254. // Use the content height or the height value, whichever is greater
  255. if ( $height !== "auto" ) {
  256. $height = $style->length_in_pt($height, $cb["h"]);
  257. if ( $height <= $content_height )
  258. $height = $content_height;
  259. else
  260. $cellmap->set_frame_heights($height,$content_height);
  261. } else
  262. $height = $content_height;
  263. }
  264. return $height;
  265. }
  266. //........................................................................
  267. /**
  268. * @param Block_Frame_Decorator $block
  269. */
  270. function reflow(Block_Frame_Decorator $block = null) {
  271. /**
  272. * @var Table_Frame_Decorator
  273. */
  274. $frame = $this->_frame;
  275. // Check if a page break is forced
  276. $page = $frame->get_root();
  277. $page->check_forced_page_break($frame);
  278. // Bail if the page is full
  279. if ( $page->is_full() )
  280. return;
  281. // Let the page know that we're reflowing a table so that splits
  282. // are suppressed (simply setting page-break-inside: avoid won't
  283. // work because we may have an arbitrary number of block elements
  284. // inside tds.)
  285. $page->table_reflow_start();
  286. // Collapse vertical margins, if required
  287. $this->_collapse_margins();
  288. $frame->position();
  289. // Table layout algorithm:
  290. // http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
  291. if ( is_null($this->_state) )
  292. $this->get_min_max_width();
  293. $cb = $frame->get_containing_block();
  294. $style = $frame->get_style();
  295. // This is slightly inexact, but should be okay. Add half the
  296. // border-spacing to the table as padding. The other half is added to
  297. // the cells themselves.
  298. if ( $style->border_collapse === "separate" ) {
  299. list($h, $v) = $style->border_spacing;
  300. $v = $style->length_in_pt($v) / 2;
  301. $h = $style->length_in_pt($h) / 2;
  302. $style->padding_left = $style->length_in_pt($style->padding_left, $cb["w"]) + $h;
  303. $style->padding_right = $style->length_in_pt($style->padding_right, $cb["w"]) + $h;
  304. $style->padding_top = $style->length_in_pt($style->padding_top, $cb["h"]) + $v;
  305. $style->padding_bottom = $style->length_in_pt($style->padding_bottom, $cb["h"]) + $v;
  306. }
  307. $this->_assign_widths();
  308. // Adjust left & right margins, if they are auto
  309. $width = $style->width;
  310. $left = $style->margin_left;
  311. $right = $style->margin_right;
  312. $diff = $cb["w"] - $width;
  313. if ( $left === "auto" && $right === "auto" ) {
  314. if ( $diff < 0 ) {
  315. $left = 0;
  316. $right = $diff;
  317. }
  318. else {
  319. $left = $right = $diff / 2;
  320. }
  321. $style->margin_left = "$left pt";
  322. $style->margin_right = "$right pt";
  323. } else {
  324. if ( $left === "auto" ) {
  325. $left = $style->length_in_pt($cb["w"] - $right - $width, $cb["w"]);
  326. }
  327. if ( $right === "auto" ) {
  328. $left = $style->length_in_pt($left, $cb["w"]);
  329. }
  330. }
  331. list($x, $y) = $frame->get_position();
  332. // Determine the content edge
  333. $content_x = $x + $left + $style->length_in_pt(array($style->padding_left,
  334. $style->border_left_width), $cb["w"]);
  335. $content_y = $y + $style->length_in_pt(array($style->margin_top,
  336. $style->border_top_width,
  337. $style->padding_top), $cb["h"]);
  338. if ( isset($cb["h"]) )
  339. $h = $cb["h"];
  340. else
  341. $h = null;
  342. $cellmap = $frame->get_cellmap();
  343. $col =& $cellmap->get_column(0);
  344. $col["x"] = $content_x;
  345. $row =& $cellmap->get_row(0);
  346. $row["y"] = $content_y;
  347. $cellmap->assign_x_positions();
  348. // Set the containing block of each child & reflow
  349. foreach ( $frame->get_children() as $child ) {
  350. // Bail if the page is full
  351. if ( !$page->in_nested_table() && $page->is_full() )
  352. break;
  353. $child->set_containing_block($content_x, $content_y, $width, $h);
  354. $child->reflow();
  355. if ( !$page->in_nested_table() )
  356. // Check if a split has occured
  357. $page->check_page_break($child);
  358. }
  359. // Assign heights to our cells:
  360. $style->height = $this->_calculate_height();
  361. if ( $style->border_collapse === "collapse" ) {
  362. // Unset our borders because our cells are now using them
  363. $style->border_style = "none";
  364. }
  365. $page->table_reflow_end();
  366. // Debugging:
  367. //echo ($this->_frame->get_cellmap());
  368. if ( $block && $style->float === "none" && $frame->is_in_flow() ) {
  369. $block->add_frame_to_line($frame);
  370. $block->add_line();
  371. }
  372. }
  373. //........................................................................
  374. function get_min_max_width() {
  375. if ( !is_null($this->_min_max_cache) )
  376. return $this->_min_max_cache;
  377. $style = $this->_frame->get_style();
  378. $this->_frame->normalise();
  379. // Add the cells to the cellmap (this will calcluate column widths as
  380. // frames are added)
  381. $this->_frame->get_cellmap()->add_frame($this->_frame);
  382. // Find the min/max width of the table and sort the columns into
  383. // absolute/percent/auto arrays
  384. $this->_state = array();
  385. $this->_state["min_width"] = 0;
  386. $this->_state["max_width"] = 0;
  387. $this->_state["percent_used"] = 0;
  388. $this->_state["absolute_used"] = 0;
  389. $this->_state["auto_min"] = 0;
  390. $this->_state["absolute"] = array();
  391. $this->_state["percent"] = array();
  392. $this->_state["auto"] = array();
  393. $columns =& $this->_frame->get_cellmap()->get_columns();
  394. foreach (array_keys($columns) as $i) {
  395. $this->_state["min_width"] += $columns[$i]["min-width"];
  396. $this->_state["max_width"] += $columns[$i]["max-width"];
  397. if ( $columns[$i]["absolute"] > 0 ) {
  398. $this->_state["absolute"][] = $i;
  399. $this->_state["absolute_used"] += $columns[$i]["absolute"];
  400. } else if ( $columns[$i]["percent"] > 0 ) {
  401. $this->_state["percent"][] = $i;
  402. $this->_state["percent_used"] += $columns[$i]["percent"];
  403. } else {
  404. $this->_state["auto"][] = $i;
  405. $this->_state["auto_min"] += $columns[$i]["min-width"];
  406. }
  407. }
  408. // Account for margins & padding
  409. $dims = array($style->border_left_width,
  410. $style->border_right_width,
  411. $style->padding_left,
  412. $style->padding_right,
  413. $style->margin_left,
  414. $style->margin_right);
  415. if ( $style->border_collapse !== "collapse" )
  416. list($dims[]) = $style->border_spacing;
  417. $delta = $style->length_in_pt($dims, $this->_frame->get_containing_block("w"));
  418. $this->_state["min_width"] += $delta;
  419. $this->_state["max_width"] += $delta;
  420. return $this->_min_max_cache = array(
  421. $this->_state["min_width"],
  422. $this->_state["max_width"],
  423. "min" => $this->_state["min_width"],
  424. "max" => $this->_state["max_width"],
  425. );
  426. }
  427. }