Matthias Jentsch 124b42c9f1 Remove tcpdf library and use dompdf instead
Remove tcpdf because of improper CSS support and use Dompdf instead, to be able
to generate pdfs that look similar to the html views

refs #4356
2014-02-12 12:11:01 +01:00

729 lines
19 KiB
PHP

<?php
/**
* @package dompdf
* @link http://www.dompdf.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
* @version $Id: cellmap.cls.php 464 2012-01-30 20:44:53Z fabien.menager $
*/
/**
* Maps table cells to the table grid.
*
* This class resolves borders in tables with collapsed borders and helps
* place row & column spanned table cells.
*
* @access private
* @package dompdf
*/
class Cellmap {
/**
* Border style weight lookup for collapsed border resolution.
*
* @var array
*/
static protected $_BORDER_STYLE_SCORE = array("inset" => 1,
"groove" => 2,
"outset" => 3,
"ridge" => 4,
"dotted" => 5,
"dashed" => 6,
"solid" => 7,
"double" => 8,
"hidden" => 9,
"none" => 0);
/**
* The table object this cellmap is attached to.
*
* @var Table_Frame_Decorator
*/
protected $_table;
/**
* The total number of rows in the table
*
* @var int
*/
protected $_num_rows;
/**
* The total number of columns in the table
*
* @var int
*/
protected $_num_cols;
/**
* 2D array mapping <row,column> to frames
*
* @var array
*/
protected $_cells;
/**
* 1D array of column dimensions
*
* @var array
*/
protected $_columns;
/**
* 1D array of row dimensions
*
* @var array
*/
protected $_rows;
/**
* 2D array of border specs
*
* @var array
*/
protected $_borders;
/**
* 1D Array mapping frames to (multiple) <row, col> pairs, keyed on
* frame_id.
*
* @var array
*/
protected $_frames;
/**
* @var int Current column when adding cells, 0-based
*/
private $__col;
/**
* @var int Current row when adding cells, 0-based
*/
private $__row;
/**
* @var bool Tells wether the columns' width can be modified
*/
private $_columns_locked = false;
//........................................................................
function __construct(Table_Frame_Decorator $table) {
$this->_table = $table;
$this->reset();
}
function __destruct() {
clear_object($this);
}
//........................................................................
function reset() {
$this->_num_rows = 0;
$this->_num_cols = 0;
$this->_cells = array();
$this->_frames = array();
if ( !$this->_columns_locked ) {
$this->_columns = array();
}
$this->_rows = array();
$this->_borders = array();
$this->__col = $this->__row = 0;
}
//........................................................................
function lock_columns() {
$this->_columns_locked = true;
}
function is_columns_locked() {
return $this->_columns_locked;
}
function get_num_rows() { return $this->_num_rows; }
function get_num_cols() { return $this->_num_cols; }
function &get_columns() {
return $this->_columns;
}
function set_columns($columns) {
$this->_columns = $columns;
}
function &get_column($i) {
if ( !isset($this->_columns[$i]) )
$this->_columns[$i] = array("x" => 0,
"min-width" => 0,
"max-width" => 0,
"used-width" => null,
"absolute" => 0,
"percent" => 0,
"auto" => true);
return $this->_columns[$i];
}
function &get_rows() {
return $this->_rows;
}
function &get_row($j) {
if ( !isset($this->_rows[$j]) )
$this->_rows[$j] = array("y" => 0,
"first-column" => 0,
"height" => null);
return $this->_rows[$j];
}
function get_border($i, $j, $h_v, $prop = null) {
if ( !isset($this->_borders[$i][$j][$h_v]) )
$this->_borders[$i][$j][$h_v] = array("width" => 0,
"style" => "solid",
"color" => "black");
if ( isset($prop) )
return $this->_borders[$i][$j][$h_v][$prop];
return $this->_borders[$i][$j][$h_v];
}
function get_border_properties($i, $j) {
$left = $this->get_border($i, $j, "vertical");
$right = $this->get_border($i, $j+1, "vertical");
$top = $this->get_border($i, $j, "horizontal");
$bottom = $this->get_border($i+1, $j, "horizontal");
return compact("top", "bottom", "left", "right");
}
//........................................................................
function get_spanned_cells($frame) {
$key = $frame->get_id();
if ( !isset($this->_frames[$key]) ) {
throw new DOMPDF_Exception("Frame not found in cellmap");
}
return $this->_frames[$key];
}
function frame_exists_in_cellmap($frame) {
$key = $frame->get_id();
return isset($this->_frames[$key]);
}
function get_frame_position($frame) {
global $_dompdf_warnings;
$key = $frame->get_id();
if ( !isset($this->_frames[$key]) ) {
throw new DOMPDF_Exception("Frame not found in cellmap");
}
$col = $this->_frames[$key]["columns"][0];
$row = $this->_frames[$key]["rows"][0];
if ( !isset($this->_columns[$col])) {
$_dompdf_warnings[] = "Frame not found in columns array. Check your table layout for missing or extra TDs.";
$x = 0;
} else
$x = $this->_columns[$col]["x"];
if ( !isset($this->_rows[$row])) {
$_dompdf_warnings[] = "Frame not found in row array. Check your table layout for missing or extra TDs.";
$y = 0;
} else
$y = $this->_rows[$row]["y"];
return array($x, $y, "x" => $x, "y" => $y);
}
function get_frame_width($frame) {
$key = $frame->get_id();
if ( !isset($this->_frames[$key]) ) {
throw new DOMPDF_Exception("Frame not found in cellmap");
}
$cols = $this->_frames[$key]["columns"];
$w = 0;
foreach ($cols as $i)
$w += $this->_columns[$i]["used-width"];
return $w;
}
function get_frame_height($frame) {
$key = $frame->get_id();
if ( !isset($this->_frames[$key]) ) {
throw new DOMPDF_Exception("Frame not found in cellmap");
}
$rows = $this->_frames[$key]["rows"];
$h = 0;
foreach ($rows as $i) {
if ( !isset($this->_rows[$i]) ) {
throw new Exception("foo");
}
$h += $this->_rows[$i]["height"];
}
return $h;
}
//........................................................................
function set_column_width($j, $width) {
if ( $this->_columns_locked ) {
return;
}
$col =& $this->get_column($j);
$col["used-width"] = $width;
$next_col =& $this->get_column($j+1);
$next_col["x"] = $next_col["x"] + $width;
}
function set_row_height($i, $height) {
$row =& $this->get_row($i);
if ( $row["height"] !== null && $height <= $row["height"] ) {
return;
}
$row["height"] = $height;
$next_row =& $this->get_row($i+1);
$next_row["y"] = $row["y"] + $height;
}
//........................................................................
protected function _resolve_border($i, $j, $h_v, $border_spec) {
$n_width = $border_spec["width"];
$n_style = $border_spec["style"];
$n_color = $border_spec["color"];
if ( !isset($this->_borders[$i][$j][$h_v]) ) {
$this->_borders[$i][$j][$h_v] = $border_spec;
return $this->_borders[$i][$j][$h_v]["width"];
}
$border = &$this->_borders[$i][$j][$h_v];
$o_width = $border["width"];
$o_style = $border["style"];
$o_color = $border["color"];
if ( ($n_style === "hidden" ||
$n_width > $o_width ||
$o_style === "none")
or
($o_width == $n_width &&
in_array($n_style, self::$_BORDER_STYLE_SCORE) &&
self::$_BORDER_STYLE_SCORE[ $n_style ] > self::$_BORDER_STYLE_SCORE[ $o_style ]) )
$border = $border_spec;
return $border["width"];
}
//........................................................................
function add_frame(Frame $frame) {
$style = $frame->get_style();
$display = $style->display;
$collapse = $this->_table->get_style()->border_collapse == "collapse";
// Recursively add the frames within tables, table-row-groups and table-rows
if ( $display === "table-row" ||
$display === "table" ||
$display === "inline-table" ||
in_array($display, Table_Frame_Decorator::$ROW_GROUPS) ) {
$start_row = $this->__row;
foreach ( $frame->get_children() as $child )
$this->add_frame( $child );
if ( $display === "table-row" )
$this->add_row();
$num_rows = $this->__row - $start_row - 1;
$key = $frame->get_id();
// Row groups always span across the entire table
$this->_frames[$key]["columns"] = range(0,max(0,$this->_num_cols-1));
$this->_frames[$key]["rows"] = range($start_row, max(0, $this->__row - 1));
$this->_frames[$key]["frame"] = $frame;
if ( $display !== "table-row" && $collapse ) {
$bp = $style->get_border_properties();
// Resolve the borders
for ( $i = 0; $i < $num_rows+1; $i++) {
$this->_resolve_border($start_row + $i, 0, "vertical", $bp["left"]);
$this->_resolve_border($start_row + $i, $this->_num_cols, "vertical", $bp["right"]);
}
for ( $j = 0; $j < $this->_num_cols; $j++) {
$this->_resolve_border($start_row, $j, "horizontal", $bp["top"]);
$this->_resolve_border($this->__row, $j, "horizontal", $bp["bottom"]);
}
}
return;
}
$node = $frame->get_node();
// Determine where this cell is going
$colspan = $node->getAttribute("colspan");
$rowspan = $node->getAttribute("rowspan");
if ( !$colspan ) {
$colspan = 1;
$node->setAttribute("colspan",1);
}
if ( !$rowspan ) {
$rowspan = 1;
$node->setAttribute("rowspan",1);
}
$key = $frame->get_id();
$bp = $style->get_border_properties();
// Add the frame to the cellmap
$max_left = $max_right = 0;
// Find the next available column (fix by Ciro Mondueri)
$ac = $this->__col;
while ( isset($this->_cells[$this->__row][$ac]) )
$ac++;
$this->__col = $ac;
// Rows:
for ( $i = 0; $i < $rowspan; $i++ ) {
$row = $this->__row + $i;
$this->_frames[$key]["rows"][] = $row;
for ( $j = 0; $j < $colspan; $j++)
$this->_cells[$row][$this->__col + $j] = $frame;
if ( $collapse ) {
// Resolve vertical borders
$max_left = max($max_left, $this->_resolve_border($row, $this->__col, "vertical", $bp["left"]));
$max_right = max($max_right, $this->_resolve_border($row, $this->__col + $colspan, "vertical", $bp["right"]));
}
}
$max_top = $max_bottom = 0;
// Columns:
for ( $j = 0; $j < $colspan; $j++ ) {
$col = $this->__col + $j;
$this->_frames[$key]["columns"][] = $col;
if ( $collapse ) {
// Resolve horizontal borders
$max_top = max($max_top, $this->_resolve_border($this->__row, $col, "horizontal", $bp["top"]));
$max_bottom = max($max_bottom, $this->_resolve_border($this->__row + $rowspan, $col, "horizontal", $bp["bottom"]));
}
}
$this->_frames[$key]["frame"] = $frame;
// Handle seperated border model
if ( !$collapse ) {
list($h, $v) = $this->_table->get_style()->border_spacing;
// Border spacing is effectively a margin between cells
$v = $style->length_in_pt($v) / 2;
$h = $style->length_in_pt($h) / 2;
$style->margin = "$v $h";
// The additional 1/2 width gets added to the table proper
} else {
// Drop the frame's actual border
$style->border_left_width = $max_left / 2;
$style->border_right_width = $max_right / 2;
$style->border_top_width = $max_top / 2;
$style->border_bottom_width = $max_bottom / 2;
$style->margin = "none";
}
// Resolve the frame's width
list($frame_min, $frame_max) = $frame->get_min_max_width();
$width = $style->width;
if ( is_percent($width) ) {
$var = "percent";
$val = (float)rtrim($width, "% ") / $colspan;
} else if ( $width !== "auto" ) {
$var = "absolute";
$val = $style->length_in_pt($frame_min) / $colspan;
}
if (!$this->_columns_locked) {
$min = 0;
$max = 0;
for ( $cs = 0; $cs < $colspan; $cs++ ) {
// Resolve the frame's width(s) with other cells
$col =& $this->get_column( $this->__col + $cs );
// Note: $var is either 'percent' or 'absolute'. We compare the
// requested percentage or absolute values with the existing widths
// and adjust accordingly.
if ( isset($var) && $val > $col[$var] ) {
$col[$var] = $val;
$col["auto"] = false;
}
$min += $col["min-width"];
$max += $col["max-width"];
}
if ( $frame_min > $min ) {
// The frame needs more space. Expand each sub-column
$inc = ($frame_min - $min) / $colspan;
for ($c = 0; $c < $colspan; $c++) {
$col =& $this->get_column($this->__col + $c);
$col["min-width"] += $inc;
}
}
if ( $frame_max > $max ) {
$inc = ($frame_max - $max) / $colspan;
for ($c = 0; $c < $colspan; $c++) {
$col =& $this->get_column($this->__col + $c);
$col["max-width"] += $inc;
}
}
}
$this->__col += $colspan;
if ( $this->__col > $this->_num_cols )
$this->_num_cols = $this->__col;
}
//........................................................................
function add_row() {
$this->__row++;
$this->_num_rows++;
// Find the next available column
$i = 0;
while ( isset($this->_cells[$this->__row][$i]) )
$i++;
$this->__col = $i;
}
//........................................................................
/**
* Remove a row from the cellmap.
*
* @param Frame
*/
function remove_row(Frame $row) {
$key = $row->get_id();
if ( !isset($this->_frames[$key]) )
return; // Presumably this row has alredy been removed
$this->_row = $this->_num_rows--;
$rows = $this->_frames[$key]["rows"];
$columns = $this->_frames[$key]["columns"];
// Remove all frames from this row
foreach ( $rows as $r ) {
foreach ( $columns as $c ) {
if ( isset($this->_cells[$r][$c]) ) {
$id = $this->_cells[$r][$c]->get_id();
$this->_frames[$id] = null;
unset($this->_frames[$id]);
$this->_cells[$r][$c] = null;
unset($this->_cells[$r][$c]);
}
}
$this->_rows[$r] = null;
unset($this->_rows[$r]);
}
$this->_frames[$key] = null;
unset($this->_frames[$key]);
}
/**
* Remove a row group from the cellmap.
*
* @param Frame $group The group to remove
*/
function remove_row_group(Frame $group) {
$key = $group->get_id();
if ( !isset($this->_frames[$key]) )
return; // Presumably this row has alredy been removed
$iter = $group->get_first_child();
while ($iter) {
$this->remove_row($iter);
$iter = $iter->get_next_sibling();
}
$this->_frames[$key] = null;
unset($this->_frames[$key]);
}
/**
* Update a row group after rows have been removed
*
* @param Frame $group The group to update
* @param Frame $last_row The last row in the row group
*/
function update_row_group(Frame $group, Frame $last_row) {
$g_key = $group->get_id();
$r_key = $last_row->get_id();
$r_rows = $this->_frames[$r_key]["rows"];
$this->_frames[$g_key]["rows"] = range( $this->_frames[$g_key]["rows"][0], end($r_rows) );
}
//........................................................................
function assign_x_positions() {
// Pre-condition: widths must be resolved and assigned to columns and
// column[0]["x"] must be set.
if ( $this->_columns_locked ) {
return;
}
$x = $this->_columns[0]["x"];
foreach ( array_keys($this->_columns) as $j ) {
$this->_columns[$j]["x"] = $x;
$x += $this->_columns[$j]["used-width"];
}
}
function assign_frame_heights() {
// Pre-condition: widths and heights of each column & row must be
// calcluated
foreach ( $this->_frames as $arr ) {
$frame = $arr["frame"];
$h = 0;
foreach( $arr["rows"] as $row ) {
if ( !isset($this->_rows[$row]) )
// The row has been removed because of a page split, so skip it.
continue;
$h += $this->_rows[$row]["height"];
}
if ( $frame instanceof Table_Cell_Frame_Decorator )
$frame->set_cell_height($h);
else
$frame->get_style()->height = $h;
}
}
//........................................................................
/**
* Re-adjust frame height if the table height is larger than its content
*/
function set_frame_heights($table_height, $content_height) {
// Distribute the increased height proportionally amongst each row
foreach ( $this->_frames as $arr ) {
$frame = $arr["frame"];
$h = 0;
foreach ($arr["rows"] as $row ) {
if ( !isset($this->_rows[$row]) )
continue;
$h += $this->_rows[$row]["height"];
}
if ( $content_height > 0 )
$new_height = ($h / $content_height) * $table_height;
else
$new_height = 0;
if ( $frame instanceof Table_Cell_Frame_Decorator )
$frame->set_cell_height($new_height);
else
$frame->get_style()->height = $new_height;
}
}
//........................................................................
// Used for debugging:
function __toString() {
$str = "";
$str .= "Columns:<br/>";
$str .= pre_r($this->_columns, true);
$str .= "Rows:<br/>";
$str .= pre_r($this->_rows, true);
$str .= "Frames:<br/>";
$arr = array();
foreach ( $this->_frames as $key => $val )
$arr[$key] = array("columns" => $val["columns"], "rows" => $val["rows"]);
$str .= pre_r($arr, true);
if ( php_sapi_name() == "cli" )
$str = strip_tags(str_replace(array("<br/>","<b>","</b>"),
array("\n",chr(27)."[01;33m", chr(27)."[0m"),
$str));
return $str;
}
}