table_frame_reflower.cls.php 17 KB

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