commited all contents created by participants

This commit is contained in:
Bachir Soussi Chiadmi
2017-12-21 18:12:40 +01:00
parent cefd1c2ad0
commit b936b1e616
2137 changed files with 1076319 additions and 730 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
<?php
namespace Dompdf;
/**
* Autoloads Dompdf classes
*
* @package Dompdf
*/
class Autoloader
{
const PREFIX = 'Dompdf';
/**
* Register the autoloader
*/
public static function register()
{
spl_autoload_register(array(new self, 'autoload'));
}
/**
* Autoloader
*
* @param string
*/
public static function autoload($class)
{
if ($class === 'Cpdf') {
require_once __DIR__ . "/../lib/Cpdf.php";
return;
}
$prefixLength = strlen(self::PREFIX);
if (0 === strncmp(self::PREFIX, $class, $prefixLength)) {
$file = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, $prefixLength));
$file = realpath(__DIR__ . (empty($file) ? '' : DIRECTORY_SEPARATOR) . $file . '.php');
if (file_exists($file)) {
require_once $file;
}
}
}
}
@@ -0,0 +1,59 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
/**
* Create canvas instances
*
* The canvas factory creates canvas instances based on the
* availability of rendering backends and config options.
*
* @package dompdf
*/
class CanvasFactory
{
/**
* Constructor is private: this is a static class
*/
private function __construct()
{
}
/**
* @param Dompdf $dompdf
* @param string|array $paper
* @param string $orientation
* @param string $class
*
* @return Canvas
*/
static function get_instance(Dompdf $dompdf, $paper = null, $orientation = null, $class = null)
{
$backend = strtolower($dompdf->getOptions()->getPdfBackend());
if (isset($class) && class_exists($class, false)) {
$class .= "_Adapter";
} else {
if (($backend === "auto" || $backend === "pdflib") &&
class_exists("PDFLib", false)
) {
$class = "Dompdf\\Adapter\\PDFLib";
}
else {
if ($backend === "gd") {
$class = "Dompdf\\Adapter\\GD";
} else {
$class = "Dompdf\\Adapter\\CPDF";
}
}
}
return new $class($paper, $orientation, $dompdf);
}
}
@@ -0,0 +1,422 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
/**
* Main rendering interface
*
* Currently {@link Dompdf\Adapter\CPDF}, {@link Dompdf\Adapter\PDFLib}, and {@link Dompdf\Adapter\GD}
* implement this interface.
*
* Implementations should measure x and y increasing to the left and down,
* respectively, with the origin in the top left corner. Implementations
* are free to use a unit other than points for length, but I can't
* guarantee that the results will look any good.
*
* @package dompdf
*/
interface Canvas
{
function __construct($paper = "letter", $orientation = "portrait", Dompdf $dompdf);
/**
* @return Dompdf
*/
function get_dompdf();
/**
* Returns the current page number
*
* @return int
*/
function get_page_number();
/**
* Returns the total number of pages
*
* @return int
*/
function get_page_count();
/**
* Sets the total number of pages
*
* @param int $count
*/
function set_page_count($count);
/**
* Draws a line from x1,y1 to x2,y2
*
* See {@link Style::munge_color()} for the format of the color array.
* See {@link Cpdf::setLineStyle()} for a description of the format of the
* $style parameter (aka dash).
*
* @param float $x1
* @param float $y1
* @param float $x2
* @param float $y2
* @param array $color
* @param float $width
* @param array $style
*/
function line($x1, $y1, $x2, $y2, $color, $width, $style = null);
/**
* Draws a rectangle at x1,y1 with width w and height h
*
* See {@link Style::munge_color()} for the format of the color array.
* See {@link Cpdf::setLineStyle()} for a description of the $style
* parameter (aka dash)
*
* @param float $x1
* @param float $y1
* @param float $w
* @param float $h
* @param array $color
* @param float $width
* @param array $style
*/
function rectangle($x1, $y1, $w, $h, $color, $width, $style = null);
/**
* Draws a filled rectangle at x1,y1 with width w and height h
*
* See {@link Style::munge_color()} for the format of the color array.
*
* @param float $x1
* @param float $y1
* @param float $w
* @param float $h
* @param array $color
*/
function filled_rectangle($x1, $y1, $w, $h, $color);
/**
* Starts a clipping rectangle at x1,y1 with width w and height h
*
* @param float $x1
* @param float $y1
* @param float $w
* @param float $h
*/
function clipping_rectangle($x1, $y1, $w, $h);
/**
* Starts a rounded clipping rectangle at x1,y1 with width w and height h
*
* @param float $x1
* @param float $y1
* @param float $w
* @param float $h
* @param float $tl
* @param float $tr
* @param float $br
* @param float $bl
*
* @return
*/
function clipping_roundrectangle($x1, $y1, $w, $h, $tl, $tr, $br, $bl);
/**
* Ends the last clipping shape
*/
function clipping_end();
/**
* Save current state
*/
function save();
/**
* Restore last state
*/
function restore();
/**
* Rotate
*
* @param float $angle angle in degrees for counter-clockwise rotation
* @param float $x Origin abscissa
* @param float $y Origin ordinate
*/
function rotate($angle, $x, $y);
/**
* Skew
*
* @param float $angle_x
* @param float $angle_y
* @param float $x Origin abscissa
* @param float $y Origin ordinate
*/
function skew($angle_x, $angle_y, $x, $y);
/**
* Scale
*
* @param float $s_x scaling factor for width as percent
* @param float $s_y scaling factor for height as percent
* @param float $x Origin abscissa
* @param float $y Origin ordinate
*/
function scale($s_x, $s_y, $x, $y);
/**
* Translate
*
* @param float $t_x movement to the right
* @param float $t_y movement to the bottom
*/
function translate($t_x, $t_y);
/**
* Transform
*
* @param $a
* @param $b
* @param $c
* @param $d
* @param $e
* @param $f
* @return
*/
function transform($a, $b, $c, $d, $e, $f);
/**
* Draws a polygon
*
* The polygon is formed by joining all the points stored in the $points
* array. $points has the following structure:
* <code>
* array(0 => x1,
* 1 => y1,
* 2 => x2,
* 3 => y2,
* ...
* );
* </code>
*
* See {@link Style::munge_color()} for the format of the color array.
* See {@link Cpdf::setLineStyle()} for a description of the $style
* parameter (aka dash)
*
* @param array $points
* @param array $color
* @param float $width
* @param array $style
* @param bool $fill Fills the polygon if true
*/
function polygon($points, $color, $width = null, $style = null, $fill = false);
/**
* Draws a circle at $x,$y with radius $r
*
* See {@link Style::munge_color()} for the format of the color array.
* See {@link Cpdf::setLineStyle()} for a description of the $style
* parameter (aka dash)
*
* @param float $x
* @param float $y
* @param float $r
* @param array $color
* @param float $width
* @param array $style
* @param bool $fill Fills the circle if true
*/
function circle($x, $y, $r, $color, $width = null, $style = null, $fill = false);
/**
* Add an image to the pdf.
*
* The image is placed at the specified x and y coordinates with the
* given width and height.
*
* @param string $img_url the path to the image
* @param float $x x position
* @param float $y y position
* @param int $w width (in pixels)
* @param int $h height (in pixels)
* @param string $resolution The resolution of the image
*/
function image($img_url, $x, $y, $w, $h, $resolution = "normal");
/**
* Add an arc to the PDF
* See {@link Style::munge_color()} for the format of the color array.
*
* @param float $x X coordinate of the arc
* @param float $y Y coordinate of the arc
* @param float $r1 Radius 1
* @param float $r2 Radius 2
* @param float $astart Start angle in degrees
* @param float $aend End angle in degrees
* @param array $color Color
* @param float $width
* @param array $style
*/
function arc($x, $y, $r1, $r2, $astart, $aend, $color, $width, $style = array());
/**
* Writes text at the specified x and y coordinates
* See {@link Style::munge_color()} for the format of the color array.
*
* @param float $x
* @param float $y
* @param string $text the text to write
* @param string $font the font file to use
* @param float $size the font size, in points
* @param array $color
* @param float $word_space word spacing adjustment
* @param float $char_space char spacing adjustment
* @param float $angle angle
*/
function text($x, $y, $text, $font, $size, $color = array(0, 0, 0), $word_space = 0.0, $char_space = 0.0, $angle = 0.0);
/**
* Add a named destination (similar to <a name="foo">...</a> in html)
*
* @param string $anchorname The name of the named destination
*/
function add_named_dest($anchorname);
/**
* Add a link to the pdf
*
* @param string $url The url to link to
* @param float $x The x position of the link
* @param float $y The y position of the link
* @param float $width The width of the link
* @param float $height The height of the link
*/
function add_link($url, $x, $y, $width, $height);
/**
* Add meta information to the pdf
*
* @param string $name Label of the value (Creator, Producer, etc.)
* @param string $value The text to set
*/
function add_info($name, $value);
/**
* Calculates text size, in points
*
* @param string $text the text to be sized
* @param string $font the desired font
* @param float $size the desired font size
* @param float $word_spacing word spacing, if any
* @param float $char_spacing
*
* @return float
*/
function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0);
/**
* Calculates font height, in points
*
* @param string $font
* @param float $size
*
* @return float
*/
function get_font_height($font, $size);
/**
* Calculates font baseline, in points
*
* @param string $font
* @param float $size
*
* @return float
*/
function get_font_baseline($font, $size);
/**
* Returns the PDF's width in points
*
* @return float
*/
function get_width();
/**
* Return the image's height in pixels
*
* @return float
*/
function get_height();
/**
* Returns the font x-height, in points
*
* @param string $font
* @param float $size
*
* @return float
*/
//function get_font_x_height($font, $size);
/**
* Sets the opacity
*
* @param float $opacity
* @param string $mode
*/
function set_opacity($opacity, $mode = "Normal");
/**
* Sets the default view
*
* @param string $view
* 'XYZ' left, top, zoom
* 'Fit'
* 'FitH' top
* 'FitV' left
* 'FitR' left,bottom,right
* 'FitB'
* 'FitBH' top
* 'FitBV' left
* @param array $options
*
* @return void
*/
function set_default_view($view, $options = array());
/**
* @param string $script
*
* @return void
*/
function javascript($script);
/**
* Starts a new page
*
* Subsequent drawing operations will appear on the new page.
*/
function new_page();
/**
* Streams the PDF directly to the browser
*
* @param string $filename the name of the PDF file
* @param array $options associative array, 'Attachment' => 0 or 1, 'compress' => 1 or 0
*/
function stream($filename, $options = null);
/**
* Returns the PDF as a string
*
* @param array $options associative array: 'compress' => 1 or 0
* @return string
*/
function output($options = null);
}
@@ -0,0 +1,918 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
use Dompdf\Exception;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
use Dompdf\FrameDecorator\TableCell as TableCellFrameDecorator;
/**
* Maps table cells to the table grid.
*
* This class resolves borders in tables with collapsed borders and helps
* place row & column spanned table cells.
*
* @package dompdf
*/
class Cellmap
{
/**
* Border style weight lookup for collapsed border resolution.
*
* @var array
*/
protected static $_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 TableFrameDecorator
*/
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 Frame[][]
*/
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 Frame[]
*/
protected $_frames;
/**
* Current column when adding cells, 0-based
*
* @var int
*/
private $__col;
/**
* Current row when adding cells, 0-based
*
* @var int
*/
private $__row;
/**
* Tells wether the columns' width can be modified
*
* @var bool
*/
private $_columns_locked = false;
/**
* Tells wether the table has table-layout:fixed
*
* @var bool
*/
private $_fixed_layout = false;
/**
* @param TableFrameDecorator $table
*/
public function __construct(TableFrameDecorator $table)
{
$this->_table = $table;
$this->reset();
}
/**
*
*/
public 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;
}
/**
*
*/
public function lock_columns()
{
$this->_columns_locked = true;
}
/**
* @return bool
*/
public function is_columns_locked()
{
return $this->_columns_locked;
}
/**
* @param $fixed
*/
public function set_layout_fixed($fixed)
{
$this->_fixed_layout = $fixed;
}
/**
* @return bool
*/
public function is_layout_fixed()
{
return $this->_fixed_layout;
}
/**
* @return int
*/
public function get_num_rows()
{
return $this->_num_rows;
}
/**
* @return int
*/
public function get_num_cols()
{
return $this->_num_cols;
}
/**
* @return array
*/
public function &get_columns()
{
return $this->_columns;
}
/**
* @param $columns
*/
public function set_columns($columns)
{
$this->_columns = $columns;
}
/**
* @param int $i
*
* @return mixed
*/
public 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];
}
/**
* @return array
*/
public function &get_rows()
{
return $this->_rows;
}
/**
* @param int $j
*
* @return mixed
*/
public function &get_row($j)
{
if (!isset($this->_rows[$j])) {
$this->_rows[$j] = array(
"y" => 0,
"first-column" => 0,
"height" => null,
);
}
return $this->_rows[$j];
}
/**
* @param int $i
* @param int $j
* @param mixed $h_v
* @param null|mixed $prop
*
* @return mixed
*/
public 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];
}
/**
* @param int $i
* @param int $j
*
* @return array
*/
public function get_border_properties($i, $j)
{
return array(
"top" => $this->get_border($i, $j, "horizontal"),
"right" => $this->get_border($i, $j + 1, "vertical"),
"bottom" => $this->get_border($i + 1, $j, "horizontal"),
"left" => $this->get_border($i, $j, "vertical"),
);
}
/**
* @param Frame $frame
*
* @return null|Frame
*/
public function get_spanned_cells(Frame $frame)
{
$key = $frame->get_id();
if (isset($this->_frames[$key])) {
return $this->_frames[$key];
}
return null;
}
/**
* @param Frame $frame
*
* @return bool
*/
public function frame_exists_in_cellmap(Frame $frame)
{
$key = $frame->get_id();
return isset($this->_frames[$key]);
}
/**
* @param Frame $frame
*
* @return array
* @throws Exception
*/
public function get_frame_position(Frame $frame)
{
global $_dompdf_warnings;
$key = $frame->get_id();
if (!isset($this->_frames[$key])) {
throw new 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);
}
/**
* @param Frame $frame
*
* @return int
* @throws Exception
*/
public function get_frame_width(Frame $frame)
{
$key = $frame->get_id();
if (!isset($this->_frames[$key])) {
throw new 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;
}
/**
* @param Frame $frame
*
* @return int
* @throws Exception
* @throws Exception
*/
public function get_frame_height(Frame $frame)
{
$key = $frame->get_id();
if (!isset($this->_frames[$key])) {
throw new 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("The row #$i could not be found, please file an issue in the tracker with the HTML code");
}
$h += $this->_rows[$i]["height"];
}
return $h;
}
/**
* @param int $j
* @param mixed $width
*/
public 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;
}
/**
* @param int $i
* @param mixed $height
*/
public 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;
}
/**
* @param int $i
* @param int $j
* @param mixed $h_v
* @param mixed $border_spec
*
* @return mixed
*/
protected function _resolve_border($i, $j, $h_v, $border_spec)
{
$n_width = $border_spec["width"];
$n_style = $border_spec["style"];
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"];
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"];
}
/**
* @param Frame $frame
*/
public 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, TableFrameDecorator::$ROW_GROUPS)
) {
$start_row = $this->__row;
foreach ($frame->get_children() as $child) {
// Ignore all Text frames and :before/:after pseudo-selector elements.
if (!($child instanceof FrameDecorator\Text) && $child->get_node()->nodeName !== 'dompdf_generated') {
$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);
if (is_numeric($v)) {
$v = $v / 2;
}
$h = $style->length_in_pt($h);
if (is_numeric($h)) {
$h = $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";
}
if (!$this->_columns_locked) {
// Resolve the frame's width
if ($this->_fixed_layout) {
list($frame_min, $frame_max) = array(0, 10e-10);
} else {
list($frame_min, $frame_max) = $frame->get_min_max_width();
}
$width = $style->width;
$val = null;
if (Helpers::is_percent($width)) {
$var = "percent";
$val = (float)rtrim($width, "% ") / $colspan;
} else if ($width !== "auto") {
$var = "absolute";
$val = $style->length_in_pt($frame_min) / $colspan;
}
$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
// FIXME try to avoid putting this dummy value when table-layout:fixed
$inc = ($this->is_layout_fixed() ? 10e-10 : ($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) {
// FIXME try to avoid putting this dummy value when table-layout:fixed
$inc = ($this->is_layout_fixed() ? 10e-10 : ($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;
}
}
/**
*
*/
public 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
*/
public 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->_cells[$r][$c] = null;
unset($this->_cells[$r][$c]);
// has multiple rows?
if (isset($this->_frames[$id]) && count($this->_frames[$id]["rows"]) > 1) {
// remove just the desired row, but leave the frame
if (($row_key = array_search($r, $this->_frames[$id]["rows"])) !== false) {
unset($this->_frames[$id]["rows"][$row_key]);
}
continue;
}
$this->_frames[$id] = null;
unset($this->_frames[$id]);
}
}
$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
*/
public 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
*/
public function update_row_group(Frame $group, Frame $last_row)
{
$g_key = $group->get_id();
$r_key = $last_row->get_id();
$r_rows = $this->_frames[$g_key]["rows"];
$this->_frames[$g_key]["rows"] = range($this->_frames[$g_key]["rows"][0], end($r_rows));
}
/**
*
*/
public 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"];
}
}
/**
*
*/
public 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 TableCellFrameDecorator) {
$frame->set_cell_height($h);
} else {
$frame->get_style()->height = $h;
}
}
}
/**
* Re-adjust frame height if the table height is larger than its content
*/
public 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 TableCellFrameDecorator) {
$frame->set_cell_height($new_height);
} else {
$frame->get_style()->height = $new_height;
}
}
}
/**
* Used for debugging:
*
* @return string
*/
public function __toString()
{
$str = "";
$str .= "Columns:<br/>";
$str .= Helpers::pre_r($this->_columns, true);
$str .= "Rows:<br/>";
$str .= Helpers::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 .= Helpers::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;
}
}
@@ -0,0 +1,639 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Css;
use Dompdf\Frame;
/**
* Translates HTML 4.0 attributes into CSS rules
*
* @package dompdf
*/
class AttributeTranslator
{
static $_style_attr = "_html_style_attribute";
// Munged data originally from
// http://www.w3.org/TR/REC-html40/index/attributes.html
// http://www.cs.tut.fi/~jkorpela/html2css.html
static private $__ATTRIBUTE_LOOKUP = array(
//'caption' => array ( 'align' => '', ),
'img' => array(
'align' => array(
'bottom' => 'vertical-align: baseline;',
'middle' => 'vertical-align: middle;',
'top' => 'vertical-align: top;',
'left' => 'float: left;',
'right' => 'float: right;'
),
'border' => 'border: %0.2Fpx solid;',
'height' => 'height: %spx;',
'hspace' => 'padding-left: %1$0.2Fpx; padding-right: %1$0.2Fpx;',
'vspace' => 'padding-top: %1$0.2Fpx; padding-bottom: %1$0.2Fpx;',
'width' => 'width: %spx;',
),
'table' => array(
'align' => array(
'left' => 'margin-left: 0; margin-right: auto;',
'center' => 'margin-left: auto; margin-right: auto;',
'right' => 'margin-left: auto; margin-right: 0;'
),
'bgcolor' => 'background-color: %s;',
'border' => '!set_table_border',
'cellpadding' => '!set_table_cellpadding', //'border-spacing: %0.2F; border-collapse: separate;',
'cellspacing' => '!set_table_cellspacing',
'frame' => array(
'void' => 'border-style: none;',
'above' => 'border-top-style: solid;',
'below' => 'border-bottom-style: solid;',
'hsides' => 'border-left-style: solid; border-right-style: solid;',
'vsides' => 'border-top-style: solid; border-bottom-style: solid;',
'lhs' => 'border-left-style: solid;',
'rhs' => 'border-right-style: solid;',
'box' => 'border-style: solid;',
'border' => 'border-style: solid;'
),
'rules' => '!set_table_rules',
'width' => 'width: %s;',
),
'hr' => array(
'align' => '!set_hr_align', // Need to grab width to set 'left' & 'right' correctly
'noshade' => 'border-style: solid;',
'size' => '!set_hr_size', //'border-width: %0.2F px;',
'width' => 'width: %s;',
),
'div' => array(
'align' => 'text-align: %s;',
),
'h1' => array(
'align' => 'text-align: %s;',
),
'h2' => array(
'align' => 'text-align: %s;',
),
'h3' => array(
'align' => 'text-align: %s;',
),
'h4' => array(
'align' => 'text-align: %s;',
),
'h5' => array(
'align' => 'text-align: %s;',
),
'h6' => array(
'align' => 'text-align: %s;',
),
//TODO: translate more form element attributes
'input' => array(
'size' => '!set_input_width'
),
'p' => array(
'align' => 'text-align: %s;',
),
// 'col' => array(
// 'align' => '',
// 'valign' => '',
// ),
// 'colgroup' => array(
// 'align' => '',
// 'valign' => '',
// ),
'tbody' => array(
'align' => '!set_table_row_align',
'valign' => '!set_table_row_valign',
),
'td' => array(
'align' => 'text-align: %s;',
'bgcolor' => '!set_background_color',
'height' => 'height: %s;',
'nowrap' => 'white-space: nowrap;',
'valign' => 'vertical-align: %s;',
'width' => 'width: %s;',
),
'tfoot' => array(
'align' => '!set_table_row_align',
'valign' => '!set_table_row_valign',
),
'th' => array(
'align' => 'text-align: %s;',
'bgcolor' => '!set_background_color',
'height' => 'height: %s;',
'nowrap' => 'white-space: nowrap;',
'valign' => 'vertical-align: %s;',
'width' => 'width: %s;',
),
'thead' => array(
'align' => '!set_table_row_align',
'valign' => '!set_table_row_valign',
),
'tr' => array(
'align' => '!set_table_row_align',
'bgcolor' => '!set_table_row_bgcolor',
'valign' => '!set_table_row_valign',
),
'body' => array(
'background' => 'background-image: url(%s);',
'bgcolor' => '!set_background_color',
'link' => '!set_body_link',
'text' => '!set_color',
),
'br' => array(
'clear' => 'clear: %s;',
),
'basefont' => array(
'color' => '!set_color',
'face' => 'font-family: %s;',
'size' => '!set_basefont_size',
),
'font' => array(
'color' => '!set_color',
'face' => 'font-family: %s;',
'size' => '!set_font_size',
),
'dir' => array(
'compact' => 'margin: 0.5em 0;',
),
'dl' => array(
'compact' => 'margin: 0.5em 0;',
),
'menu' => array(
'compact' => 'margin: 0.5em 0;',
),
'ol' => array(
'compact' => 'margin: 0.5em 0;',
'start' => 'counter-reset: -dompdf-default-counter %d;',
'type' => 'list-style-type: %s;',
),
'ul' => array(
'compact' => 'margin: 0.5em 0;',
'type' => 'list-style-type: %s;',
),
'li' => array(
'type' => 'list-style-type: %s;',
'value' => 'counter-reset: -dompdf-default-counter %d;',
),
'pre' => array(
'width' => 'width: %s;',
),
);
static protected $_last_basefont_size = 3;
static protected $_font_size_lookup = array(
// For basefont support
-3 => "4pt",
-2 => "5pt",
-1 => "6pt",
0 => "7pt",
1 => "8pt",
2 => "10pt",
3 => "12pt",
4 => "14pt",
5 => "18pt",
6 => "24pt",
7 => "34pt",
// For basefont support
8 => "48pt",
9 => "44pt",
10 => "52pt",
11 => "60pt",
);
/**
* @param Frame $frame
*/
static function translate_attributes(Frame $frame)
{
$node = $frame->get_node();
$tag = $node->nodeName;
if (!isset(self::$__ATTRIBUTE_LOOKUP[$tag])) {
return;
}
$valid_attrs = self::$__ATTRIBUTE_LOOKUP[$tag];
$attrs = $node->attributes;
$style = rtrim($node->getAttribute(self::$_style_attr), "; ");
if ($style != "") {
$style .= ";";
}
foreach ($attrs as $attr => $attr_node) {
if (!isset($valid_attrs[$attr])) {
continue;
}
$value = $attr_node->value;
$target = $valid_attrs[$attr];
// Look up $value in $target, if $target is an array:
if (is_array($target)) {
if (isset($target[$value])) {
$style .= " " . self::_resolve_target($node, $target[$value], $value);
}
} else {
// otherwise use target directly
$style .= " " . self::_resolve_target($node, $target, $value);
}
}
if (!is_null($style)) {
$style = ltrim($style);
$node->setAttribute(self::$_style_attr, $style);
}
}
/**
* @param \DOMNode $node
* @param string $target
* @param string $value
*
* @return string
*/
static protected function _resolve_target(\DOMNode $node, $target, $value)
{
if ($target[0] === "!") {
// Function call
$func = "_" . mb_substr($target, 1);
return self::$func($node, $value);
}
return $value ? sprintf($target, $value) : "";
}
/**
* @param \DOMElement $node
* @param string $new_style
*/
static function append_style(\DOMElement $node, $new_style)
{
$style = rtrim($node->getAttribute(self::$_style_attr), ";");
$style .= $new_style;
$style = ltrim($style, ";");
$node->setAttribute(self::$_style_attr, $style);
}
/**
* @param \DOMNode $node
*
* @return \DOMNodeList|\DOMElement[]
*/
static protected function get_cell_list(\DOMNode $node)
{
$xpath = new \DOMXpath($node->ownerDocument);
switch ($node->nodeName) {
default:
case "table":
$query = "tr/td | thead/tr/td | tbody/tr/td | tfoot/tr/td | tr/th | thead/tr/th | tbody/tr/th | tfoot/tr/th";
break;
case "tbody":
case "tfoot":
case "thead":
$query = "tr/td | tr/th";
break;
case "tr":
$query = "td | th";
break;
}
return $xpath->query($query, $node);
}
/**
* @param string $value
*
* @return string
*/
static protected function _get_valid_color($value)
{
if (preg_match('/^#?([0-9A-F]{6})$/i', $value, $matches)) {
$value = "#$matches[1]";
}
return $value;
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return string
*/
static protected function _set_color(\DOMElement $node, $value)
{
$value = self::_get_valid_color($value);
return "color: $value;";
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return string
*/
static protected function _set_background_color(\DOMElement $node, $value)
{
$value = self::_get_valid_color($value);
return "background-color: $value;";
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null
*/
static protected function _set_table_cellpadding(\DOMElement $node, $value)
{
$cell_list = self::get_cell_list($node);
foreach ($cell_list as $cell) {
self::append_style($cell, "; padding: {$value}px;");
}
return null;
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return string
*/
static protected function _set_table_border(\DOMElement $node, $value)
{
$cell_list = self::get_cell_list($node);
foreach ($cell_list as $cell) {
$style = rtrim($cell->getAttribute(self::$_style_attr));
$style .= "; border-width: " . ($value > 0 ? 1 : 0) . "pt; border-style: inset;";
$style = ltrim($style, ";");
$cell->setAttribute(self::$_style_attr, $style);
}
$style = rtrim($node->getAttribute(self::$_style_attr), ";");
$style .= "; border-width: $value" . "px; ";
return ltrim($style, "; ");
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return string
*/
static protected function _set_table_cellspacing(\DOMElement $node, $value)
{
$style = rtrim($node->getAttribute(self::$_style_attr), ";");
if ($value == 0) {
$style .= "; border-collapse: collapse;";
} else {
$style .= "; border-spacing: {$value}px; border-collapse: separate;";
}
return ltrim($style, ";");
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null|string
*/
static protected function _set_table_rules(\DOMElement $node, $value)
{
$new_style = "; border-collapse: collapse;";
switch ($value) {
case "none":
$new_style .= "border-style: none;";
break;
case "groups":
// FIXME: unsupported
return null;
case "rows":
$new_style .= "border-style: solid none solid none; border-width: 1px; ";
break;
case "cols":
$new_style .= "border-style: none solid none solid; border-width: 1px; ";
break;
case "all":
$new_style .= "border-style: solid; border-width: 1px; ";
break;
default:
// Invalid value
return null;
}
$cell_list = self::get_cell_list($node);
foreach ($cell_list as $cell) {
$style = $cell->getAttribute(self::$_style_attr);
$style .= $new_style;
$cell->setAttribute(self::$_style_attr, $style);
}
$style = rtrim($node->getAttribute(self::$_style_attr), ";");
$style .= "; border-collapse: collapse; ";
return ltrim($style, "; ");
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return string
*/
static protected function _set_hr_size(\DOMElement $node, $value)
{
$style = rtrim($node->getAttribute(self::$_style_attr), ";");
$style .= "; border-width: " . max(0, $value - 2) . "; ";
return ltrim($style, "; ");
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null|string
*/
static protected function _set_hr_align(\DOMElement $node, $value)
{
$style = rtrim($node->getAttribute(self::$_style_attr), ";");
$width = $node->getAttribute("width");
if ($width == "") {
$width = "100%";
}
$remainder = 100 - (double)rtrim($width, "% ");
switch ($value) {
case "left":
$style .= "; margin-right: $remainder %;";
break;
case "right":
$style .= "; margin-left: $remainder %;";
break;
case "center":
$style .= "; margin-left: auto; margin-right: auto;";
break;
default:
return null;
}
return ltrim($style, "; ");
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null|string
*/
static protected function _set_input_width(\DOMElement $node, $value)
{
if (empty($value)) { return null; }
if ($node->hasAttribute("type") && in_array(strtolower($node->getAttribute("type")), array("text","password"))) {
return sprintf("width: %Fem", (((int)$value * .65)+2));
} else {
return sprintf("width: %upx;", (int)$value);
}
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null
*/
static protected function _set_table_row_align(\DOMElement $node, $value)
{
$cell_list = self::get_cell_list($node);
foreach ($cell_list as $cell) {
self::append_style($cell, "; text-align: $value;");
}
return null;
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null
*/
static protected function _set_table_row_valign(\DOMElement $node, $value)
{
$cell_list = self::get_cell_list($node);
foreach ($cell_list as $cell) {
self::append_style($cell, "; vertical-align: $value;");
}
return null;
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null
*/
static protected function _set_table_row_bgcolor(\DOMElement $node, $value)
{
$cell_list = self::get_cell_list($node);
$value = self::_get_valid_color($value);
foreach ($cell_list as $cell) {
self::append_style($cell, "; background-color: $value;");
}
return null;
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null
*/
static protected function _set_body_link(\DOMElement $node, $value)
{
$a_list = $node->getElementsByTagName("a");
$value = self::_get_valid_color($value);
foreach ($a_list as $a) {
self::append_style($a, "; color: $value;");
}
return null;
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null
*/
static protected function _set_basefont_size(\DOMElement $node, $value)
{
// FIXME: ? we don't actually set the font size of anything here, just
// the base size for later modification by <font> tags.
self::$_last_basefont_size = $value;
return null;
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return string
*/
static protected function _set_font_size(\DOMElement $node, $value)
{
$style = $node->getAttribute(self::$_style_attr);
if ($value[0] === "-" || $value[0] === "+") {
$value = self::$_last_basefont_size + (int)$value;
}
if (isset(self::$_font_size_lookup[$value])) {
$style .= "; font-size: " . self::$_font_size_lookup[$value] . ";";
} else {
$style .= "; font-size: $value;";
}
return ltrim($style, "; ");
}
}
@@ -0,0 +1,307 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Css;
use Dompdf\Helpers;
class Color
{
static $cssColorNames = array(
"aliceblue" => "F0F8FF",
"antiquewhite" => "FAEBD7",
"aqua" => "00FFFF",
"aquamarine" => "7FFFD4",
"azure" => "F0FFFF",
"beige" => "F5F5DC",
"bisque" => "FFE4C4",
"black" => "000000",
"blanchedalmond" => "FFEBCD",
"blue" => "0000FF",
"blueviolet" => "8A2BE2",
"brown" => "A52A2A",
"burlywood" => "DEB887",
"cadetblue" => "5F9EA0",
"chartreuse" => "7FFF00",
"chocolate" => "D2691E",
"coral" => "FF7F50",
"cornflowerblue" => "6495ED",
"cornsilk" => "FFF8DC",
"crimson" => "DC143C",
"cyan" => "00FFFF",
"darkblue" => "00008B",
"darkcyan" => "008B8B",
"darkgoldenrod" => "B8860B",
"darkgray" => "A9A9A9",
"darkgreen" => "006400",
"darkgrey" => "A9A9A9",
"darkkhaki" => "BDB76B",
"darkmagenta" => "8B008B",
"darkolivegreen" => "556B2F",
"darkorange" => "FF8C00",
"darkorchid" => "9932CC",
"darkred" => "8B0000",
"darksalmon" => "E9967A",
"darkseagreen" => "8FBC8F",
"darkslateblue" => "483D8B",
"darkslategray" => "2F4F4F",
"darkslategrey" => "2F4F4F",
"darkturquoise" => "00CED1",
"darkviolet" => "9400D3",
"deeppink" => "FF1493",
"deepskyblue" => "00BFFF",
"dimgray" => "696969",
"dimgrey" => "696969",
"dodgerblue" => "1E90FF",
"firebrick" => "B22222",
"floralwhite" => "FFFAF0",
"forestgreen" => "228B22",
"fuchsia" => "FF00FF",
"gainsboro" => "DCDCDC",
"ghostwhite" => "F8F8FF",
"gold" => "FFD700",
"goldenrod" => "DAA520",
"gray" => "808080",
"green" => "008000",
"greenyellow" => "ADFF2F",
"grey" => "808080",
"honeydew" => "F0FFF0",
"hotpink" => "FF69B4",
"indianred" => "CD5C5C",
"indigo" => "4B0082",
"ivory" => "FFFFF0",
"khaki" => "F0E68C",
"lavender" => "E6E6FA",
"lavenderblush" => "FFF0F5",
"lawngreen" => "7CFC00",
"lemonchiffon" => "FFFACD",
"lightblue" => "ADD8E6",
"lightcoral" => "F08080",
"lightcyan" => "E0FFFF",
"lightgoldenrodyellow" => "FAFAD2",
"lightgray" => "D3D3D3",
"lightgreen" => "90EE90",
"lightgrey" => "D3D3D3",
"lightpink" => "FFB6C1",
"lightsalmon" => "FFA07A",
"lightseagreen" => "20B2AA",
"lightskyblue" => "87CEFA",
"lightslategray" => "778899",
"lightslategrey" => "778899",
"lightsteelblue" => "B0C4DE",
"lightyellow" => "FFFFE0",
"lime" => "00FF00",
"limegreen" => "32CD32",
"linen" => "FAF0E6",
"magenta" => "FF00FF",
"maroon" => "800000",
"mediumaquamarine" => "66CDAA",
"mediumblue" => "0000CD",
"mediumorchid" => "BA55D3",
"mediumpurple" => "9370DB",
"mediumseagreen" => "3CB371",
"mediumslateblue" => "7B68EE",
"mediumspringgreen" => "00FA9A",
"mediumturquoise" => "48D1CC",
"mediumvioletred" => "C71585",
"midnightblue" => "191970",
"mintcream" => "F5FFFA",
"mistyrose" => "FFE4E1",
"moccasin" => "FFE4B5",
"navajowhite" => "FFDEAD",
"navy" => "000080",
"oldlace" => "FDF5E6",
"olive" => "808000",
"olivedrab" => "6B8E23",
"orange" => "FFA500",
"orangered" => "FF4500",
"orchid" => "DA70D6",
"palegoldenrod" => "EEE8AA",
"palegreen" => "98FB98",
"paleturquoise" => "AFEEEE",
"palevioletred" => "DB7093",
"papayawhip" => "FFEFD5",
"peachpuff" => "FFDAB9",
"peru" => "CD853F",
"pink" => "FFC0CB",
"plum" => "DDA0DD",
"powderblue" => "B0E0E6",
"purple" => "800080",
"red" => "FF0000",
"rosybrown" => "BC8F8F",
"royalblue" => "4169E1",
"saddlebrown" => "8B4513",
"salmon" => "FA8072",
"sandybrown" => "F4A460",
"seagreen" => "2E8B57",
"seashell" => "FFF5EE",
"sienna" => "A0522D",
"silver" => "C0C0C0",
"skyblue" => "87CEEB",
"slateblue" => "6A5ACD",
"slategray" => "708090",
"slategrey" => "708090",
"snow" => "FFFAFA",
"springgreen" => "00FF7F",
"steelblue" => "4682B4",
"tan" => "D2B48C",
"teal" => "008080",
"thistle" => "D8BFD8",
"tomato" => "FF6347",
"turquoise" => "40E0D0",
"violet" => "EE82EE",
"wheat" => "F5DEB3",
"white" => "FFFFFF",
"whitesmoke" => "F5F5F5",
"yellow" => "FFFF00",
"yellowgreen" => "9ACD32",
);
/**
* @param $color
* @return array|mixed|null|string
*/
static function parse($color)
{
if (is_array($color)) {
// Assume the array has the right format...
// FIXME: should/could verify this.
return $color;
}
static $cache = array();
$color = strtolower($color);
if (isset($cache[$color])) {
return $cache[$color];
}
if (in_array($color, array("transparent", "inherit"))) {
return $cache[$color] = $color;
}
if (isset(self::$cssColorNames[$color])) {
return $cache[$color] = self::getArray(self::$cssColorNames[$color]);
}
$length = mb_strlen($color);
// #rgb format
if ($length == 4 && $color[0] === "#") {
return $cache[$color] = self::getArray($color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3]);
} // #rgba format
else if ($length == 5 && $color[0] === "#") {
$alpha = round(hexdec($color[4] . $color[4])/255, 2);
return $cache[$color] = self::getArray($color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3], $alpha);
} // #rrggbb format
else if ($length == 7 && $color[0] === "#") {
return $cache[$color] = self::getArray(mb_substr($color, 1, 6));
} // #rrggbbaa format
else if ($length == 9 && $color[0] === "#") {
$alpha = round(hexdec(mb_substr($color, 7, 2))/255, 2);
return $cache[$color] = self::getArray(mb_substr($color, 1, 8), $alpha);
} // rgb( r,g,b ) / rgbaa( r,g,b,α ) format
else if (mb_strpos($color, "rgb") !== false) {
$i = mb_strpos($color, "(");
$j = mb_strpos($color, ")");
// Bad color value
if ($i === false || $j === false) {
return null;
}
$triplet = explode(",", mb_substr($color, $i + 1, $j - $i - 1));
// alpha transparency
// FIXME: not currently using transparency
$alpha = 1.0;
if (count($triplet) == 4) {
$alpha = (float)(trim(array_pop($triplet)));
// bad value, set to fully opaque
if ($alpha > 1.0 || $alpha < 0.0) {
$alpha = 1.0;
}
}
if (count($triplet) != 3) {
return null;
}
foreach (array_keys($triplet) as $c) {
$triplet[$c] = trim($triplet[$c]);
if (Helpers::is_percent($triplet[$c])) {
$triplet[$c] = round((float)$triplet[$c] * 2.55);
}
}
return $cache[$color] = self::getArray(vsprintf("%02X%02X%02X", $triplet), $alpha);
}
// cmyk( c,m,y,k ) format
// http://www.w3.org/TR/css3-gcpm/#cmyk-colors
else if (mb_strpos($color, "cmyk") !== false) {
$i = mb_strpos($color, "(");
$j = mb_strpos($color, ")");
// Bad color value
if ($i === false || $j === false) {
return null;
}
$values = explode(",", mb_substr($color, $i + 1, $j - $i - 1));
if (count($values) != 4) {
return null;
}
$values = array_map(function($c) {
return min(1.0, max(0.0, floatval(trim($c))));
}, $values);
return $cache[$color] = self::getArray($values);
}
return null;
}
/**
* @param $color
* @param float $alpha
* @return array
*/
static function getArray($color, $alpha = 1.0)
{
$c = array(null, null, null, null, "alpha" => $alpha, "hex" => null);
if (is_array($color)) {
$c = $color;
$c["c"] = $c[0];
$c["m"] = $c[1];
$c["y"] = $c[2];
$c["k"] = $c[3];
$c["alpha"] = $alpha;
$c["hex"] = "cmyk($c[0],$c[1],$c[2],$c[3])";
} else {
$c[0] = hexdec(mb_substr($color, 0, 2)) / 0xff;
$c[1] = hexdec(mb_substr($color, 2, 2)) / 0xff;
$c[2] = hexdec(mb_substr($color, 4, 2)) / 0xff;
$c["r"] = $c[0];
$c["g"] = $c[1];
$c["b"] = $c[2];
$c["alpha"] = $alpha;
$c["hex"] = sprintf("#%s%02X", mb_substr($color, 0, 6), round($alpha * 255));
}
return $c;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Exception;
use Dompdf\Exception;
/**
* Image exception thrown by DOMPDF
*
* @package dompdf
*/
class ImageException extends Exception
{
/**
* Class constructor
*
* @param string $message Error message
* @param int $code Error code
*/
function __construct($message = null, $code = 0)
{
parent::__construct($message, $code);
}
}
@@ -0,0 +1,29 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
/**
* Standard exception thrown by DOMPDF classes
*
* @package dompdf
*/
class Exception extends \Exception
{
/**
* Class constructor
*
* @param string $message Error message
* @param int $code Error code
*/
public function __construct($message = null, $code = 0)
{
parent::__construct($message, $code);
}
}
@@ -0,0 +1,534 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Helmut Tischer <htischer@weihenstephan.org>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
use FontLib\Font;
/**
* The font metrics class
*
* This class provides information about fonts and text. It can resolve
* font names into actual installed font files, as well as determine the
* size of text in a particular font and size.
*
* @static
* @package dompdf
*/
class FontMetrics
{
/**
* Name of the font cache file
*
* This file must be writable by the webserver process only to update it
* with save_font_families() after adding the .afm file references of a new font family
* with FontMetrics::saveFontFamilies().
* This is typically done only from command line with load_font.php on converting
* ttf fonts to ufm with php-font-lib.
*/
const CACHE_FILE = "dompdf_font_family_cache.php";
/**
* @var Canvas
* @deprecated
*/
protected $pdf;
/**
* Underlying {@link Canvas} object to perform text size calculations
*
* @var Canvas
*/
protected $canvas;
/**
* Array of font family names to font files
*
* Usually cached by the {@link load_font.php} script
*
* @var array
*/
protected $fontLookup = array();
/**
* @var Options
*/
private $options;
/**
* Class initialization
*/
public function __construct(Canvas $canvas, Options $options)
{
$this->setCanvas($canvas);
$this->setOptions($options);
$this->loadFontFamilies();
}
/**
* @deprecated
*/
public function save_font_families()
{
$this->saveFontFamilies();
}
/**
* Saves the stored font family cache
*
* The name and location of the cache file are determined by {@link
* FontMetrics::CACHE_FILE}. This file should be writable by the
* webserver process.
*
* @see Font_Metrics::load_font_families()
*/
public function saveFontFamilies()
{
// replace the path to the DOMPDF font directories with the corresponding constants (allows for more portability)
$cacheData = sprintf("<?php return array (%s", PHP_EOL);
foreach ($this->fontLookup as $family => $variants) {
$cacheData .= sprintf(" '%s' => array(%s", addslashes($family), PHP_EOL);
foreach ($variants as $variant => $path) {
$path = sprintf("'%s'", $path);
$path = str_replace('\'' . $this->getOptions()->getFontDir() , '$fontDir . \'' , $path);
$path = str_replace('\'' . $this->getOptions()->getRootDir() , '$rootDir . \'' , $path);
$cacheData .= sprintf(" '%s' => %s,%s", $variant, $path, PHP_EOL);
}
$cacheData .= sprintf(" ),%s", PHP_EOL);
}
$cacheData .= ") ?>";
file_put_contents($this->getCacheFile(), $cacheData);
}
/**
* @deprecated
*/
public function load_font_families()
{
$this->loadFontFamilies();
}
/**
* Loads the stored font family cache
*
* @see save_font_families()
*/
public function loadFontFamilies()
{
$fontDir = $this->getOptions()->getFontDir();
$rootDir = $this->getOptions()->getRootDir();
// FIXME: tempoarary define constants for cache files <= v0.6.2
if (!defined("DOMPDF_DIR")) { define("DOMPDF_DIR", $rootDir); }
if (!defined("DOMPDF_FONT_DIR")) { define("DOMPDF_FONT_DIR", $fontDir); }
$file = $rootDir . "/lib/fonts/dompdf_font_family_cache.dist.php";
$distFonts = require $file;
if (!is_readable($this->getCacheFile())) {
$this->fontLookup = $distFonts;
return;
}
$cacheData = require $this->getCacheFile();
$this->fontLookup = array();
if (is_array($this->fontLookup)) {
foreach ($cacheData as $key => $value) {
$this->fontLookup[stripslashes($key)] = $value;
}
}
// Merge provided fonts
$this->fontLookup += $distFonts;
}
/**
* @param array $style
* @param string $remote_file
* @param resource $context
* @return bool
* @deprecated
*/
public function register_font($style, $remote_file, $context = null)
{
return $this->registerFont($style, $remote_file);
}
/**
* @param array $style
* @param string $remoteFile
* @param resource $context
* @return bool
*/
public function registerFont($style, $remoteFile, $context = null)
{
$fontDir = $this->getOptions()->getFontDir();
$fontname = mb_strtolower($style["family"]);
$families = $this->getFontFamilies();
$entry = array();
if (isset($families[$fontname])) {
$entry = $families[$fontname];
}
$localFile = $fontDir . DIRECTORY_SEPARATOR . md5($remoteFile);
$localTempFile = $this->options->get('tempDir') . "/" . md5($remoteFile);
$cacheEntry = $localFile;
$localFile .= ".".strtolower(pathinfo($remoteFile,PATHINFO_EXTENSION));
$styleString = $this->getType("{$style['weight']} {$style['style']}");
if ( !isset($entry[$styleString]) ) {
$entry[$styleString] = $cacheEntry;
// Download the remote file
list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
if (false === $remoteFileContent) {
return false;
}
file_put_contents($localTempFile, $remoteFileContent);
$font = Font::load($localTempFile);
if (!$font) {
unlink($localTempFile);
return false;
}
$font->parse();
$font->saveAdobeFontMetrics("$cacheEntry.ufm");
$font->close();
unlink($localTempFile);
if ( !file_exists("$cacheEntry.ufm") ) {
return false;
}
// Save the changes
file_put_contents($localFile, $remoteFileContent);
if ( !file_exists($localFile) ) {
unlink("$cacheEntry.ufm");
return false;
}
$this->setFontFamily($fontname, $entry);
$this->saveFontFamilies();
}
return true;
}
/**
* @param $text
* @param $font
* @param $size
* @param float $word_spacing
* @param float $char_spacing
* @return float
* @deprecated
*/
public function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0)
{
//return self::$_pdf->get_text_width($text, $font, $size, $word_spacing, $char_spacing);
return $this->getTextWidth($text, $font, $size, $word_spacing, $char_spacing);
}
/**
* Calculates text size, in points
*
* @param string $text the text to be sized
* @param string $font the desired font
* @param float $size the desired font size
* @param float $wordSpacing
* @param float $charSpacing
*
* @internal param float $spacing word spacing, if any
* @return float
*/
public function getTextWidth($text, $font, $size, $wordSpacing = 0.0, $charSpacing = 0.0)
{
// @todo Make sure this cache is efficient before enabling it
static $cache = array();
if ($text === "") {
return 0;
}
// Don't cache long strings
$useCache = !isset($text[50]); // Faster than strlen
$key = "$font/$size/$wordSpacing/$charSpacing";
if ($useCache && isset($cache[$key][$text])) {
return $cache[$key]["$text"];
}
$width = $this->getCanvas()->get_text_width($text, $font, $size, $wordSpacing, $charSpacing);
if ($useCache) {
$cache[$key][$text] = $width;
}
return $width;
}
/**
* @param $font
* @param $size
* @return float
* @deprecated
*/
public function get_font_height($font, $size)
{
return $this->getFontHeight($font, $size);
}
/**
* Calculates font height
*
* @param string $font
* @param float $size
*
* @return float
*/
public function getFontHeight($font, $size)
{
return $this->getCanvas()->get_font_height($font, $size);
}
/**
* @param $family_raw
* @param string $subtype_raw
* @return string
* @deprecated
*/
public function get_font($family_raw, $subtype_raw = "normal")
{
return $this->getFont($family_raw, $subtype_raw);
}
/**
* Resolves a font family & subtype into an actual font file
* Subtype can be one of 'normal', 'bold', 'italic' or 'bold_italic'. If
* the particular font family has no suitable font file, the default font
* ({@link Options::defaultFont}) is used. The font file returned
* is the absolute pathname to the font file on the system.
*
* @param string $familyRaw
* @param string $subtypeRaw
*
* @return string
*/
public function getFont($familyRaw, $subtypeRaw = "normal")
{
static $cache = array();
if (isset($cache[$familyRaw][$subtypeRaw])) {
return $cache[$familyRaw][$subtypeRaw];
}
/* Allow calling for various fonts in search path. Therefore not immediately
* return replacement on non match.
* Only when called with NULL try replacement.
* When this is also missing there is really trouble.
* If only the subtype fails, nevertheless return failure.
* Only on checking the fallback font, check various subtypes on same font.
*/
$subtype = strtolower($subtypeRaw);
if ($familyRaw) {
$family = str_replace(array("'", '"'), "", strtolower($familyRaw));
if (isset($this->fontLookup[$family][$subtype])) {
return $cache[$familyRaw][$subtypeRaw] = $this->fontLookup[$family][$subtype];
}
return null;
}
$family = "serif";
if (isset($this->fontLookup[$family][$subtype])) {
return $cache[$familyRaw][$subtypeRaw] = $this->fontLookup[$family][$subtype];
}
if (!isset($this->fontLookup[$family])) {
return null;
}
$family = $this->fontLookup[$family];
foreach ($family as $sub => $font) {
if (strpos($subtype, $sub) !== false) {
return $cache[$familyRaw][$subtypeRaw] = $font;
}
}
if ($subtype !== "normal") {
foreach ($family as $sub => $font) {
if ($sub !== "normal") {
return $cache[$familyRaw][$subtypeRaw] = $font;
}
}
}
$subtype = "normal";
if (isset($family[$subtype])) {
return $cache[$familyRaw][$subtypeRaw] = $family[$subtype];
}
return null;
}
/**
* @param $family
* @return null|string
* @deprecated
*/
public function get_family($family)
{
return $this->getFamily($family);
}
/**
* @param string $family
* @return null|string
*/
public function getFamily($family)
{
$family = str_replace(array("'", '"'), "", mb_strtolower($family));
if (isset($this->fontLookup[$family])) {
return $this->fontLookup[$family];
}
return null;
}
/**
* @param $type
* @return string
* @deprecated
*/
public function get_type($type)
{
return $this->getType($type);
}
/**
* @param string $type
* @return string
*/
public function getType($type)
{
if (preg_match("/bold/i", $type)) {
if (preg_match("/italic|oblique/i", $type)) {
$type = "bold_italic";
} else {
$type = "bold";
}
} elseif (preg_match("/italic|oblique/i", $type)) {
$type = "italic";
} else {
$type = "normal";
}
return $type;
}
/**
* @return array
* @deprecated
*/
public function get_font_families()
{
return $this->getFontFamilies();
}
/**
* Returns the current font lookup table
*
* @return array
*/
public function getFontFamilies()
{
return $this->fontLookup;
}
/**
* @param string $fontname
* @param mixed $entry
* @deprecated
*/
public function set_font_family($fontname, $entry)
{
$this->setFontFamily($fontname, $entry);
}
/**
* @param string $fontname
* @param mixed $entry
*/
public function setFontFamily($fontname, $entry)
{
$this->fontLookup[mb_strtolower($fontname)] = $entry;
}
/**
* @return string
*/
public function getCacheFile()
{
return $this->getOptions()->getFontDir() . DIRECTORY_SEPARATOR . self::CACHE_FILE;
}
/**
* @param Options $options
* @return $this
*/
public function setOptions(Options $options)
{
$this->options = $options;
return $this;
}
/**
* @return Options
*/
public function getOptions()
{
return $this->options;
}
/**
* @param Canvas $canvas
* @return $this
*/
public function setCanvas(Canvas $canvas)
{
$this->canvas = $canvas;
// Still write deprecated pdf for now. It might be used by a parent class.
$this->pdf = $canvas;
return $this;
}
/**
* @return Canvas
*/
public function getCanvas()
{
return $this->canvas;
}
}
@@ -0,0 +1,287 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Frame;
use Dompdf\Css\Style;
use Dompdf\Dompdf;
use Dompdf\Exception;
use Dompdf\Frame;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use DOMXPath;
use Dompdf\FrameDecorator\Page as PageFrameDecorator;
use Dompdf\FrameReflower\Page as PageFrameReflower;
use Dompdf\Positioner\AbstractPositioner;
/**
* Contains frame decorating logic
*
* This class is responsible for assigning the correct {@link AbstractFrameDecorator},
* {@link AbstractPositioner}, and {@link AbstractFrameReflower} objects to {@link Frame}
* objects. This is determined primarily by the Frame's display type, but
* also by the Frame's node's type (e.g. DomElement vs. #text)
*
* @access private
* @package dompdf
*/
class Factory
{
/**
* Array of positioners for specific frame types
*
* @var AbstractPositioner[]
*/
protected static $_positioners;
/**
* Decorate the root Frame
*
* @param $root Frame The frame to decorate
* @param $dompdf Dompdf The dompdf instance
*
* @return PageFrameDecorator
*/
static function decorate_root(Frame $root, Dompdf $dompdf)
{
$frame = new PageFrameDecorator($root, $dompdf);
$frame->set_reflower(new PageFrameReflower($frame));
$root->set_decorator($frame);
return $frame;
}
/**
* Decorate a Frame
*
* @param Frame $frame The frame to decorate
* @param Dompdf $dompdf The dompdf instance
* @param Frame $root The frame to decorate
*
* @throws Exception
* @return AbstractFrameDecorator
* FIXME: this is admittedly a little smelly...
*/
static function decorate_frame(Frame $frame, Dompdf $dompdf, Frame $root = null)
{
if (is_null($dompdf)) {
throw new Exception("The DOMPDF argument is required");
}
$style = $frame->get_style();
// Floating (and more generally out-of-flow) elements are blocks
// http://coding.smashingmagazine.com/2007/05/01/css-float-theory-things-you-should-know/
if (!$frame->is_in_flow() && in_array($style->display, Style::$INLINE_TYPES)) {
$style->display = "block";
}
$display = $style->display;
switch ($display) {
case "flex": //FIXME: display type not yet supported
case "table-caption": //FIXME: display type not yet supported
case "block":
$positioner = "Block";
$decorator = "Block";
$reflower = "Block";
break;
case "inline-flex": //FIXME: display type not yet supported
case "inline-block":
$positioner = "Inline";
$decorator = "Block";
$reflower = "Block";
break;
case "inline":
$positioner = "Inline";
if ($frame->is_text_node()) {
$decorator = "Text";
$reflower = "Text";
} else {
if ($style->float !== "none") {
$decorator = "Block";
$reflower = "Block";
} else {
$decorator = "Inline";
$reflower = "Inline";
}
}
break;
case "table":
$positioner = "Block";
$decorator = "Table";
$reflower = "Table";
break;
case "inline-table":
$positioner = "Inline";
$decorator = "Table";
$reflower = "Table";
break;
case "table-row-group":
case "table-header-group":
case "table-footer-group":
$positioner = "NullPositioner";
$decorator = "TableRowGroup";
$reflower = "TableRowGroup";
break;
case "table-row":
$positioner = "NullPositioner";
$decorator = "TableRow";
$reflower = "TableRow";
break;
case "table-cell":
$positioner = "TableCell";
$decorator = "TableCell";
$reflower = "TableCell";
break;
case "list-item":
$positioner = "Block";
$decorator = "Block";
$reflower = "Block";
break;
case "-dompdf-list-bullet":
if ($style->list_style_position === "inside") {
$positioner = "Inline";
} else {
$positioner = "ListBullet";
}
if ($style->list_style_image !== "none") {
$decorator = "ListBulletImage";
} else {
$decorator = "ListBullet";
}
$reflower = "ListBullet";
break;
case "-dompdf-image":
$positioner = "Inline";
$decorator = "Image";
$reflower = "Image";
break;
case "-dompdf-br":
$positioner = "Inline";
$decorator = "Inline";
$reflower = "Inline";
break;
default:
// FIXME: should throw some sort of warning or something?
case "none":
if ($style->_dompdf_keep !== "yes") {
// Remove the node and the frame
$frame->get_parent()->remove_child($frame);
return;
}
$positioner = "NullPositioner";
$decorator = "NullFrameDecorator";
$reflower = "NullFrameReflower";
break;
}
// Handle CSS position
$position = $style->position;
if ($position === "absolute") {
$positioner = "Absolute";
} else {
if ($position === "fixed") {
$positioner = "Fixed";
}
}
$node = $frame->get_node();
// Handle nodeName
if ($node->nodeName === "img") {
$style->display = "-dompdf-image";
$decorator = "Image";
$reflower = "Image";
}
$decorator = "Dompdf\\FrameDecorator\\$decorator";
$reflower = "Dompdf\\FrameReflower\\$reflower";
/** @var AbstractFrameDecorator $deco */
$deco = new $decorator($frame, $dompdf);
$deco->set_positioner(self::getPositionerInstance($positioner));
$deco->set_reflower(new $reflower($deco, $dompdf->getFontMetrics()));
if ($root) {
$deco->set_root($root);
}
if ($display === "list-item") {
// Insert a list-bullet frame
$xml = $dompdf->getDom();
$bullet_node = $xml->createElement("bullet"); // arbitrary choice
$b_f = new Frame($bullet_node);
$node = $frame->get_node();
$parent_node = $node->parentNode;
if ($parent_node) {
if (!$parent_node->hasAttribute("dompdf-children-count")) {
$xpath = new DOMXPath($xml);
$count = $xpath->query("li", $parent_node)->length;
$parent_node->setAttribute("dompdf-children-count", $count);
}
if (is_numeric($node->getAttribute("value"))) {
$index = intval($node->getAttribute("value"));
} else {
if (!$parent_node->hasAttribute("dompdf-counter")) {
$index = ($parent_node->hasAttribute("start") ? $parent_node->getAttribute("start") : 1);
} else {
$index = (int)$parent_node->getAttribute("dompdf-counter") + 1;
}
}
$parent_node->setAttribute("dompdf-counter", $index);
$bullet_node->setAttribute("dompdf-counter", $index);
}
$new_style = $dompdf->getCss()->create_style();
$new_style->display = "-dompdf-list-bullet";
$new_style->inherit($style);
$b_f->set_style($new_style);
$deco->prepend_child(Factory::decorate_frame($b_f, $dompdf, $root));
}
return $deco;
}
/**
* Creates Positioners
*
* @param string $type type of positioner to use
* @return AbstractPositioner
*/
protected static function getPositionerInstance($type)
{
if (!isset(self::$_positioners[$type])) {
$class = '\\Dompdf\\Positioner\\'.$type;
self::$_positioners[$type] = new $class();
}
return self::$_positioners[$type];
}
}
@@ -0,0 +1,91 @@
<?php
namespace Dompdf\Frame;
use Iterator;
use Dompdf\Frame;
/**
* Linked-list Iterator
*
* Returns children in order and allows for list to change during iteration,
* provided the changes occur to or after the current element
*
* @access private
* @package dompdf
*/
class FrameListIterator implements Iterator
{
/**
* @var Frame
*/
protected $_parent;
/**
* @var Frame
*/
protected $_cur;
/**
* @var int
*/
protected $_num;
/**
* @param Frame $frame
*/
public function __construct(Frame $frame)
{
$this->_parent = $frame;
$this->_cur = $frame->get_first_child();
$this->_num = 0;
}
/**
*
*/
public function rewind()
{
$this->_cur = $this->_parent->get_first_child();
$this->_num = 0;
}
/**
* @return bool
*/
public function valid()
{
return isset($this->_cur); // && ($this->_cur->get_prev_sibling() === $this->_prev);
}
/**
* @return int
*/
public function key()
{
return $this->_num;
}
/**
* @return Frame
*/
public function current()
{
return $this->_cur;
}
/**
* @return Frame
*/
public function next()
{
$ret = $this->_cur;
if (!$ret) {
return null;
}
$this->_cur = $this->_cur->get_next_sibling();
$this->_num++;
return $ret;
}
}
@@ -0,0 +1,35 @@
<?php
namespace Dompdf\Frame;
use Dompdf\Frame;
use IteratorAggregate;
/**
* Linked-list IteratorAggregate
*
* @access private
* @package dompdf
*/
class FrameList implements IteratorAggregate
{
/**
* @var Frame
*/
protected $_frame;
/**
* @param Frame $frame
*/
function __construct($frame)
{
$this->_frame = $frame;
}
/**
* @return FrameListIterator
*/
function getIterator()
{
return new FrameListIterator($this->_frame);
}
}
@@ -0,0 +1,96 @@
<?php
namespace Dompdf\Frame;
use Iterator;
use Dompdf\Frame;
/**
* Pre-order Iterator
*
* Returns frames in preorder traversal order (parent then children)
*
* @access private
* @package dompdf
*/
class FrameTreeIterator implements Iterator
{
/**
* @var Frame
*/
protected $_root;
/**
* @var array
*/
protected $_stack = array();
/**
* @var int
*/
protected $_num;
/**
* @param Frame $root
*/
public function __construct(Frame $root)
{
$this->_stack[] = $this->_root = $root;
$this->_num = 0;
}
/**
*
*/
public function rewind()
{
$this->_stack = array($this->_root);
$this->_num = 0;
}
/**
* @return bool
*/
public function valid()
{
return count($this->_stack) > 0;
}
/**
* @return int
*/
public function key()
{
return $this->_num;
}
/**
* @return Frame
*/
public function current()
{
return end($this->_stack);
}
/**
* @return Frame
*/
public function next()
{
$b = end($this->_stack);
// Pop last element
unset($this->_stack[key($this->_stack)]);
$this->_num++;
// Push all children onto the stack in reverse order
if ($c = $b->get_last_child()) {
$this->_stack[] = $c;
while ($c = $c->get_prev_sibling()) {
$this->_stack[] = $c;
}
}
return $b;
}
}
@@ -0,0 +1,35 @@
<?php
namespace Dompdf\Frame;
use IteratorAggregate;
use Dompdf\Frame;
/**
* Pre-order IteratorAggregate
*
* @access private
* @package dompdf
*/
class FrameTreeList implements IteratorAggregate
{
/**
* @var \Dompdf\Frame
*/
protected $_root;
/**
* @param \Dompdf\Frame $root
*/
public function __construct(Frame $root)
{
$this->_root = $root;
}
/**
* @return FrameTreeIterator
*/
public function getIterator()
{
return new FrameTreeIterator($this->_root);
}
}
@@ -0,0 +1,316 @@
<?php
namespace Dompdf\Frame;
use DOMDocument;
use DOMNode;
use DOMElement;
use DOMXPath;
use Dompdf\Exception;
use Dompdf\Frame;
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
/**
* Represents an entire document as a tree of frames
*
* The FrameTree consists of {@link Frame} objects each tied to specific
* DOMNode objects in a specific DomDocument. The FrameTree has the same
* structure as the DomDocument, but adds additional capabalities for
* styling and layout.
*
* @package dompdf
*/
class FrameTree
{
/**
* Tags to ignore while parsing the tree
*
* @var array
*/
protected static $HIDDEN_TAGS = array(
"area",
"base",
"basefont",
"head",
"style",
"meta",
"title",
"colgroup",
"noembed",
"param",
"#comment"
);
/**
* The main DomDocument
*
* @see http://ca2.php.net/manual/en/ref.dom.php
* @var DOMDocument
*/
protected $_dom;
/**
* The root node of the FrameTree.
*
* @var Frame
*/
protected $_root;
/**
* Subtrees of absolutely positioned elements
*
* @var array of Frames
*/
protected $_absolute_frames;
/**
* A mapping of {@link Frame} objects to DOMNode objects
*
* @var array
*/
protected $_registry;
/**
* Class constructor
*
* @param DOMDocument $dom the main DomDocument object representing the current html document
*/
public function __construct(DomDocument $dom)
{
$this->_dom = $dom;
$this->_root = null;
$this->_registry = array();
}
/**
* Returns the DOMDocument object representing the curent html document
*
* @return DOMDocument
*/
public function get_dom()
{
return $this->_dom;
}
/**
* Returns the root frame of the tree
*
* @return Frame
*/
public function get_root()
{
return $this->_root;
}
/**
* Returns a specific frame given its id
*
* @param string $id
*
* @return Frame|null
*/
public function get_frame($id)
{
return isset($this->_registry[$id]) ? $this->_registry[$id] : null;
}
/**
* Returns a post-order iterator for all frames in the tree
*
* @return FrameTreeList|Frame[]
*/
public function get_frames()
{
return new FrameTreeList($this->_root);
}
/**
* Builds the tree
*/
public function build_tree()
{
$html = $this->_dom->getElementsByTagName("html")->item(0);
if (is_null($html)) {
$html = $this->_dom->firstChild;
}
if (is_null($html)) {
throw new Exception("Requested HTML document contains no data.");
}
$this->fix_tables();
$this->_root = $this->_build_tree_r($html);
}
/**
* Adds missing TBODYs around TR
*/
protected function fix_tables()
{
$xp = new DOMXPath($this->_dom);
// Move table caption before the table
// FIXME find a better way to deal with it...
$captions = $xp->query('//table/caption');
foreach ($captions as $caption) {
$table = $caption->parentNode;
$table->parentNode->insertBefore($caption, $table);
}
$firstRows = $xp->query('//table/tr[1]');
/** @var DOMElement $tableChild */
foreach ($firstRows as $tableChild) {
$tbody = $this->_dom->createElement('tbody');
$tableNode = $tableChild->parentNode;
do {
if ($tableChild->nodeName === 'tr') {
$tmpNode = $tableChild;
$tableChild = $tableChild->nextSibling;
$tableNode->removeChild($tmpNode);
$tbody->appendChild($tmpNode);
} else {
if ($tbody->hasChildNodes() === true) {
$tableNode->insertBefore($tbody, $tableChild);
$tbody = $this->_dom->createElement('tbody');
}
$tableChild = $tableChild->nextSibling;
}
} while ($tableChild);
if ($tbody->hasChildNodes() === true) {
$tableNode->appendChild($tbody);
}
}
}
// FIXME: temporary hack, preferably we will improve rendering of sequential #text nodes
/**
* Remove a child from a node
*
* Remove a child from a node. If the removed node results in two
* adjacent #text nodes then combine them.
*
* @param DOMNode $node the current DOMNode being considered
* @param array $children an array of nodes that are the children of $node
* @param int $index index from the $children array of the node to remove
*/
protected function _remove_node(DOMNode $node, array &$children, $index)
{
$child = $children[$index];
$previousChild = $child->previousSibling;
$nextChild = $child->nextSibling;
$node->removeChild($child);
if (isset($previousChild, $nextChild)) {
if ($previousChild->nodeName === "#text" && $nextChild->nodeName === "#text")
{
$previousChild->nodeValue .= $nextChild->nodeValue;
$this->_remove_node($node, $children, $index+1);
}
}
array_splice($children, $index, 1);
}
/**
* Recursively adds {@link Frame} objects to the tree
*
* Recursively build a tree of Frame objects based on a dom tree.
* No layout information is calculated at this time, although the
* tree may be adjusted (i.e. nodes and frames for generated content
* and images may be created).
*
* @param DOMNode $node the current DOMNode being considered
*
* @return Frame
*/
protected function _build_tree_r(DOMNode $node)
{
$frame = new Frame($node);
$id = $frame->get_id();
$this->_registry[$id] = $frame;
if (!$node->hasChildNodes()) {
return $frame;
}
// Store the children in an array so that the tree can be modified
$children = array();
$length = $node->childNodes->length;
for ($i = 0; $i < $length; $i++) {
$children[] = $node->childNodes->item($i);
}
$index = 0;
// INFO: We don't advance $index if a node is removed to avoid skipping nodes
while ($index < count($children)) {
$child = $children[$index];
$nodeName = strtolower($child->nodeName);
// Skip non-displaying nodes
if (in_array($nodeName, self::$HIDDEN_TAGS)) {
if ($nodeName !== "head" && $nodeName !== "style") {
$this->_remove_node($node, $children, $index);
} else {
$index++;
}
continue;
}
// Skip empty text nodes
if ($nodeName === "#text" && $child->nodeValue === "") {
$this->_remove_node($node, $children, $index);
continue;
}
// Skip empty image nodes
if ($nodeName === "img" && $child->getAttribute("src") === "") {
$this->_remove_node($node, $children, $index);
continue;
}
if (is_object($child)) {
$frame->append_child($this->_build_tree_r($child), false);
}
$index++;
}
return $frame;
}
/**
* @param DOMElement $node
* @param DOMElement $new_node
* @param string $pos
*
* @return mixed
*/
public function insert_node(DOMElement $node, DOMElement $new_node, $pos)
{
if ($pos === "after" || !$node->firstChild) {
$node->appendChild($new_node);
} else {
$node->insertBefore($new_node, $node->firstChild);
}
$this->_build_tree_r($new_node);
$frame_id = $new_node->getAttribute("frame_id");
$frame = $this->get_frame($frame_id);
$parent_id = $node->getAttribute("frame_id");
$parent = $this->get_frame($parent_id);
if ($parent) {
if ($pos === "before") {
$parent->prepend_child($frame, false);
} else {
$parent->append_child($frame, false);
}
}
return $frame_id;
}
}
@@ -0,0 +1,913 @@
<?php
namespace Dompdf\FrameDecorator;
use DOMElement;
use DOMNode;
use DOMText;
use Dompdf\Helpers;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\Frame\FrameTreeList;
use Dompdf\Frame\Factory;
use Dompdf\FrameReflower\AbstractFrameReflower;
use Dompdf\Css\Style;
use Dompdf\Positioner\AbstractPositioner;
use Dompdf\Exception;
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
/**
* Base AbstractFrameDecorator class
*
* @package dompdf
*/
abstract class AbstractFrameDecorator extends Frame
{
const DEFAULT_COUNTER = "-dompdf-default-counter";
public $_counters = array(); // array([id] => counter_value) (for generated content)
/**
* The root node of the DOM tree
*
* @var Frame
*/
protected $_root;
/**
* The decorated frame
*
* @var Frame
*/
protected $_frame;
/**
* AbstractPositioner object used to position this frame (Strategy pattern)
*
* @var AbstractPositioner
*/
protected $_positioner;
/**
* Reflower object used to calculate frame dimensions (Strategy pattern)
*
* @var \Dompdf\FrameReflower\AbstractFrameReflower
*/
protected $_reflower;
/**
* Reference to the current dompdf instance
*
* @var Dompdf
*/
protected $_dompdf;
/**
* First block parent
*
* @var Block
*/
private $_block_parent;
/**
* First positionned parent (position: relative | absolute | fixed)
*
* @var AbstractFrameDecorator
*/
private $_positionned_parent;
/**
* Cache for the get_parent wehile loop results
*
* @var Frame
*/
private $_cached_parent;
/**
* Class constructor
*
* @param Frame $frame The decoration target
* @param Dompdf $dompdf The Dompdf object
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
$this->_frame = $frame;
$this->_root = null;
$this->_dompdf = $dompdf;
$frame->set_decorator($this);
}
/**
* "Destructor": foribly free all references held by this object
*
* @param bool $recursive if true, call dispose on all children
*/
function dispose($recursive = false)
{
if ($recursive) {
while ($child = $this->get_first_child()) {
$child->dispose(true);
}
}
$this->_root = null;
unset($this->_root);
$this->_frame->dispose(true);
$this->_frame = null;
unset($this->_frame);
$this->_positioner = null;
unset($this->_positioner);
$this->_reflower = null;
unset($this->_reflower);
}
/**
* Return a copy of this frame with $node as its node
*
* @param DOMNode $node
*
* @return Frame
*/
function copy(DOMNode $node)
{
$frame = new Frame($node);
$frame->set_style(clone $this->_frame->get_original_style());
return Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
}
/**
* Create a deep copy: copy this node and all children
*
* @return Frame
*/
function deep_copy()
{
$node = $this->_frame->get_node();
if ($node instanceof DOMElement && $node->hasAttribute("id")) {
$node->setAttribute("data-dompdf-original-id", $node->getAttribute("id"));
$node->removeAttribute("id");
}
$frame = new Frame($node->cloneNode());
$frame->set_style(clone $this->_frame->get_original_style());
$deco = Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
foreach ($this->get_children() as $child) {
$deco->append_child($child->deep_copy());
}
return $deco;
}
/**
* Delegate calls to decorated frame object
*/
function reset()
{
$this->_frame->reset();
$this->_counters = array();
$this->_cached_parent = null; //clear get_parent() cache
// Reset all children
foreach ($this->get_children() as $child) {
$child->reset();
}
}
// Getters -----------
/**
* @return string
*/
function get_id()
{
return $this->_frame->get_id();
}
/**
* @return Frame
*/
function get_frame()
{
return $this->_frame;
}
/**
* @return DOMElement|DOMText
*/
function get_node()
{
return $this->_frame->get_node();
}
/**
* @return Style
*/
function get_style()
{
return $this->_frame->get_style();
}
/**
* @return Style
*/
function get_original_style()
{
return $this->_frame->get_original_style();
}
/**
* @param integer $i
*
* @return array|float
*/
function get_containing_block($i = null)
{
return $this->_frame->get_containing_block($i);
}
/**
* @param integer $i
*
* @return array|float
*/
function get_position($i = null)
{
return $this->_frame->get_position($i);
}
/**
* @return Dompdf
*/
function get_dompdf()
{
return $this->_dompdf;
}
/**
* @return float
*/
function get_margin_height()
{
return $this->_frame->get_margin_height();
}
/**
* @return float
*/
function get_margin_width()
{
return $this->_frame->get_margin_width();
}
/**
* @return array
*/
function get_content_box()
{
return $this->_frame->get_content_box();
}
/**
* @return array
*/
function get_padding_box()
{
return $this->_frame->get_padding_box();
}
/**
* @return array
*/
function get_border_box()
{
return $this->_frame->get_border_box();
}
/**
* @param integer $id
*/
function set_id($id)
{
$this->_frame->set_id($id);
}
/**
* @param Style $style
*/
function set_style(Style $style)
{
$this->_frame->set_style($style);
}
/**
* @param float $x
* @param float $y
* @param float $w
* @param float $h
*/
function set_containing_block($x = null, $y = null, $w = null, $h = null)
{
$this->_frame->set_containing_block($x, $y, $w, $h);
}
/**
* @param float $x
* @param float $y
*/
function set_position($x = null, $y = null)
{
$this->_frame->set_position($x, $y);
}
/**
* @return bool
*/
function is_auto_height()
{
return $this->_frame->is_auto_height();
}
/**
* @return bool
*/
function is_auto_width()
{
return $this->_frame->is_auto_width();
}
/**
* @return string
*/
function __toString()
{
return $this->_frame->__toString();
}
/**
* @param Frame $child
* @param bool $update_node
*/
function prepend_child(Frame $child, $update_node = true)
{
while ($child instanceof AbstractFrameDecorator) {
$child = $child->_frame;
}
$this->_frame->prepend_child($child, $update_node);
}
/**
* @param Frame $child
* @param bool $update_node
*/
function append_child(Frame $child, $update_node = true)
{
while ($child instanceof AbstractFrameDecorator) {
$child = $child->_frame;
}
$this->_frame->append_child($child, $update_node);
}
/**
* @param Frame $new_child
* @param Frame $ref
* @param bool $update_node
*/
function insert_child_before(Frame $new_child, Frame $ref, $update_node = true)
{
while ($new_child instanceof AbstractFrameDecorator) {
$new_child = $new_child->_frame;
}
if ($ref instanceof AbstractFrameDecorator) {
$ref = $ref->_frame;
}
$this->_frame->insert_child_before($new_child, $ref, $update_node);
}
/**
* @param Frame $new_child
* @param Frame $ref
* @param bool $update_node
*/
function insert_child_after(Frame $new_child, Frame $ref, $update_node = true)
{
$insert_frame = $new_child;
while ($insert_frame instanceof AbstractFrameDecorator) {
$insert_frame = $insert_frame->_frame;
}
$reference_frame = $ref;
while ($reference_frame instanceof AbstractFrameDecorator) {
$reference_frame = $reference_frame->_frame;
}
$this->_frame->insert_child_after($insert_frame, $reference_frame, $update_node);
}
/**
* @param Frame $child
* @param bool $update_node
*
* @return Frame
*/
function remove_child(Frame $child, $update_node = true)
{
while ($child instanceof AbstractFrameDecorator) {
$child = $child->_frame;
}
return $this->_frame->remove_child($child, $update_node);
}
/**
* @return AbstractFrameDecorator
*/
function get_parent($use_cache = true)
{
if ($use_cache && $this->_cached_parent) {
return $this->_cached_parent;
}
$p = $this->_frame->get_parent();
if ($p && $deco = $p->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $this->_cached_parent = $deco;
} else {
return $this->_cached_parent = $p;
}
}
/**
* @return AbstractFrameDecorator
*/
function get_first_child()
{
$c = $this->_frame->get_first_child();
if ($c && $deco = $c->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $deco;
} else {
if ($c) {
return $c;
}
}
return null;
}
/**
* @return AbstractFrameDecorator
*/
function get_last_child()
{
$c = $this->_frame->get_last_child();
if ($c && $deco = $c->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $deco;
} else {
if ($c) {
return $c;
}
}
return null;
}
/**
* @return AbstractFrameDecorator
*/
function get_prev_sibling()
{
$s = $this->_frame->get_prev_sibling();
if ($s && $deco = $s->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $deco;
} else {
if ($s) {
return $s;
}
}
return null;
}
/**
* @return AbstractFrameDecorator
*/
function get_next_sibling()
{
$s = $this->_frame->get_next_sibling();
if ($s && $deco = $s->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $deco;
} else {
if ($s) {
return $s;
}
}
return null;
}
/**
* @return FrameTreeList
*/
function get_subtree()
{
return new FrameTreeList($this);
}
function set_positioner(AbstractPositioner $posn)
{
$this->_positioner = $posn;
if ($this->_frame instanceof AbstractFrameDecorator) {
$this->_frame->set_positioner($posn);
}
}
function set_reflower(AbstractFrameReflower $reflower)
{
$this->_reflower = $reflower;
if ($this->_frame instanceof AbstractFrameDecorator) {
$this->_frame->set_reflower($reflower);
}
}
/**
* @return \Dompdf\FrameReflower\AbstractFrameReflower
*/
function get_reflower()
{
return $this->_reflower;
}
/**
* @param Frame $root
*/
function set_root(Frame $root)
{
$this->_root = $root;
if ($this->_frame instanceof AbstractFrameDecorator) {
$this->_frame->set_root($root);
}
}
/**
* @return Page
*/
function get_root()
{
return $this->_root;
}
/**
* @return Block
*/
function find_block_parent()
{
// Find our nearest block level parent
$p = $this->get_parent();
while ($p) {
if ($p->is_block()) {
break;
}
$p = $p->get_parent();
}
return $this->_block_parent = $p;
}
/**
* @return AbstractFrameDecorator
*/
function find_positionned_parent()
{
// Find our nearest relative positionned parent
$p = $this->get_parent();
while ($p) {
if ($p->is_positionned()) {
break;
}
$p = $p->get_parent();
}
if (!$p) {
$p = $this->_root->get_first_child(); // <body>
}
return $this->_positionned_parent = $p;
}
/**
* split this frame at $child.
* The current frame is cloned and $child and all children following
* $child are added to the clone. The clone is then passed to the
* current frame's parent->split() method.
*
* @param Frame $child
* @param boolean $force_pagebreak
*
* @throws Exception
* @return void
*/
function split(Frame $child = null, $force_pagebreak = false)
{
// decrement any counters that were incremented on the current node, unless that node is the body
$style = $this->_frame->get_style();
if (
$this->_frame->get_node()->nodeName !== "body" &&
$style->counter_increment &&
($decrement = $style->counter_increment) !== "none"
) {
$this->decrement_counters($decrement);
}
if (is_null($child)) {
// check for counter increment on :before content (always a child of the selected element @link AbstractFrameReflower::_set_content)
// this can push the current node to the next page before counter rules have bubbled up (but only if
// it's been rendered, thus the position check)
if (!$this->is_text_node() && $this->get_node()->hasAttribute("dompdf_before_frame_id")) {
foreach ($this->_frame->get_children() as $child) {
if (
$this->get_node()->getAttribute("dompdf_before_frame_id") == $child->get_id() &&
$child->get_position('x') !== null
) {
$style = $child->get_style();
if ($style->counter_increment && ($decrement = $style->counter_increment) !== "none") {
$this->decrement_counters($decrement);
}
}
}
}
$this->get_parent()->split($this, $force_pagebreak);
return;
}
if ($child->get_parent() !== $this) {
throw new Exception("Unable to split: frame is not a child of this one.");
}
$node = $this->_frame->get_node();
if ($node instanceof DOMElement && $node->hasAttribute("id")) {
$node->setAttribute("data-dompdf-original-id", $node->getAttribute("id"));
$node->removeAttribute("id");
}
$split = $this->copy($node->cloneNode());
$split->reset();
$split->get_original_style()->text_indent = 0;
$split->_splitted = true;
// The body's properties must be kept
if ($node->nodeName !== "body") {
// Style reset on the first and second parts
$style = $this->_frame->get_style();
$style->margin_bottom = 0;
$style->padding_bottom = 0;
$style->border_bottom = 0;
// second
$orig_style = $split->get_original_style();
$orig_style->text_indent = 0;
$orig_style->margin_top = 0;
$orig_style->padding_top = 0;
$orig_style->border_top = 0;
$orig_style->page_break_before = "auto";
}
// recalculate the float offsets after paging
$this->get_parent()->insert_child_after($split, $this);
if ($this instanceof Block) {
foreach ($this->get_line_boxes() as $index => $line_box) {
$line_box->get_float_offsets();
}
}
// Add $frame and all following siblings to the new split node
$iter = $child;
while ($iter) {
$frame = $iter;
$iter = $iter->get_next_sibling();
$frame->reset();
$frame->_parent = $split;
$split->append_child($frame);
// recalculate the float offsets
if ($frame instanceof Block) {
foreach ($frame->get_line_boxes() as $index => $line_box) {
$line_box->get_float_offsets();
}
}
}
$this->get_parent()->split($split, $force_pagebreak);
// If this node resets a counter save the current value to use when rendering on the next page
if ($style->counter_reset && ($reset = $style->counter_reset) !== "none") {
$vars = preg_split('/\s+/', trim($reset), 2);
$split->_counters['__' . $vars[0]] = $this->lookup_counter_frame($vars[0])->_counters[$vars[0]];
}
}
/**
* @param string $id
* @param int $value
*/
function reset_counter($id = self::DEFAULT_COUNTER, $value = 0)
{
$this->get_parent()->_counters[$id] = intval($value);
}
/**
* @param $counters
*/
function decrement_counters($counters)
{
foreach ($counters as $id => $increment) {
$this->increment_counter($id, intval($increment) * -1);
}
}
/**
* @param $counters
*/
function increment_counters($counters)
{
foreach ($counters as $id => $increment) {
$this->increment_counter($id, intval($increment));
}
}
/**
* @param string $id
* @param int $increment
*/
function increment_counter($id = self::DEFAULT_COUNTER, $increment = 1)
{
$counter_frame = $this->lookup_counter_frame($id);
if ($counter_frame) {
if (!isset($counter_frame->_counters[$id])) {
$counter_frame->_counters[$id] = 0;
}
$counter_frame->_counters[$id] += $increment;
}
}
/**
* @param string $id
* @return AbstractFrameDecorator|null
*/
function lookup_counter_frame($id = self::DEFAULT_COUNTER)
{
$f = $this->get_parent();
while ($f) {
if (isset($f->_counters[$id])) {
return $f;
}
$fp = $f->get_parent();
if (!$fp) {
return $f;
}
$f = $fp;
}
return null;
}
/**
* @param string $id
* @param string $type
* @return bool|string
*
* TODO: What version is the best : this one or the one in ListBullet ?
*/
function counter_value($id = self::DEFAULT_COUNTER, $type = "decimal")
{
$type = mb_strtolower($type);
if (!isset($this->_counters[$id])) {
$this->_counters[$id] = 0;
}
$value = $this->_counters[$id];
switch ($type) {
default:
case "decimal":
return $value;
case "decimal-leading-zero":
return str_pad($value, 2, "0", STR_PAD_LEFT);
case "lower-roman":
return Helpers::dec2roman($value);
case "upper-roman":
return mb_strtoupper(Helpers::dec2roman($value));
case "lower-latin":
case "lower-alpha":
return chr(($value % 26) + ord('a') - 1);
case "upper-latin":
case "upper-alpha":
return chr(($value % 26) + ord('A') - 1);
case "lower-greek":
return Helpers::unichr($value + 944);
case "upper-greek":
return Helpers::unichr($value + 912);
}
}
/**
*
*/
final function position()
{
$this->_positioner->position($this);
}
/**
* @param $offset_x
* @param $offset_y
* @param bool $ignore_self
*/
final function move($offset_x, $offset_y, $ignore_self = false)
{
$this->_positioner->move($this, $offset_x, $offset_y, $ignore_self);
}
/**
* @param Block|null $block
*/
final function reflow(Block $block = null)
{
// Uncomment this to see the frames before they're laid out, instead of
// during rendering.
//echo $this->_frame; flush();
$this->_reflower->reflow($block);
}
/**
* @return array
*/
final function get_min_max_width()
{
return $this->_reflower->get_min_max_width();
}
/**
* Determine current frame width based on contents
*
* @return float
*/
final function calculate_auto_width()
{
return $this->_reflower->calculate_auto_width();
}
}
@@ -0,0 +1,286 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\LineBox;
/**
* Decorates frames for block layout
*
* @access private
* @package dompdf
*/
class Block extends AbstractFrameDecorator
{
/**
* Current line index
*
* @var int
*/
protected $_cl;
/**
* The block's line boxes
*
* @var LineBox[]
*/
protected $_line_boxes;
/**
* Block constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
$this->_line_boxes = array(new LineBox($this));
$this->_cl = 0;
}
/**
*
*/
function reset()
{
parent::reset();
$this->_line_boxes = array(new LineBox($this));
$this->_cl = 0;
}
/**
* @return LineBox
*/
function get_current_line_box()
{
return $this->_line_boxes[$this->_cl];
}
/**
* @return integer
*/
function get_current_line_number()
{
return $this->_cl;
}
/**
* @return LineBox[]
*/
function get_line_boxes()
{
return $this->_line_boxes;
}
/**
* @param integer $line_number
* @return integer
*/
function set_current_line_number($line_number)
{
$line_boxes_count = count($this->_line_boxes);
$cl = max(min($line_number, $line_boxes_count), 0);
return ($this->_cl = $line_number);
}
/**
* @param integer $i
*/
function clear_line($i)
{
if (isset($this->_line_boxes[$i])) {
unset($this->_line_boxes[$i]);
}
}
/**
* @param Frame $frame
*/
function add_frame_to_line(Frame $frame)
{
if (!$frame->is_in_flow()) {
return;
}
$style = $frame->get_style();
$frame->set_containing_line($this->_line_boxes[$this->_cl]);
/*
// Adds a new line after a block, only if certain conditions are met
if ((($frame instanceof Inline && $frame->get_node()->nodeName !== "br") ||
$frame instanceof Text && trim($frame->get_text())) &&
($frame->get_prev_sibling() && $frame->get_prev_sibling()->get_style()->display === "block" &&
$this->_line_boxes[$this->_cl]->w > 0 )) {
$this->maximize_line_height( $style->length_in_pt($style->line_height), $frame );
$this->add_line();
// Add each child of the inline frame to the line individually
foreach ($frame->get_children() as $child)
$this->add_frame_to_line( $child );
}
else*/
// Handle inline frames (which are effectively wrappers)
if ($frame instanceof Inline) {
// Handle line breaks
if ($frame->get_node()->nodeName === "br") {
$this->maximize_line_height($style->length_in_pt($style->line_height), $frame);
$this->add_line(true);
}
return;
}
// Trim leading text if this is an empty line. Kinda a hack to put it here,
// but what can you do...
if ($this->get_current_line_box()->w == 0 &&
$frame->is_text_node() &&
!$frame->is_pre()
) {
$frame->set_text(ltrim($frame->get_text()));
$frame->recalculate_width();
}
$w = $frame->get_margin_width();
// FIXME: Why? Doesn't quite seem to be the correct thing to do,
// but does appear to be necessary. Hack to handle wrapped white space?
if ($w == 0 && $frame->get_node()->nodeName !== "hr") {
return;
}
// Debugging code:
/*
Helpers::pre_r("\n<h3>Adding frame to line:</h3>");
// Helpers::pre_r("Me: " . $this->get_node()->nodeName . " (" . spl_object_hash($this->get_node()) . ")");
// Helpers::pre_r("Node: " . $frame->get_node()->nodeName . " (" . spl_object_hash($frame->get_node()) . ")");
if ( $frame->is_text_node() )
Helpers::pre_r('"'.$frame->get_node()->nodeValue.'"');
Helpers::pre_r("Line width: " . $this->_line_boxes[$this->_cl]->w);
Helpers::pre_r("Frame: " . get_class($frame));
Helpers::pre_r("Frame width: " . $w);
Helpers::pre_r("Frame height: " . $frame->get_margin_height());
Helpers::pre_r("Containing block width: " . $this->get_containing_block("w"));
*/
// End debugging
$line = $this->_line_boxes[$this->_cl];
if ($line->left + $line->w + $line->right + $w > $this->get_containing_block("w")) {
$this->add_line();
}
$frame->position();
$current_line = $this->_line_boxes[$this->_cl];
$current_line->add_frame($frame);
if ($frame->is_text_node()) {
$current_line->wc += count(preg_split("/\s+/", trim($frame->get_text())));
}
$this->increase_line_width($w);
$this->maximize_line_height($frame->get_margin_height(), $frame);
}
/**
* @param Frame $frame
*/
function remove_frames_from_line(Frame $frame)
{
// Search backwards through the lines for $frame
$i = $this->_cl;
$j = null;
while ($i >= 0) {
if (($j = in_array($frame, $this->_line_boxes[$i]->get_frames(), true)) !== false) {
break;
}
$i--;
}
if ($j === false) {
return;
}
// Remove $frame and all frames that follow
while ($j < count($this->_line_boxes[$i]->get_frames())) {
$frames = $this->_line_boxes[$i]->get_frames();
$f = $frames[$j];
$frames[$j] = null;
unset($frames[$j]);
$j++;
$this->_line_boxes[$i]->w -= $f->get_margin_width();
}
// Recalculate the height of the line
$h = 0;
foreach ($this->_line_boxes[$i]->get_frames() as $f) {
$h = max($h, $f->get_margin_height());
}
$this->_line_boxes[$i]->h = $h;
// Remove all lines that follow
while ($this->_cl > $i) {
$this->_line_boxes[$this->_cl] = null;
unset($this->_line_boxes[$this->_cl]);
$this->_cl--;
}
}
/**
* @param float $w
*/
function increase_line_width($w)
{
$this->_line_boxes[$this->_cl]->w += $w;
}
/**
* @param $val
* @param Frame $frame
*/
function maximize_line_height($val, Frame $frame)
{
if ($val > $this->_line_boxes[$this->_cl]->h) {
$this->_line_boxes[$this->_cl]->tallest_frame = $frame;
$this->_line_boxes[$this->_cl]->h = $val;
}
}
/**
* @param bool $br
*/
function add_line($br = false)
{
// if ( $this->_line_boxes[$this->_cl]["h"] == 0 ) //count($this->_line_boxes[$i]["frames"]) == 0 ||
// return;
$this->_line_boxes[$this->_cl]->br = $br;
$y = $this->_line_boxes[$this->_cl]->y + $this->_line_boxes[$this->_cl]->h;
$new_line = new LineBox($this, $y);
$this->_line_boxes[++$this->_cl] = $new_line;
}
//........................................................................
}
@@ -0,0 +1,90 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\FontMetrics;
use Dompdf\Image\Cache;
/**
* Decorates frames for image layout and rendering
*
* @package dompdf
*/
class Image extends AbstractFrameDecorator
{
/**
* The path to the image file (note that remote images are
* downloaded locally to Options:tempDir).
*
* @var string
*/
protected $_image_url;
/**
* The image's file error message
*
* @var string
*/
protected $_image_msg;
/**
* Class constructor
*
* @param Frame $frame the frame to decorate
* @param DOMPDF $dompdf the document's dompdf object (required to resolve relative & remote urls)
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
$url = $frame->get_node()->getAttribute("src");
$debug_png = $dompdf->getOptions()->getDebugPng();
if ($debug_png) print '[__construct ' . $url . ']';
list($this->_image_url, /*$type*/, $this->_image_msg) = Cache::resolve_url(
$url,
$dompdf->getProtocol(),
$dompdf->getBaseHost(),
$dompdf->getBasePath(),
$dompdf
);
if (Cache::is_broken($this->_image_url) &&
$alt = $frame->get_node()->getAttribute("alt")
) {
$style = $frame->get_style();
$style->width = (4 / 3) * $dompdf->getFontMetrics()->getTextWidth($alt, $style->font_family, $style->font_size, $style->word_spacing);
$style->height = $dompdf->getFontMetrics()->getFontHeight($style->font_family, $style->font_size);
}
}
/**
* Return the image's url
*
* @return string The url of this image
*/
function get_image_url()
{
return $this->_image_url;
}
/**
* Return the image's error message
*
* @return string The image's error message
*/
function get_image_msg()
{
return $this->_image_msg;
}
}
@@ -0,0 +1,107 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Helmut Tischer <htischer@weihenstephan.org>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use DOMElement;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\Exception;
/**
* Decorates frames for inline layout
*
* @access private
* @package dompdf
*/
class Inline extends AbstractFrameDecorator
{
/**
* Inline constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
}
/**
* @param Frame|null $frame
* @param bool $force_pagebreak
* @throws Exception
*/
function split(Frame $frame = null, $force_pagebreak = false)
{
if (is_null($frame)) {
$this->get_parent()->split($this, $force_pagebreak);
return;
}
if ($frame->get_parent() !== $this) {
throw new Exception("Unable to split: frame is not a child of this one.");
}
$node = $this->_frame->get_node();
if ($node instanceof DOMElement && $node->hasAttribute("id")) {
$node->setAttribute("data-dompdf-original-id", $node->getAttribute("id"));
$node->removeAttribute("id");
}
$split = $this->copy($node->cloneNode());
// if this is a generated node don't propagate the content style
if ($split->get_node()->nodeName == "dompdf_generated") {
$split->get_style()->content = "normal";
}
$this->get_parent()->insert_child_after($split, $this);
// Unset the current node's right style properties
$style = $this->_frame->get_style();
$style->margin_right = 0;
$style->padding_right = 0;
$style->border_right_width = 0;
// Unset the split node's left style properties since we don't want them
// to propagate
$style = $split->get_style();
$style->margin_left = 0;
$style->padding_left = 0;
$style->border_left_width = 0;
//On continuation of inline element on next line,
//don't repeat non-vertically repeatble background images
//See e.g. in testcase image_variants, long desriptions
if (($url = $style->background_image) && $url !== "none"
&& ($repeat = $style->background_repeat) && $repeat !== "repeat" && $repeat !== "repeat-y"
) {
$style->background_image = "none";
}
// Add $frame and all following siblings to the new split node
$iter = $frame;
while ($iter) {
$frame = $iter;
$iter = $iter->get_next_sibling();
$frame->reset();
$split->append_child($frame);
}
$page_breaks = array("always", "left", "right");
$frame_style = $frame->get_style();
if ($force_pagebreak ||
in_array($frame_style->page_break_before, $page_breaks) ||
in_array($frame_style->page_break_after, $page_breaks)
) {
$this->get_parent()->split($split, true);
}
}
}
@@ -0,0 +1,155 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Helmut Tischer <htischer@weihenstephan.org>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\Helpers;
/**
* Decorates frames for list bullets with custom images
*
* @package dompdf
*/
class ListBulletImage extends AbstractFrameDecorator
{
/**
* The underlying image frame
*
* @var Image
*/
protected $_img;
/**
* The image's width in pixels
*
* @var int
*/
protected $_width;
/**
* The image's height in pixels
*
* @var int
*/
protected $_height;
/**
* Class constructor
*
* @param Frame $frame the bullet frame to decorate
* @param Dompdf $dompdf the document's dompdf object
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
$style = $frame->get_style();
$url = $style->list_style_image;
$frame->get_node()->setAttribute("src", $url);
$this->_img = new Image($frame, $dompdf);
parent::__construct($this->_img, $dompdf);
list($width, $height) = Helpers::dompdf_getimagesize($this->_img->get_image_url(), $dompdf->getHttpContext());
// Resample the bullet image to be consistent with 'auto' sized images
// See also Image::get_min_max_width
// Tested php ver: value measured in px, suffix "px" not in value: rtrim unnecessary.
$dpi = $this->_dompdf->getOptions()->getDpi();
$this->_width = ((float)rtrim($width, "px") * 72) / $dpi;
$this->_height = ((float)rtrim($height, "px") * 72) / $dpi;
//If an image is taller as the containing block/box, the box should be extended.
//Neighbour elements are overwriting the overlapping image areas.
//Todo: Where can the box size be extended?
//Code below has no effect.
//See block_frame_reflower _calculate_restricted_height
//See generated_frame_reflower, Dompdf:render() "list-item", "-dompdf-list-bullet"S.
//Leave for now
//if ($style->min_height < $this->_height ) {
// $style->min_height = $this->_height;
//}
//$style->height = "auto";
}
/**
* Return the bullet's width
*
* @return int
*/
function get_width()
{
//ignore image width, use same width as on predefined bullet ListBullet
//for proper alignment of bullet image and text. Allow image to not fitting on left border.
//This controls the distance between bullet image and text
//return $this->_width;
return $this->_frame->get_style()->get_font_size() * ListBullet::BULLET_SIZE +
2 * ListBullet::BULLET_PADDING;
}
/**
* Return the bullet's height
*
* @return int
*/
function get_height()
{
//based on image height
return $this->_height;
}
/**
* Override get_margin_width
*
* @return int
*/
function get_margin_width()
{
//ignore image width, use same width as on predefined bullet ListBullet
//for proper alignment of bullet image and text. Allow image to not fitting on left border.
//This controls the extra indentation of text to make room for the bullet image.
//Here use actual image size, not predefined bullet size
//return $this->_frame->get_style()->get_font_size()*ListBullet::BULLET_SIZE +
// 2 * ListBullet::BULLET_PADDING;
// Small hack to prevent indenting of list text
// Image Might not exist, then position like on list_bullet_frame_decorator fallback to none.
if ($this->_frame->get_style()->list_style_position === "outside" ||
$this->_width == 0
)
return 0;
//This aligns the "inside" image position with the text.
//The text starts to the right of the image.
//Between the image and the text there is an added margin of image width.
//Where this comes from is unknown.
//The corresponding ListBullet sets a smaller margin. bullet size?
return $this->_width + 2 * ListBullet::BULLET_PADDING;
}
/**
* Override get_margin_height()
*
* @return int
*/
function get_margin_height()
{
//Hits only on "inset" lists items, to increase height of box
//based on image height
return $this->_height + 2 * ListBullet::BULLET_PADDING;
}
/**
* Return image url
*
* @return string
*/
function get_image_url()
{
return $this->_img->get_image_url();
}
}
@@ -0,0 +1,91 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Helmut Tischer <htischer@weihenstephan.org>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Frame;
/**
* Decorates frames for list bullet rendering
*
* @package dompdf
*/
class ListBullet extends AbstractFrameDecorator
{
const BULLET_PADDING = 1; // Distance from bullet to text in pt
// As fraction of font size (including descent). See also DECO_THICKNESS.
const BULLET_THICKNESS = 0.04; // Thickness of bullet outline. Screen: 0.08, print: better less, e.g. 0.04
const BULLET_DESCENT = 0.3; //descent of font below baseline. Todo: Guessed for now.
const BULLET_SIZE = 0.35; // bullet diameter. For now 0.5 of font_size without descent.
static $BULLET_TYPES = array("disc", "circle", "square");
/**
* ListBullet constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
}
/**
* @return float|int
*/
function get_margin_width()
{
$style = $this->_frame->get_style();
// Small hack to prevent extra indenting of list text on list_style_position === "inside"
// and on suppressed bullet
if ($style->list_style_position === "outside" ||
$style->list_style_type === "none"
) {
return 0;
}
return $style->get_font_size() * self::BULLET_SIZE + 2 * self::BULLET_PADDING;
}
/**
* hits only on "inset" lists items, to increase height of box
*
* @return float|int
*/
function get_margin_height()
{
$style = $this->_frame->get_style();
if ($style->list_style_type === "none") {
return 0;
}
return $style->get_font_size() * self::BULLET_SIZE + 2 * self::BULLET_PADDING;
}
/**
* @return float|int
*/
function get_width()
{
return $this->get_margin_height();
}
/**
* @return float|int
*/
function get_height()
{
return $this->get_margin_height();
}
//........................................................................
}
@@ -0,0 +1,34 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Frame;
/**
* Dummy decorator
*
* @package dompdf
*/
class NullFrameDecorator extends AbstractFrameDecorator
{
/**
* NullFrameDecorator constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
$style = $this->_frame->get_style();
$style->width = 0;
$style->height = 0;
$style->margin = 0;
$style->padding = 0;
}
}
@@ -0,0 +1,660 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Css\Style;
use Dompdf\Dompdf;
use Dompdf\Helpers;
use Dompdf\Frame;
use Dompdf\Renderer;
/**
* Decorates frames for page layout
*
* @access private
* @package dompdf
*/
class Page extends AbstractFrameDecorator
{
/**
* y value of bottom page margin
*
* @var float
*/
protected $_bottom_page_margin;
/**
* Flag indicating page is full.
*
* @var bool
*/
protected $_page_full;
/**
* Number of tables currently being reflowed
*
* @var int
*/
protected $_in_table;
/**
* The pdf renderer
*
* @var Renderer
*/
protected $_renderer;
/**
* This page's floating frames
*
* @var array
*/
protected $_floating_frames = array();
//........................................................................
/**
* Class constructor
*
* @param Frame $frame the frame to decorate
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
$this->_page_full = false;
$this->_in_table = 0;
$this->_bottom_page_margin = null;
}
/**
* Set the renderer used for this pdf
*
* @param Renderer $renderer the renderer to use
*/
function set_renderer($renderer)
{
$this->_renderer = $renderer;
}
/**
* Return the renderer used for this pdf
*
* @return Renderer
*/
function get_renderer()
{
return $this->_renderer;
}
/**
* Set the frame's containing block. Overridden to set $this->_bottom_page_margin.
*
* @param float $x
* @param float $y
* @param float $w
* @param float $h
*/
function set_containing_block($x = null, $y = null, $w = null, $h = null)
{
parent::set_containing_block($x, $y, $w, $h);
//$w = $this->get_containing_block("w");
if (isset($h)) {
$this->_bottom_page_margin = $h;
} // - $this->_frame->get_style()->length_in_pt($this->_frame->get_style()->margin_bottom, $w);
}
/**
* Returns true if the page is full and is no longer accepting frames.
*
* @return bool
*/
function is_full()
{
return $this->_page_full;
}
/**
* Start a new page by resetting the full flag.
*/
function next_page()
{
$this->_floating_frames = array();
$this->_renderer->new_page();
$this->_page_full = false;
}
/**
* Indicate to the page that a table is currently being reflowed.
*/
function table_reflow_start()
{
$this->_in_table++;
}
/**
* Indicate to the page that table reflow is finished.
*/
function table_reflow_end()
{
$this->_in_table--;
}
/**
* Return whether we are currently in a nested table or not
*
* @return bool
*/
function in_nested_table()
{
return $this->_in_table > 1;
}
/**
* Check if a forced page break is required before $frame. This uses the
* frame's page_break_before property as well as the preceeding frame's
* page_break_after property.
*
* @link http://www.w3.org/TR/CSS21/page.html#forced
*
* @param Frame $frame the frame to check
*
* @return bool true if a page break occured
*/
function check_forced_page_break(Frame $frame)
{
// Skip check if page is already split
if ($this->_page_full) {
return null;
}
$block_types = array("block", "list-item", "table", "inline");
$page_breaks = array("always", "left", "right");
$style = $frame->get_style();
if (!in_array($style->display, $block_types)) {
return false;
}
// Find the previous block-level sibling
$prev = $frame->get_prev_sibling();
while ($prev && !in_array($prev->get_style()->display, $block_types)) {
$prev = $prev->get_prev_sibling();
}
if (in_array($style->page_break_before, $page_breaks)) {
// Prevent cascading splits
$frame->split(null, true);
// We have to grab the style again here because split() resets
// $frame->style to the frame's orignal style.
$frame->get_style()->page_break_before = "auto";
$this->_page_full = true;
return true;
}
if ($prev && in_array($prev->get_style()->page_break_after, $page_breaks)) {
// Prevent cascading splits
$frame->split(null, true);
$prev->get_style()->page_break_after = "auto";
$this->_page_full = true;
return true;
}
if ($prev && $prev->get_last_child() && $frame->get_node()->nodeName != "body") {
$prev_last_child = $prev->get_last_child();
if (in_array($prev_last_child->get_style()->page_break_after, $page_breaks)) {
$frame->split(null, true);
$prev_last_child->get_style()->page_break_after = "auto";
$this->_page_full = true;
return true;
}
}
return false;
}
/**
* Determine if a page break is allowed before $frame
* http://www.w3.org/TR/CSS21/page.html#allowed-page-breaks
*
* In the normal flow, page breaks can occur at the following places:
*
* 1. In the vertical margin between block boxes. When a page
* break occurs here, the used values of the relevant
* 'margin-top' and 'margin-bottom' properties are set to '0'.
* 2. Between line boxes inside a block box.
*
* These breaks are subject to the following rules:
*
* * Rule A: Breaking at (1) is allowed only if the
* 'page-break-after' and 'page-break-before' properties of
* all the elements generating boxes that meet at this margin
* allow it, which is when at least one of them has the value
* 'always', 'left', or 'right', or when all of them are
* 'auto'.
*
* * Rule B: However, if all of them are 'auto' and the
* nearest common ancestor of all the elements has a
* 'page-break-inside' value of 'avoid', then breaking here is
* not allowed.
*
* * Rule C: Breaking at (2) is allowed only if the number of
* line boxes between the break and the start of the enclosing
* block box is the value of 'orphans' or more, and the number
* of line boxes between the break and the end of the box is
* the value of 'widows' or more.
*
* * Rule D: In addition, breaking at (2) is allowed only if
* the 'page-break-inside' property is 'auto'.
*
* If the above doesn't provide enough break points to keep
* content from overflowing the page boxes, then rules B and D are
* dropped in order to find additional breakpoints.
*
* If that still does not lead to sufficient break points, rules A
* and C are dropped as well, to find still more break points.
*
* We will also allow breaks between table rows. However, when
* splitting a table, the table headers should carry over to the
* next page (but they don't yet).
*
* @param Frame $frame the frame to check
*
* @return bool true if a break is allowed, false otherwise
*/
protected function _page_break_allowed(Frame $frame)
{
$block_types = array("block", "list-item", "table", "-dompdf-image");
Helpers::dompdf_debug("page-break", "_page_break_allowed(" . $frame->get_node()->nodeName . ")");
$display = $frame->get_style()->display;
// Block Frames (1):
if (in_array($display, $block_types)) {
// Avoid breaks within table-cells
if ($this->_in_table) {
Helpers::dompdf_debug("page-break", "In table: " . $this->_in_table);
return false;
}
// Rules A & B
if ($frame->get_style()->page_break_before === "avoid") {
Helpers::dompdf_debug("page-break", "before: avoid");
return false;
}
// Find the preceeding block-level sibling
$prev = $frame->get_prev_sibling();
while ($prev && !in_array($prev->get_style()->display, $block_types)) {
$prev = $prev->get_prev_sibling();
}
// Does the previous element allow a page break after?
if ($prev && $prev->get_style()->page_break_after === "avoid") {
Helpers::dompdf_debug("page-break", "after: avoid");
return false;
}
// If both $prev & $frame have the same parent, check the parent's
// page_break_inside property.
$parent = $frame->get_parent();
if ($prev && $parent && $parent->get_style()->page_break_inside === "avoid") {
Helpers::dompdf_debug("page-break", "parent inside: avoid");
return false;
}
// To prevent cascading page breaks when a top-level element has
// page-break-inside: avoid, ensure that at least one frame is
// on the page before splitting.
if ($parent->get_node()->nodeName === "body" && !$prev) {
// We are the body's first child
Helpers::dompdf_debug("page-break", "Body's first child.");
return false;
}
// If the frame is the first block-level frame, use the value from
// $frame's parent instead.
if (!$prev && $parent) {
return $this->_page_break_allowed($parent);
}
Helpers::dompdf_debug("page-break", "block: break allowed");
return true;
} // Inline frames (2):
else {
if (in_array($display, Style::$INLINE_TYPES)) {
// Avoid breaks within table-cells
if ($this->_in_table) {
Helpers::dompdf_debug("page-break", "In table: " . $this->_in_table);
return false;
}
// Rule C
$block_parent = $frame->find_block_parent();
if (count($block_parent->get_line_boxes()) < $frame->get_style()->orphans) {
Helpers::dompdf_debug("page-break", "orphans");
return false;
}
// FIXME: Checking widows is tricky without having laid out the
// remaining line boxes. Just ignore it for now...
// Rule D
$p = $block_parent;
while ($p) {
if ($p->get_style()->page_break_inside === "avoid") {
Helpers::dompdf_debug("page-break", "parent->inside: avoid");
return false;
}
$p = $p->find_block_parent();
}
// To prevent cascading page breaks when a top-level element has
// page-break-inside: avoid, ensure that at least one frame with
// some content is on the page before splitting.
$prev = $frame->get_prev_sibling();
while ($prev && ($prev->is_text_node() && trim($prev->get_node()->nodeValue) == "")) {
$prev = $prev->get_prev_sibling();
}
if ($block_parent->get_node()->nodeName === "body" && !$prev) {
// We are the body's first child
Helpers::dompdf_debug("page-break", "Body's first child.");
return false;
}
// Skip breaks on empty text nodes
if ($frame->is_text_node() &&
$frame->get_node()->nodeValue == ""
) {
return false;
}
Helpers::dompdf_debug("page-break", "inline: break allowed");
return true;
// Table-rows
} else {
if ($display === "table-row") {
// Simply check if the parent table's page_break_inside property is
// not 'avoid'
$p = Table::find_parent_table($frame);
while ($p) {
if ($p->get_style()->page_break_inside === "avoid") {
Helpers::dompdf_debug("page-break", "parent->inside: avoid");
return false;
}
$p = $p->find_block_parent();
}
// Avoid breaking after the first row of a table
if ($p && $p->get_first_child() === $frame) {
Helpers::dompdf_debug("page-break", "table: first-row");
return false;
}
// If this is a nested table, prevent the page from breaking
if ($this->_in_table > 1) {
Helpers::dompdf_debug("page-break", "table: nested table");
return false;
}
Helpers::dompdf_debug("page-break", "table-row/row-groups: break allowed");
return true;
} else {
if (in_array($display, Table::$ROW_GROUPS)) {
// Disallow breaks at row-groups: only split at row boundaries
return false;
} else {
Helpers::dompdf_debug("page-break", "? " . $frame->get_style()->display . "");
return false;
}
}
}
}
}
/**
* Check if $frame will fit on the page. If the frame does not fit,
* the frame tree is modified so that a page break occurs in the
* correct location.
*
* @param Frame $frame the frame to check
*
* @return bool
*/
function check_page_break(Frame $frame)
{
// Do not split if we have already or if the frame was already
// pushed to the next page (prevents infinite loops)
if ($this->_page_full || $frame->_already_pushed) {
return false;
}
// If the frame is absolute of fixed it shouldn't break
$p = $frame;
do {
if ($p->is_absolute())
return false;
} while ($p = $p->get_parent());
$margin_height = $frame->get_margin_height();
// FIXME If the row is taller than the page and
// if it the first of the page, we don't break
if ($frame->get_style()->display === "table-row" &&
!$frame->get_prev_sibling() &&
$margin_height > $this->get_margin_height()
)
return false;
// Determine the frame's maximum y value
$max_y = (float)$frame->get_position("y") + $margin_height;
// If a split is to occur here, then the bottom margins & paddings of all
// parents of $frame must fit on the page as well:
$p = $frame->get_parent();
while ($p) {
$max_y += $p->get_style()->computed_bottom_spacing();
$p = $p->get_parent();
}
// Check if $frame flows off the page
if ($max_y <= $this->_bottom_page_margin)
// no: do nothing
return false;
Helpers::dompdf_debug("page-break", "check_page_break");
Helpers::dompdf_debug("page-break", "in_table: " . $this->_in_table);
// yes: determine page break location
$iter = $frame;
$flg = false;
$in_table = $this->_in_table;
Helpers::dompdf_debug("page-break", "Starting search");
while ($iter) {
// echo "\nbacktrack: " .$iter->get_node()->nodeName ." ".spl_object_hash($iter->get_node()). "";
if ($iter === $this) {
Helpers::dompdf_debug("page-break", "reached root.");
// We've reached the root in our search. Just split at $frame.
break;
}
if ($this->_page_break_allowed($iter)) {
Helpers::dompdf_debug("page-break", "break allowed, splitting.");
$iter->split(null, true);
$this->_page_full = true;
$this->_in_table = $in_table;
$frame->_already_pushed = true;
return true;
}
if (!$flg && $next = $iter->get_last_child()) {
Helpers::dompdf_debug("page-break", "following last child.");
if ($next->is_table())
$this->_in_table++;
$iter = $next;
continue;
}
if ($next = $iter->get_prev_sibling()) {
Helpers::dompdf_debug("page-break", "following prev sibling.");
if ($next->is_table() && !$iter->is_table())
$this->_in_table++;
else if (!$next->is_table() && $iter->is_table())
$this->_in_table--;
$iter = $next;
$flg = false;
continue;
}
if ($next = $iter->get_parent()) {
Helpers::dompdf_debug("page-break", "following parent.");
if ($iter->is_table())
$this->_in_table--;
$iter = $next;
$flg = true;
continue;
}
break;
}
$this->_in_table = $in_table;
// No valid page break found. Just break at $frame.
Helpers::dompdf_debug("page-break", "no valid break found, just splitting.");
// If we are in a table, backtrack to the nearest top-level table row
if ($this->_in_table) {
$iter = $frame;
while ($iter && $iter->get_style()->display !== "table-row" && $iter->get_style()->display !== 'table-row-group')
$iter = $iter->get_parent();
$iter->split(null, true);
} else {
$frame->split(null, true);
}
$this->_page_full = true;
$frame->_already_pushed = true;
return true;
}
//........................................................................
/**
* @param Frame|null $frame
* @param bool $force_pagebreak
*/
function split(Frame $frame = null, $force_pagebreak = false)
{
// Do nothing
}
/**
* Add a floating frame
*
* @param Frame $frame
*
* @return void
*/
function add_floating_frame(Frame $frame)
{
array_unshift($this->_floating_frames, $frame);
}
/**
* @return Frame[]
*/
function get_floating_frames()
{
return $this->_floating_frames;
}
/**
* @param $key
*/
public function remove_floating_frame($key)
{
unset($this->_floating_frames[$key]);
}
/**
* @param Frame $child
* @return int|mixed
*/
public function get_lowest_float_offset(Frame $child)
{
$style = $child->get_style();
$side = $style->clear;
$float = $style->float;
$y = 0;
if ($float === "none") {
foreach ($this->_floating_frames as $key => $frame) {
if ($side === "both" || $frame->get_style()->float === $side) {
$y = max($y, $frame->get_position("y") + $frame->get_margin_height());
}
$this->remove_floating_frame($key);
}
}
if ($y > 0) {
$y++; // add 1px buffer from float
}
return $y;
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
/**
* Decorates table cells for layout
*
* @package dompdf
*/
class TableCell extends BlockFrameDecorator
{
protected $_resolved_borders;
protected $_content_height;
//........................................................................
/**
* TableCell constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
$this->_resolved_borders = array();
$this->_content_height = 0;
}
//........................................................................
function reset()
{
parent::reset();
$this->_resolved_borders = array();
$this->_content_height = 0;
$this->_frame->reset();
}
/**
* @return int
*/
function get_content_height()
{
return $this->_content_height;
}
/**
* @param $height
*/
function set_content_height($height)
{
$this->_content_height = $height;
}
/**
* @param $height
*/
function set_cell_height($height)
{
$style = $this->get_style();
$v_space = (float)$style->length_in_pt(array($style->margin_top,
$style->padding_top,
$style->border_top_width,
$style->border_bottom_width,
$style->padding_bottom,
$style->margin_bottom),
$style->width);
$new_height = $height - $v_space;
$style->height = $new_height;
if ($new_height > $this->_content_height) {
$y_offset = 0;
// Adjust our vertical alignment
switch ($style->vertical_align) {
default:
case "baseline":
// FIXME: this isn't right
case "top":
// Don't need to do anything
return;
case "middle":
$y_offset = ($new_height - $this->_content_height) / 2;
break;
case "bottom":
$y_offset = $new_height - $this->_content_height;
break;
}
if ($y_offset) {
// Move our children
foreach ($this->get_line_boxes() as $line) {
foreach ($line->get_frames() as $frame)
$frame->move(0, $y_offset);
}
}
}
}
/**
* @param $side
* @param $border_spec
*/
function set_resolved_border($side, $border_spec)
{
$this->_resolved_borders[$side] = $border_spec;
}
/**
* @param $side
* @return mixed
*/
function get_resolved_border($side)
{
return $this->_resolved_borders[$side];
}
/**
* @return array
*/
function get_resolved_borders()
{
return $this->_resolved_borders;
}
}
@@ -0,0 +1,70 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Frame;
/**
* Table row group decorator
*
* Overrides split() method for tbody, thead & tfoot elements
*
* @package dompdf
*/
class TableRowGroup extends AbstractFrameDecorator
{
/**
* Class constructor
*
* @param Frame $frame Frame to decorate
* @param Dompdf $dompdf Current dompdf instance
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
}
/**
* Override split() to remove all child rows and this element from the cellmap
*
* @param Frame $child
* @param bool $force_pagebreak
*
* @return void
*/
function split(Frame $child = null, $force_pagebreak = false)
{
if (is_null($child)) {
parent::split();
return;
}
// Remove child & all subsequent rows from the cellmap
$cellmap = $this->get_parent()->get_cellmap();
$iter = $child;
while ($iter) {
$cellmap->remove_row($iter);
$iter = $iter->get_next_sibling();
}
// If we are splitting at the first child remove the
// table-row-group from the cellmap as well
if ($child === $this->get_first_child()) {
$cellmap->remove_row_group($this);
parent::split();
return;
}
$cellmap->update_row_group($this, $child->get_prev_sibling());
parent::split($child);
}
}
@@ -0,0 +1,54 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
/**
* Decorates Frames for table row layout
*
* @package dompdf
*/
class TableRow extends AbstractFrameDecorator
{
/**
* TableRow constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
}
//........................................................................
/**
* Remove all non table-cell frames from this row and move them after
* the table.
*/
function normalise()
{
// Find our table parent
$p = TableFrameDecorator::find_parent_table($this);
$erroneous_frames = array();
foreach ($this->get_children() as $child) {
$display = $child->get_style()->display;
if ($display !== "table-cell")
$erroneous_frames[] = $child;
}
// dump the extra nodes after the table.
foreach ($erroneous_frames as $frame)
$p->move_after($frame);
}
}
@@ -0,0 +1,398 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Cellmap;
use DOMNode;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\Frame\Factory;
/**
* Decorates Frames for table layout
*
* @package dompdf
*/
class Table extends AbstractFrameDecorator
{
public static $VALID_CHILDREN = array(
"table-row-group",
"table-row",
"table-header-group",
"table-footer-group",
"table-column",
"table-column-group",
"table-caption",
"table-cell"
);
public static $ROW_GROUPS = array(
'table-row-group',
'table-header-group',
'table-footer-group'
);
/**
* The Cellmap object for this table. The cellmap maps table cells
* to rows and columns, and aids in calculating column widths.
*
* @var \Dompdf\Cellmap
*/
protected $_cellmap;
/**
* The minimum width of the table, in pt
*
* @var float
*/
protected $_min_width;
/**
* The maximum width of the table, in pt
*
* @var float
*/
protected $_max_width;
/**
* Table header rows. Each table header is duplicated when a table
* spans pages.
*
* @var array
*/
protected $_headers;
/**
* Table footer rows. Each table footer is duplicated when a table
* spans pages.
*
* @var array
*/
protected $_footers;
/**
* Class constructor
*
* @param Frame $frame the frame to decorate
* @param Dompdf $dompdf
*/
public function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
$this->_cellmap = new Cellmap($this);
if ($frame->get_style()->table_layout === "fixed") {
$this->_cellmap->set_layout_fixed(true);
}
$this->_min_width = null;
$this->_max_width = null;
$this->_headers = array();
$this->_footers = array();
}
public function reset()
{
parent::reset();
$this->_cellmap->reset();
$this->_min_width = null;
$this->_max_width = null;
$this->_headers = array();
$this->_footers = array();
$this->_reflower->reset();
}
//........................................................................
/**
* split the table at $row. $row and all subsequent rows will be
* added to the clone. This method is overidden in order to remove
* frames from the cellmap properly.
*
* @param Frame $child
* @param bool $force_pagebreak
*
* @return void
*/
public function split(Frame $child = null, $force_pagebreak = false)
{
if (is_null($child)) {
parent::split();
return;
}
// If $child is a header or if it is the first non-header row, do
// not duplicate headers, simply move the table to the next page.
if (count($this->_headers) && !in_array($child, $this->_headers, true) &&
!in_array($child->get_prev_sibling(), $this->_headers, true)
) {
$first_header = null;
// Insert copies of the table headers before $child
foreach ($this->_headers as $header) {
$new_header = $header->deep_copy();
if (is_null($first_header)) {
$first_header = $new_header;
}
$this->insert_child_before($new_header, $child);
}
parent::split($first_header);
} elseif (in_array($child->get_style()->display, self::$ROW_GROUPS)) {
// Individual rows should have already been handled
parent::split($child);
} else {
$iter = $child;
while ($iter) {
$this->_cellmap->remove_row($iter);
$iter = $iter->get_next_sibling();
}
parent::split($child);
}
}
/**
* Return a copy of this frame with $node as its node
*
* @param DOMNode $node
*
* @return Frame
*/
public function copy(DOMNode $node)
{
$deco = parent::copy($node);
// In order to keep columns' widths through pages
$deco->_cellmap->set_columns($this->_cellmap->get_columns());
$deco->_cellmap->lock_columns();
return $deco;
}
/**
* Static function to locate the parent table of a frame
*
* @param Frame $frame
*
* @return Table the table that is an ancestor of $frame
*/
public static function find_parent_table(Frame $frame)
{
while ($frame = $frame->get_parent())
if ($frame->is_table())
break;
return $frame;
}
/**
* Return this table's Cellmap
*
* @return \Dompdf\Cellmap
*/
public function get_cellmap()
{
return $this->_cellmap;
}
/**
* Return the minimum width of this table
*
* @return float
*/
public function get_min_width()
{
return $this->_min_width;
}
/**
* Return the maximum width of this table
*
* @return float
*/
public function get_max_width()
{
return $this->_max_width;
}
/**
* Set the minimum width of the table
*
* @param float $width the new minimum width
*/
public function set_min_width($width)
{
$this->_min_width = $width;
}
/**
* Set the maximum width of the table
*
* @param float $width the new maximum width
*/
public function set_max_width($width)
{
$this->_max_width = $width;
}
/**
* Restructure tree so that the table has the correct structure.
* Invalid children (i.e. all non-table-rows) are moved below the
* table.
*
* @fixme #1363 Method has some bugs. $table_row has not been initialized and lookup most likely could return an
* array of Style instead a Style Object
*/
public function normalise()
{
// Store frames generated by invalid tags and move them outside the table
$erroneous_frames = array();
$anon_row = false;
$iter = $this->get_first_child();
while ($iter) {
$child = $iter;
$iter = $iter->get_next_sibling();
$display = $child->get_style()->display;
if ($anon_row) {
if ($display === "table-row") {
// Add the previous anonymous row
$this->insert_child_before($table_row, $child);
$table_row->normalise();
$child->normalise();
$this->_cellmap->add_row();
$anon_row = false;
continue;
}
// add the child to the anonymous row
$table_row->append_child($child);
continue;
} else {
if ($display === "table-row") {
$child->normalise();
continue;
}
if ($display === "table-cell") {
$css = $this->get_style()->get_stylesheet();
// Create an anonymous table row group
$tbody = $this->get_node()->ownerDocument->createElement("tbody");
$frame = new Frame($tbody);
$style = $css->create_style();
$style->inherit($this->get_style());
// Lookup styles for tbody tags. If the user wants styles to work
// better, they should make the tbody explicit... I'm not going to
// try to guess what they intended.
if ($tbody_style = $css->lookup("tbody")) {
$style->merge($tbody_style);
}
$style->display = 'table-row-group';
// Okay, I have absolutely no idea why I need this clone here, but
// if it's omitted, php (as of 2004-07-28) segfaults.
$frame->set_style($style);
$table_row_group = Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
// Create an anonymous table row
$tr = $this->get_node()->ownerDocument->createElement("tr");
$frame = new Frame($tr);
$style = $css->create_style();
$style->inherit($this->get_style());
// Lookup styles for tr tags. If the user wants styles to work
// better, they should make the tr explicit... I'm not going to
// try to guess what they intended.
if ($tr_style = $css->lookup("tr")) {
$style->merge($tr_style);
}
$style->display = 'table-row';
// Okay, I have absolutely no idea why I need this clone here, but
// if it's omitted, php (as of 2004-07-28) segfaults.
$frame->set_style(clone $style);
$table_row = Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
// Add the cell to the row
$table_row->append_child($child, true);
// Add the tr to the tbody
$table_row_group->append_child($table_row, true);
$anon_row = true;
continue;
}
if (!in_array($display, self::$VALID_CHILDREN)) {
$erroneous_frames[] = $child;
continue;
}
// Normalise other table parts (i.e. row groups)
foreach ($child->get_children() as $grandchild) {
if ($grandchild->get_style()->display === "table-row") {
$grandchild->normalise();
}
}
// Add headers and footers
if ($display === "table-header-group") {
$this->_headers[] = $child;
} elseif ($display === "table-footer-group") {
$this->_footers[] = $child;
}
}
}
if ($anon_row && $table_row_group instanceof AbstractFrameDecorator) {
// Add the row to the table
$this->_frame->append_child($table_row_group->_frame);
$table_row->normalise();
}
foreach ($erroneous_frames as $frame) {
$this->move_after($frame);
}
}
//........................................................................
/**
* Moves the specified frame and it's corresponding node outside of
* the table.
*
* @param Frame $frame the frame to move
*/
public function move_after(Frame $frame)
{
$this->get_parent()->insert_child_after($frame, $this);
}
}
@@ -0,0 +1,205 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Brian Sweeney <eclecticgeek@gmail.com>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Frame;
use Dompdf\Exception;
use DOMText;
use Dompdf\FontMetrics;
/**
* Decorates Frame objects for text layout
*
* @access private
* @package dompdf
*/
class Text extends AbstractFrameDecorator
{
// protected members
protected $_text_spacing;
/**
* Text constructor.
* @param Frame $frame
* @param Dompdf $dompdf
* @throws Exception
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
if (!$frame->is_text_node()) {
throw new Exception("Text_Decorator can only be applied to #text nodes.");
}
parent::__construct($frame, $dompdf);
$this->_text_spacing = null;
}
function reset()
{
parent::reset();
$this->_text_spacing = null;
}
// Accessor methods
/**
* @return null
*/
function get_text_spacing()
{
return $this->_text_spacing;
}
/**
* @return string
*/
function get_text()
{
// FIXME: this should be in a child class (and is incorrect)
// if ( $this->_frame->get_style()->content !== "normal" ) {
// $this->_frame->get_node()->data = $this->_frame->get_style()->content;
// $this->_frame->get_style()->content = "normal";
// }
// Helpers::pre_r("---");
// $style = $this->_frame->get_style();
// var_dump($text = $this->_frame->get_node()->data);
// var_dump($asc = utf8_decode($text));
// for ($i = 0; $i < strlen($asc); $i++)
// Helpers::pre_r("$i: " . $asc[$i] . " - " . ord($asc[$i]));
// Helpers::pre_r("width: " . $this->_dompdf->getFontMetrics()->getTextWidth($text, $style->font_family, $style->font_size));
return $this->_frame->get_node()->data;
}
//........................................................................
/**
* Vertical margins & padding do not apply to text frames
*
* http://www.w3.org/TR/CSS21/visudet.html#inline-non-replaced:
*
* The vertical padding, border and margin of an inline, non-replaced box
* start at the top and bottom of the content area, not the
* 'line-height'. But only the 'line-height' is used to calculate the
* height of the line box.
*
* @return float|int
*/
function get_margin_height()
{
// This function is called in add_frame_to_line() and is used to
// determine the line height, so we actually want to return the
// 'line-height' property, not the actual margin box
$style = $this->get_parent()->get_style();
$font = $style->font_family;
$size = $style->font_size;
/*
Helpers::pre_r('-----');
Helpers::pre_r($style->line_height);
Helpers::pre_r($style->font_size);
Helpers::pre_r($this->_dompdf->getFontMetrics()->getFontHeight($font, $size));
Helpers::pre_r(($style->line_height / $size) * $this->_dompdf->getFontMetrics()->getFontHeight($font, $size));
*/
return ($style->line_height / ($size > 0 ? $size : 1)) * $this->_dompdf->getFontMetrics()->getFontHeight($font, $size);
}
/**
* @return array
*/
function get_padding_box()
{
$pb = $this->_frame->get_padding_box();
$pb[3] = $pb["h"] = $this->_frame->get_style()->height;
return $pb;
}
/**
* @param $spacing
*/
function set_text_spacing($spacing)
{
$style = $this->_frame->get_style();
$this->_text_spacing = $spacing;
$char_spacing = (float)$style->length_in_pt($style->letter_spacing);
// Re-adjust our width to account for the change in spacing
$style->width = $this->_dompdf->getFontMetrics()->getTextWidth($this->get_text(), $style->font_family, $style->font_size, $spacing, $char_spacing);
}
/**
* Recalculate the text width
*
* @return float
*/
function recalculate_width()
{
$style = $this->get_style();
$text = $this->get_text();
$size = $style->font_size;
$font = $style->font_family;
$word_spacing = (float)$style->length_in_pt($style->word_spacing);
$char_spacing = (float)$style->length_in_pt($style->letter_spacing);
return $style->width = $this->_dompdf->getFontMetrics()->getTextWidth($text, $font, $size, $word_spacing, $char_spacing);
}
// Text manipulation methods
/**
* split the text in this frame at the offset specified. The remaining
* text is added a sibling frame following this one and is returned.
*
* @param $offset
* @return Frame|null
*/
function split_text($offset)
{
if ($offset == 0) {
return null;
}
$split = $this->_frame->get_node()->splitText($offset);
$deco = $this->copy($split);
$p = $this->get_parent();
$p->insert_child_after($deco, $this, false);
if ($p instanceof Inline) {
$p->split($deco);
}
return $deco;
}
/**
* @param $offset
* @param $count
*/
function delete_text($offset, $count)
{
$this->_frame->get_node()->deleteData($offset, $count);
}
/**
* @param $text
*/
function set_text($text)
{
$this->_frame->get_node()->data = $text;
}
}
@@ -0,0 +1,500 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Adapter\CPDF;
use Dompdf\Css\Style;
use Dompdf\Dompdf;
use Dompdf\Helpers;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block;
use Dompdf\Frame\Factory;
/**
* Base reflower class
*
* Reflower objects are responsible for determining the width and height of
* individual frames. They also create line and page breaks as necessary.
*
* @package dompdf
*/
abstract class AbstractFrameReflower
{
/**
* Frame for this reflower
*
* @var Frame
*/
protected $_frame;
/**
* Cached min/max size
*
* @var array
*/
protected $_min_max_cache;
/**
* AbstractFrameReflower constructor.
* @param Frame $frame
*/
function __construct(Frame $frame)
{
$this->_frame = $frame;
$this->_min_max_cache = null;
}
function dispose()
{
}
/**
* @return Dompdf
*/
function get_dompdf()
{
return $this->_frame->get_dompdf();
}
/**
* Collapse frames margins
* http://www.w3.org/TR/CSS2/box.html#collapsing-margins
*/
protected function _collapse_margins()
{
$frame = $this->_frame;
$cb = $frame->get_containing_block();
$style = $frame->get_style();
// Margins of float/absolutely positioned/inline-block elements do not collapse.
if (!$frame->is_in_flow() || $frame->is_inline_block()) {
return;
}
$t = $style->length_in_pt($style->margin_top, $cb["h"]);
$b = $style->length_in_pt($style->margin_bottom, $cb["h"]);
// Handle 'auto' values
if ($t === "auto") {
$style->margin_top = "0pt";
$t = 0;
}
if ($b === "auto") {
$style->margin_bottom = "0pt";
$b = 0;
}
// Collapse vertical margins:
$n = $frame->get_next_sibling();
if ($n && !$n->is_block()) {
while ($n = $n->get_next_sibling()) {
if ($n->is_block()) {
break;
}
if (!$n->get_first_child()) {
$n = null;
break;
}
}
}
if ($n) {
$n_style = $n->get_style();
$b = max($b, (float)$n_style->length_in_pt($n_style->margin_top, $cb["h"]));
$n_style->margin_top = "0pt";
$style->margin_bottom = $b . "pt";
}
// Collapse our first child's margin, if there is no border or padding
if ($style->get_border_top_width() == 0 && $style->length_in_pt($style->padding_top) == 0) {
$f = $this->_frame->get_first_child();
if ( $f && !$f->is_block() ) {
while ( $f = $f->get_next_sibling() ) {
if ( $f->is_block() ) {
break;
}
if ( !$f->get_first_child() ) {
$f = null;
break;
}
}
}
// Margin are collapsed only between block elements
if ($f) {
$f_style = $f->get_style();
$t = max($t, (float)$f_style->length_in_pt($f_style->margin_top, $cb["h"]));
$style->margin_top = $t."pt";
$f_style->margin_top = "0pt";
}
}
// Collapse our last child's margin, if there is no border or padding
if ($style->get_border_bottom_width() == 0 && $style->length_in_pt($style->padding_bottom) == 0) {
$l = $this->_frame->get_last_child();
if ($l && !$l->is_block()) {
while ( $l = $l->get_prev_sibling() ) {
if ( $l->is_block() ) {
break;
}
if ( !$l->get_last_child() ) {
$l = null;
break;
}
}
}
// Margin are collapsed only between block elements
if ($l) {
$l_style = $l->get_style();
$b = max($b, (float)$l_style->length_in_pt($l_style->margin_bottom, $cb["h"]));
$style->margin_bottom = $b."pt";
$l_style->margin_bottom = "0pt";
}
}
}
/**
* @param Block|null $block
* @return mixed
*/
abstract function reflow(Block $block = null);
/**
* Required for table layout: Returns an array(0 => min, 1 => max, "min"
* => min, "max" => max) of the minimum and maximum widths of this frame.
* This provides a basic implementation. Child classes should override
* this if necessary.
*
* @return array|null
*/
function get_min_max_width()
{
if (!is_null($this->_min_max_cache)) {
return $this->_min_max_cache;
}
$style = $this->_frame->get_style();
// Account for margins & padding
$dims = array($style->padding_left,
$style->padding_right,
$style->border_left_width,
$style->border_right_width,
$style->margin_left,
$style->margin_right);
$cb_w = $this->_frame->get_containing_block("w");
$delta = (float)$style->length_in_pt($dims, $cb_w);
// Handle degenerate case
if (!$this->_frame->get_first_child()) {
return $this->_min_max_cache = array(
$delta, $delta,
"min" => $delta,
"max" => $delta,
);
}
$low = array();
$high = array();
for ($iter = $this->_frame->get_children()->getIterator();
$iter->valid();
$iter->next()) {
$inline_min = 0;
$inline_max = 0;
// Add all adjacent inline widths together to calculate max width
while ($iter->valid() && in_array($iter->current()->get_style()->display, Style::$INLINE_TYPES)) {
$child = $iter->current();
$minmax = $child->get_min_max_width();
if (in_array($iter->current()->get_style()->white_space, array("pre", "nowrap"))) {
$inline_min += $minmax["min"];
} else {
$low[] = $minmax["min"];
}
$inline_max += $minmax["max"];
$iter->next();
}
if ($inline_max > 0) $high[] = $inline_max;
if ($inline_min > 0) $low[] = $inline_min;
if ($iter->valid()) {
list($low[], $high[]) = $iter->current()->get_min_max_width();
continue;
}
}
$min = count($low) ? max($low) : 0;
$max = count($high) ? max($high) : 0;
// Use specified width if it is greater than the minimum defined by the
// content. If the width is a percentage ignore it for now.
$width = $style->width;
if ($width !== "auto" && !Helpers::is_percent($width)) {
$width = (float)$style->length_in_pt($width, $cb_w);
if ($min < $width) {
$min = $width;
}
if ($max < $width) {
$max = $width;
}
}
$min += $delta;
$max += $delta;
return $this->_min_max_cache = array($min, $max, "min" => $min, "max" => $max);
}
/**
* Parses a CSS string containing quotes and escaped hex characters
*
* @param $string string The CSS string to parse
* @param $single_trim
* @return string
*/
protected function _parse_string($string, $single_trim = false)
{
if ($single_trim) {
$string = preg_replace('/^[\"\']/', "", $string);
$string = preg_replace('/[\"\']$/', "", $string);
} else {
$string = trim($string, "'\"");
}
$string = str_replace(array("\\\n", '\\"', "\\'"),
array("", '"', "'"), $string);
// Convert escaped hex characters into ascii characters (e.g. \A => newline)
$string = preg_replace_callback("/\\\\([0-9a-fA-F]{0,6})/",
function ($matches) { return \Dompdf\Helpers::unichr(hexdec($matches[1])); },
$string);
return $string;
}
/**
* Parses a CSS "quotes" property
*
* @return array|null An array of pairs of quotes
*/
protected function _parse_quotes()
{
// Matches quote types
$re = '/(\'[^\']*\')|(\"[^\"]*\")/';
$quotes = $this->_frame->get_style()->quotes;
// split on spaces, except within quotes
if (!preg_match_all($re, "$quotes", $matches, PREG_SET_ORDER)) {
return null;
}
$quotes_array = array();
foreach ($matches as $_quote) {
$quotes_array[] = $this->_parse_string($_quote[0], true);
}
if (empty($quotes_array)) {
$quotes_array = array('"', '"');
}
return array_chunk($quotes_array, 2);
}
/**
* Parses the CSS "content" property
*
* @return string|null The resulting string
*/
protected function _parse_content()
{
// Matches generated content
$re = "/\n" .
"\s(counters?\\([^)]*\\))|\n" .
"\A(counters?\\([^)]*\\))|\n" .
"\s([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\3|\n" .
"\A([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\5|\n" .
"\s([^\s\"']+)|\n" .
"\A([^\s\"']+)\n" .
"/xi";
$content = $this->_frame->get_style()->content;
$quotes = $this->_parse_quotes();
// split on spaces, except within quotes
if (!preg_match_all($re, $content, $matches, PREG_SET_ORDER)) {
return null;
}
$text = "";
foreach ($matches as $match) {
if (isset($match[2]) && $match[2] !== "") {
$match[1] = $match[2];
}
if (isset($match[6]) && $match[6] !== "") {
$match[4] = $match[6];
}
if (isset($match[8]) && $match[8] !== "") {
$match[7] = $match[8];
}
if (isset($match[1]) && $match[1] !== "") {
// counters?(...)
$match[1] = mb_strtolower(trim($match[1]));
// Handle counter() references:
// http://www.w3.org/TR/CSS21/generate.html#content
$i = mb_strpos($match[1], ")");
if ($i === false) {
continue;
}
preg_match('/(counters?)(^\()*?\(\s*([^\s,]+)\s*(,\s*["\']?([^"\'\)]+)["\']?\s*(,\s*([^\s)]+)\s*)?)?\)/i', $match[1], $args);
$counter_id = $args[3];
if (strtolower($args[1]) == 'counter') {
// counter(name [,style])
if (isset($args[5])) {
$type = trim($args[5]);
} else {
$type = null;
}
$p = $this->_frame->lookup_counter_frame($counter_id);
$text .= $p->counter_value($counter_id, $type);
} else if (strtolower($args[1]) == 'counters') {
// counters(name, string [,style])
if (isset($args[5])) {
$string = $this->_parse_string($args[5]);
} else {
$string = "";
}
if (isset($args[7])) {
$type = trim($args[7]);
} else {
$type = null;
}
$p = $this->_frame->lookup_counter_frame($counter_id);
$tmp = array();
while ($p) {
// We only want to use the counter values when they actually increment the counter
if (array_key_exists($counter_id, $p->_counters)) {
array_unshift($tmp, $p->counter_value($counter_id, $type));
}
$p = $p->lookup_counter_frame($counter_id);
}
$text .= implode($string, $tmp);
} else {
// countertops?
continue;
}
} else if (isset($match[4]) && $match[4] !== "") {
// String match
$text .= $this->_parse_string($match[4]);
} else if (isset($match[7]) && $match[7] !== "") {
// Directive match
if ($match[7] === "open-quote") {
// FIXME: do something here
$text .= $quotes[0][0];
} else if ($match[7] === "close-quote") {
// FIXME: do something else here
$text .= $quotes[0][1];
} else if ($match[7] === "no-open-quote") {
// FIXME:
} else if ($match[7] === "no-close-quote") {
// FIXME:
} else if (mb_strpos($match[7], "attr(") === 0) {
$i = mb_strpos($match[7], ")");
if ($i === false) {
continue;
}
$attr = mb_substr($match[7], 5, $i - 5);
if ($attr == "") {
continue;
}
$text .= $this->_frame->get_parent()->get_node()->getAttribute($attr);
} else {
continue;
}
}
}
return $text;
}
/**
* Sets the generated content of a generated frame
*/
protected function _set_content()
{
$frame = $this->_frame;
$style = $frame->get_style();
// if the element was pushed to a new page use the saved counter value, otherwise use the CSS reset value
if ($style->counter_reset && ($reset = $style->counter_reset) !== "none") {
$vars = preg_split('/\s+/', trim($reset), 2);
$frame->reset_counter($vars[0], (isset($frame->_counters['__' . $vars[0]]) ? $frame->_counters['__' . $vars[0]] : (isset($vars[1]) ? $vars[1] : 0)));
}
if ($style->counter_increment && ($increment = $style->counter_increment) !== "none") {
$frame->increment_counters($increment);
}
if ($style->content && $frame->get_node()->nodeName === "dompdf_generated") {
$content = $this->_parse_content();
// add generated content to the font subset
// FIXME: This is currently too late because the font subset has already been generated.
// See notes in issue #750.
if ($frame->get_dompdf()->getOptions()->getIsFontSubsettingEnabled() && $frame->get_dompdf()->get_canvas() instanceof CPDF) {
$frame->get_dompdf()->get_canvas()->register_string_subset($style->font_family, $content);
}
$node = $frame->get_node()->ownerDocument->createTextNode($content);
$new_style = $style->get_stylesheet()->create_style();
$new_style->inherit($style);
$new_frame = new Frame($node);
$new_frame->set_style($new_style);
Factory::decorate_frame($new_frame, $frame->get_dompdf(), $frame->get_root());
$frame->append_child($new_frame);
}
}
/**
* Determine current frame width based on contents
*
* @return float
*/
public function calculate_auto_width()
{
return $this->_frame->get_margin_width();
}
}
@@ -0,0 +1,953 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FontMetrics;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\TableCell as TableCellFrameDecorator;
use Dompdf\FrameDecorator\Text as TextFrameDecorator;
use Dompdf\Exception;
use Dompdf\Css\Style;
/**
* Reflows block frames
*
* @package dompdf
*/
class Block extends AbstractFrameReflower
{
// Minimum line width to justify, as fraction of available width
const MIN_JUSTIFY_WIDTH = 0.80;
/**
* @var BlockFrameDecorator
*/
protected $_frame;
function __construct(BlockFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* Calculate the ideal used value for the width property as per:
* http://www.w3.org/TR/CSS21/visudet.html#Computing_widths_and_margins
*
* @param float $width
*
* @return array
*/
protected function _calculate_width($width)
{
$frame = $this->_frame;
$style = $frame->get_style();
$w = $frame->get_containing_block("w");
if ($style->position === "fixed") {
$w = $frame->get_parent()->get_containing_block("w");
}
$rm = $style->length_in_pt($style->margin_right, $w);
$lm = $style->length_in_pt($style->margin_left, $w);
$left = $style->length_in_pt($style->left, $w);
$right = $style->length_in_pt($style->right, $w);
// Handle 'auto' values
$dims = array($style->border_left_width,
$style->border_right_width,
$style->padding_left,
$style->padding_right,
$width !== "auto" ? $width : 0,
$rm !== "auto" ? $rm : 0,
$lm !== "auto" ? $lm : 0);
// absolutely positioned boxes take the 'left' and 'right' properties into account
if ($frame->is_absolute()) {
$absolute = true;
$dims[] = $left !== "auto" ? $left : 0;
$dims[] = $right !== "auto" ? $right : 0;
} else {
$absolute = false;
}
$sum = (float)$style->length_in_pt($dims, $w);
// Compare to the containing block
$diff = $w - $sum;
if ($diff > 0) {
if ($absolute) {
// resolve auto properties: see
// http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
if ($width === "auto" && $left === "auto" && $right === "auto") {
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
// Technically, the width should be "shrink-to-fit" i.e. based on the
// preferred width of the content... a little too costly here as a
// special case. Just get the width to take up the slack:
$left = 0;
$right = 0;
$width = $diff;
} else if ($width === "auto") {
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
if ($left === "auto") {
$left = 0;
}
if ($right === "auto") {
$right = 0;
}
$width = $diff;
} else if ($left === "auto") {
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
if ($right === "auto") {
$right = 0;
}
$left = $diff;
} else if ($right === "auto") {
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
$right = $diff;
}
} else {
// Find auto properties and get them to take up the slack
if ($width === "auto") {
$width = $diff;
} else if ($lm === "auto" && $rm === "auto") {
$lm = $rm = round($diff / 2);
} else if ($lm === "auto") {
$lm = $diff;
} else if ($rm === "auto") {
$rm = $diff;
}
}
} else if ($diff < 0) {
// We are over constrained--set margin-right to the difference
$rm = $diff;
}
return array(
"width" => $width,
"margin_left" => $lm,
"margin_right" => $rm,
"left" => $left,
"right" => $right,
);
}
/**
* Call the above function, but resolve max/min widths
*
* @throws Exception
* @return array
*/
protected function _calculate_restricted_width()
{
$frame = $this->_frame;
$style = $frame->get_style();
$cb = $frame->get_containing_block();
if ($style->position === "fixed") {
$cb = $frame->get_root()->get_containing_block();
}
//if ( $style->position === "absolute" )
// $cb = $frame->find_positionned_parent()->get_containing_block();
if (!isset($cb["w"])) {
throw new Exception("Box property calculation requires containing block width");
}
// Treat width 100% as auto
if ($style->width === "100%") {
$width = "auto";
} else {
$width = $style->length_in_pt($style->width, $cb["w"]);
}
$calculate_width = $this->_calculate_width($width);
$margin_left = $calculate_width['margin_left'];
$margin_right = $calculate_width['margin_right'];
$width = $calculate_width['width'];
$left = $calculate_width['left'];
$right = $calculate_width['right'];
// Handle min/max width
$min_width = $style->length_in_pt($style->min_width, $cb["w"]);
$max_width = $style->length_in_pt($style->max_width, $cb["w"]);
if ($max_width !== "none" && $min_width > $max_width) {
list($max_width, $min_width) = array($min_width, $max_width);
}
if ($max_width !== "none" && $width > $max_width) {
extract($this->_calculate_width($max_width));
}
if ($width < $min_width) {
$calculate_width = $this->_calculate_width($min_width);
$margin_left = $calculate_width['margin_left'];
$margin_right = $calculate_width['margin_right'];
$width = $calculate_width['width'];
$left = $calculate_width['left'];
$right = $calculate_width['right'];
}
return array($width, $margin_left, $margin_right, $left, $right);
}
/**
* Determine the unrestricted height of content within the block
* not by adding each line's height, but by getting the last line's position.
* This because lines could have been pushed lower by a clearing element.
*
* @return float
*/
protected function _calculate_content_height()
{
$height = 0;
$lines = $this->_frame->get_line_boxes();
if (count($lines) > 0) {
$last_line = end($lines);
$content_box = $this->_frame->get_content_box();
$height = $last_line->y + $last_line->h - $content_box["y"];
}
return $height;
}
/**
* Determine the frame's restricted height
*
* @return array
*/
protected function _calculate_restricted_height()
{
$frame = $this->_frame;
$style = $frame->get_style();
$content_height = $this->_calculate_content_height();
$cb = $frame->get_containing_block();
$height = $style->length_in_pt($style->height, $cb["h"]);
$top = $style->length_in_pt($style->top, $cb["h"]);
$bottom = $style->length_in_pt($style->bottom, $cb["h"]);
$margin_top = $style->length_in_pt($style->margin_top, $cb["h"]);
$margin_bottom = $style->length_in_pt($style->margin_bottom, $cb["h"]);
if ($frame->is_absolute()) {
// see http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-height
$dims = array($top !== "auto" ? $top : 0,
$style->margin_top !== "auto" ? $style->margin_top : 0,
$style->padding_top,
$style->border_top_width,
$height !== "auto" ? $height : 0,
$style->border_bottom_width,
$style->padding_bottom,
$style->margin_bottom !== "auto" ? $style->margin_bottom : 0,
$bottom !== "auto" ? $bottom : 0);
$sum = (float)$style->length_in_pt($dims, $cb["h"]);
$diff = $cb["h"] - $sum;
if ($diff > 0) {
if ($height === "auto" && $top === "auto" && $bottom === "auto") {
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$height = $diff;
} else if ($height === "auto" && $top === "auto") {
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$height = $content_height;
$top = $diff - $content_height;
} else if ($height === "auto" && $bottom === "auto") {
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$height = $content_height;
$bottom = $diff - $content_height;
} else if ($top === "auto" && $bottom === "auto") {
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$bottom = $diff;
} else if ($top === "auto") {
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$top = $diff;
} else if ($height === "auto") {
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$height = $diff;
} else if ($bottom === "auto") {
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$bottom = $diff;
} else {
if ($style->overflow === "visible") {
// set all autos to zero
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
if ($top === "auto") {
$top = 0;
}
if ($bottom === "auto") {
$bottom = 0;
}
if ($height === "auto") {
$height = $content_height;
}
}
// FIXME: overflow hidden
}
}
} else {
// Expand the height if overflow is visible
if ($height === "auto" && $content_height > $height /* && $style->overflow === "visible" */) {
$height = $content_height;
}
// FIXME: this should probably be moved to a seperate function as per
// _calculate_restricted_width
// Only handle min/max height if the height is independent of the frame's content
if (!($style->overflow === "visible" ||
($style->overflow === "hidden" && $height === "auto"))
) {
$min_height = $style->min_height;
$max_height = $style->max_height;
if (isset($cb["h"])) {
$min_height = $style->length_in_pt($min_height, $cb["h"]);
$max_height = $style->length_in_pt($max_height, $cb["h"]);
} else if (isset($cb["w"])) {
if (mb_strpos($min_height, "%") !== false) {
$min_height = 0;
} else {
$min_height = $style->length_in_pt($min_height, $cb["w"]);
}
if (mb_strpos($max_height, "%") !== false) {
$max_height = "none";
} else {
$max_height = $style->length_in_pt($max_height, $cb["w"]);
}
}
if ($max_height !== "none" && $min_height > $max_height) {
// Swap 'em
list($max_height, $min_height) = array($min_height, $max_height);
}
if ($max_height !== "none" && $height > $max_height) {
$height = $max_height;
}
if ($height < $min_height) {
$height = $min_height;
}
}
}
return array($height, $margin_top, $margin_bottom, $top, $bottom);
}
/**
* Adjust the justification of each of our lines.
* http://www.w3.org/TR/CSS21/text.html#propdef-text-align
*/
protected function _text_align()
{
$style = $this->_frame->get_style();
$w = $this->_frame->get_containing_block("w");
$width = (float)$style->length_in_pt($style->width, $w);
switch ($style->text_align) {
default:
case "left":
foreach ($this->_frame->get_line_boxes() as $line) {
if (!$line->left) {
continue;
}
foreach ($line->get_frames() as $frame) {
if ($frame instanceof BlockFrameDecorator) {
continue;
}
$frame->set_position($frame->get_position("x") + $line->left);
}
}
return;
case "right":
foreach ($this->_frame->get_line_boxes() as $line) {
// Move each child over by $dx
$dx = $width - $line->w - $line->right;
foreach ($line->get_frames() as $frame) {
// Block frames are not aligned by text-align
if ($frame instanceof BlockFrameDecorator) {
continue;
}
$frame->set_position($frame->get_position("x") + $dx);
}
}
break;
case "justify":
// We justify all lines except the last one
$lines = $this->_frame->get_line_boxes(); // needs to be a variable (strict standards)
$last_line = array_pop($lines);
foreach ($lines as $i => $line) {
if ($line->br) {
unset($lines[$i]);
}
}
// One space character's width. Will be used to get a more accurate spacing
$space_width = $this->get_dompdf()->getFontMetrics()->getTextWidth(" ", $style->font_family, $style->font_size);
foreach ($lines as $line) {
if ($line->left) {
foreach ($line->get_frames() as $frame) {
if (!$frame instanceof TextFrameDecorator) {
continue;
}
$frame->set_position($frame->get_position("x") + $line->left);
}
}
// Set the spacing for each child
if ($line->wc > 1) {
$spacing = ($width - ($line->left + $line->w + $line->right) + $space_width) / ($line->wc - 1);
} else {
$spacing = 0;
}
$dx = 0;
foreach ($line->get_frames() as $frame) {
if (!$frame instanceof TextFrameDecorator) {
continue;
}
$text = $frame->get_text();
$spaces = mb_substr_count($text, " ");
$char_spacing = (float)$style->length_in_pt($style->letter_spacing);
$_spacing = $spacing + $char_spacing;
$frame->set_position($frame->get_position("x") + $dx);
$frame->set_text_spacing($_spacing);
$dx += $spaces * $_spacing;
}
// The line (should) now occupy the entire width
$line->w = $width;
}
// Adjust the last line if necessary
if ($last_line->left) {
foreach ($last_line->get_frames() as $frame) {
if ($frame instanceof BlockFrameDecorator) {
continue;
}
$frame->set_position($frame->get_position("x") + $last_line->left);
}
}
break;
case "center":
case "centre":
foreach ($this->_frame->get_line_boxes() as $line) {
// Centre each line by moving each frame in the line by:
$dx = ($width + $line->left - $line->w - $line->right) / 2;
foreach ($line->get_frames() as $frame) {
// Block frames are not aligned by text-align
if ($frame instanceof BlockFrameDecorator) {
continue;
}
$frame->set_position($frame->get_position("x") + $dx);
}
}
break;
}
}
/**
* Align inline children vertically.
* Aligns each child vertically after each line is reflowed
*/
function vertical_align()
{
$canvas = null;
foreach ($this->_frame->get_line_boxes() as $line) {
$height = $line->h;
foreach ($line->get_frames() as $frame) {
$style = $frame->get_style();
$isInlineBlock = (
'-dompdf-image' === $style->display
|| 'inline-block' === $style->display
|| 'inline-table' === $style->display
);
if (!$isInlineBlock && $style->display !== "inline") {
continue;
}
if (!isset($canvas)) {
$canvas = $frame->get_root()->get_dompdf()->get_canvas();
}
$baseline = $canvas->get_font_baseline($style->font_family, $style->font_size);
$y_offset = 0;
//FIXME: The 0.8 ratio applied to the height is arbitrary (used to accommodate descenders?)
if($isInlineBlock) {
$lineFrames = $line->get_frames();
if (count($lineFrames) == 1) {
continue;
}
$frameBox = $frame->get_frame()->get_border_box();
$imageHeightDiff = $height * 0.8 - $frameBox['h'];
$align = $frame->get_style()->vertical_align;
if (in_array($align, Style::$vertical_align_keywords) === true) {
switch ($align) {
case "middle":
$y_offset = $imageHeightDiff / 2;
break;
case "sub":
$y_offset = 0.3 * $height + $imageHeightDiff;
break;
case "super":
$y_offset = -0.2 * $height + $imageHeightDiff;
break;
case "text-top": // FIXME: this should be the height of the frame minus the height of the text
$y_offset = $height - (float)$style->length_in_pt($style->get_line_height(), $style->font_size);
break;
case "top":
break;
case "text-bottom": // FIXME: align bottom of image with the descender?
case "bottom":
$y_offset = 0.3 * $height + $imageHeightDiff;
break;
case "baseline":
default:
$y_offset = $imageHeightDiff;
break;
}
} else {
$y_offset = $baseline - (float)$style->length_in_pt($align, $style->font_size) - $frameBox['h'];
}
} else {
$parent = $frame->get_parent();
if ($parent instanceof TableCellFrameDecorator) {
$align = "baseline";
} else {
$align = $parent->get_style()->vertical_align;
}
if (in_array($align, Style::$vertical_align_keywords) === true) {
switch ($align) {
case "middle":
$y_offset = ($height * 0.8 - $baseline) / 2;
break;
case "sub":
$y_offset = $height * 0.8 - $baseline * 0.5;
break;
case "super":
$y_offset = $height * 0.8 - $baseline * 1.4;
break;
case "text-top":
case "top": // Not strictly accurate, but good enough for now
break;
case "text-bottom":
case "bottom":
$y_offset = $height * 0.8 - $baseline;
break;
case "baseline":
default:
$y_offset = $height * 0.8 - $baseline;
break;
}
} else {
$y_offset = $height * 0.8 - $baseline - (float)$style->length_in_pt($align, $style->font_size);
}
}
if ($y_offset !== 0) {
$frame->move(0, $y_offset);
}
}
}
}
/**
* @param Frame $child
*/
function process_clear(Frame $child)
{
$child_style = $child->get_style();
$root = $this->_frame->get_root();
// Handle "clear"
if ($child_style->clear !== "none") {
//TODO: this is a WIP for handling clear/float frames that are in between inline frames
if ($child->get_prev_sibling() !== null) {
$this->_frame->add_line();
}
if ($child_style->float !== "none" && $child->get_next_sibling()) {
$this->_frame->set_current_line_number($this->_frame->get_current_line_number() - 1);
}
$lowest_y = $root->get_lowest_float_offset($child);
// If a float is still applying, we handle it
if ($lowest_y) {
if ($child->is_in_flow()) {
$line_box = $this->_frame->get_current_line_box();
$line_box->y = $lowest_y + $child->get_margin_height();
$line_box->left = 0;
$line_box->right = 0;
}
$child->move(0, $lowest_y - $child->get_position("y"));
}
}
}
/**
* @param Frame $child
* @param float $cb_x
* @param float $cb_w
*/
function process_float(Frame $child, $cb_x, $cb_w)
{
$child_style = $child->get_style();
$root = $this->_frame->get_root();
// Handle "float"
if ($child_style->float !== "none") {
$root->add_floating_frame($child);
// Remove next frame's beginning whitespace
$next = $child->get_next_sibling();
if ($next && $next instanceof TextFrameDecorator) {
$next->set_text(ltrim($next->get_text()));
}
$line_box = $this->_frame->get_current_line_box();
list($old_x, $old_y) = $child->get_position();
$float_x = $cb_x;
$float_y = $old_y;
$float_w = $child->get_margin_width();
if ($child_style->clear === "none") {
switch ($child_style->float) {
case "left":
$float_x += $line_box->left;
break;
case "right":
$float_x += ($cb_w - $line_box->right - $float_w);
break;
}
} else {
if ($child_style->float === "right") {
$float_x += ($cb_w - $float_w);
}
}
if ($cb_w < $float_x + $float_w - $old_x) {
// TODO handle when floating elements don't fit
}
$line_box->get_float_offsets();
if ($child->_float_next_line) {
$float_y += $line_box->h;
}
$child->set_position($float_x, $float_y);
$child->move($float_x - $old_x, $float_y - $old_y, true);
}
}
/**
* @param BlockFrameDecorator $block
* @return mixed|void
*/
function reflow(BlockFrameDecorator $block = null)
{
// Check if a page break is forced
$page = $this->_frame->get_root();
$page->check_forced_page_break($this->_frame);
// Bail if the page is full
if ($page->is_full()) {
return;
}
// Generated content
$this->_set_content();
// Collapse margins if required
$this->_collapse_margins();
$style = $this->_frame->get_style();
$cb = $this->_frame->get_containing_block();
if ($style->position === "fixed") {
$cb = $this->_frame->get_root()->get_containing_block();
}
// Determine the constraints imposed by this frame: calculate the width
// of the content area:
list($w, $left_margin, $right_margin, $left, $right) = $this->_calculate_restricted_width();
// Store the calculated properties
$style->width = $w;
$style->margin_left = $left_margin;
$style->margin_right = $right_margin;
$style->left = $left;
$style->right = $right;
// Update the position
$this->_frame->position();
list($x, $y) = $this->_frame->get_position();
// Adjust the first line based on the text-indent property
$indent = (float)$style->length_in_pt($style->text_indent, $cb["w"]);
$this->_frame->increase_line_width($indent);
// Determine the content edge
$top = (float)$style->length_in_pt(array($style->margin_top,
$style->padding_top,
$style->border_top_width), $cb["h"]);
$bottom = (float)$style->length_in_pt(array($style->border_bottom_width,
$style->margin_bottom,
$style->padding_bottom), $cb["h"]);
$cb_x = $x + (float)$left_margin + (float)$style->length_in_pt(array($style->border_left_width,
$style->padding_left), $cb["w"]);
$cb_y = $y + $top;
$cb_h = ($cb["h"] + $cb["y"]) - $bottom - $cb_y;
// Set the y position of the first line in this block
$line_box = $this->_frame->get_current_line_box();
$line_box->y = $cb_y;
$line_box->get_float_offsets();
// Set the containing blocks and reflow each child
foreach ($this->_frame->get_children() as $child) {
// Bail out if the page is full
if ($page->is_full()) {
break;
}
$child->set_containing_block($cb_x, $cb_y, $w, $cb_h);
$this->process_clear($child);
$child->reflow($this->_frame);
// Don't add the child to the line if a page break has occurred
if ($page->check_page_break($child)) {
break;
}
$this->process_float($child, $cb_x, $w);
}
// Determine our height
list($height, $margin_top, $margin_bottom, $top, $bottom) = $this->_calculate_restricted_height();
$style->height = $height;
$style->margin_top = $margin_top;
$style->margin_bottom = $margin_bottom;
$style->top = $top;
$style->bottom = $bottom;
$orig_style = $this->_frame->get_original_style();
$needs_reposition = ($style->position === "absolute" && ($style->right !== "auto" || $style->bottom !== "auto"));
// Absolute positioning measurement
if ($needs_reposition) {
if ($orig_style->width === "auto" && ($orig_style->left === "auto" || $orig_style->right === "auto")) {
$width = 0;
foreach ($this->_frame->get_line_boxes() as $line) {
$width = max($line->w, $width);
}
$style->width = $width;
}
$style->left = $orig_style->left;
$style->right = $orig_style->right;
}
// Calculate inline-block / float auto-widths
if (($style->display === "inline-block" || $style->float !== 'none') && $orig_style->width === 'auto') {
$width = 0;
foreach ($this->_frame->get_line_boxes() as $line) {
$line->recalculate_width();
$width = max($line->w, $width);
}
if ($width === 0) {
foreach ($this->_frame->get_children() as $child) {
$width += $child->calculate_auto_width();
}
}
$style->width = $width;
}
$this->_text_align();
$this->vertical_align();
// Absolute positioning
if ($needs_reposition) {
list($x, $y) = $this->_frame->get_position();
$this->_frame->position();
list($new_x, $new_y) = $this->_frame->get_position();
$this->_frame->move($new_x - $x, $new_y - $y, true);
}
if ($block && $this->_frame->is_in_flow()) {
$block->add_frame_to_line($this->_frame);
// May be inline-block
if ($style->display === "block") {
$block->add_line();
}
}
}
/**
* Determine current frame width based on contents
*
* @return float
*/
public function calculate_auto_width()
{
$width = 0;
foreach ($this->_frame->get_line_boxes() as $line) {
$line_width = 0;
foreach ($line->get_frames() as $frame) {
if ($frame->get_original_style()->width == 'auto') {
$line_width += $frame->calculate_auto_width();
} else {
$line_width += $frame->get_margin_width();
}
}
$width = max($line_width, $width);
}
$this->_frame->get_style()->width = $width;
return $this->_frame->get_margin_width();
}
}
@@ -0,0 +1,204 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Helpers;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Image as ImageFrameDecorator;
/**
* Image reflower class
*
* @package dompdf
*/
class Image extends AbstractFrameReflower
{
/**
* Image constructor.
* @param ImageFrameDecorator $frame
*/
function __construct(ImageFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$this->_frame->position();
//FLOAT
//$frame = $this->_frame;
//$page = $frame->get_root();
//if ($frame->get_style()->float !== "none" ) {
// $page->add_floating_frame($this);
//}
// Set the frame's width
$this->get_min_max_width();
if ($block) {
$block->add_frame_to_line($this->_frame);
}
}
/**
* @return array
*/
function get_min_max_width()
{
if ($this->get_dompdf()->getOptions()->getDebugPng()) {
// Determine the image's size. Time consuming. Only when really needed?
list($img_width, $img_height) = Helpers::dompdf_getimagesize($this->_frame->get_image_url(), $this->get_dompdf()->getHttpContext());
print "get_min_max_width() " .
$this->_frame->get_style()->width . ' ' .
$this->_frame->get_style()->height . ';' .
$this->_frame->get_parent()->get_style()->width . " " .
$this->_frame->get_parent()->get_style()->height . ";" .
$this->_frame->get_parent()->get_parent()->get_style()->width . ' ' .
$this->_frame->get_parent()->get_parent()->get_style()->height . ';' .
$img_width . ' ' .
$img_height . '|';
}
$style = $this->_frame->get_style();
$width_forced = true;
$height_forced = true;
//own style auto or invalid value: use natural size in px
//own style value: ignore suffix text including unit, use given number as px
//own style %: walk up parent chain until found available space in pt; fill available space
//
//special ignored unit: e.g. 10ex: e treated as exponent; x ignored; 10e completely invalid ->like auto
$width = ($style->width > 0 ? $style->width : 0);
if (Helpers::is_percent($width)) {
$t = 0.0;
for ($f = $this->_frame->get_parent(); $f; $f = $f->get_parent()) {
$f_style = $f->get_style();
$t = $f_style->length_in_pt($f_style->width);
if ($t != 0) {
break;
}
}
$width = ((float)rtrim($width, "%") * $t) / 100; //maybe 0
} else {
// Don't set image original size if "%" branch was 0 or size not given.
// Otherwise aspect changed on %/auto combination for width/height
// Resample according to px per inch
// See also ListBulletImage::__construct
$width = $style->length_in_pt($width);
}
$height = ($style->height > 0 ? $style->height : 0);
if (Helpers::is_percent($height)) {
$t = 0.0;
for ($f = $this->_frame->get_parent(); $f; $f = $f->get_parent()) {
$f_style = $f->get_style();
$t = $f_style->length_in_pt($f_style->height);
if ($t != 0) {
break;
}
}
$height = ((float)rtrim($height, "%") * $t) / 100; //maybe 0
} else {
// Don't set image original size if "%" branch was 0 or size not given.
// Otherwise aspect changed on %/auto combination for width/height
// Resample according to px per inch
// See also ListBulletImage::__construct
$height = $style->length_in_pt($height);
}
if ($width == 0 || $height == 0) {
// Determine the image's size. Time consuming. Only when really needed!
list($img_width, $img_height) = Helpers::dompdf_getimagesize($this->_frame->get_image_url(), $this->get_dompdf()->getHttpContext());
// don't treat 0 as error. Can be downscaled or can be catched elsewhere if image not readable.
// Resample according to px per inch
// See also ListBulletImage::__construct
if ($width == 0 && $height == 0) {
$dpi = $this->_frame->get_dompdf()->getOptions()->getDpi();
$width = (float)($img_width * 72) / $dpi;
$height = (float)($img_height * 72) / $dpi;
$width_forced = false;
$height_forced = false;
} elseif ($height == 0 && $width != 0) {
$height_forced = false;
$height = ($width / $img_width) * $img_height; //keep aspect ratio
} elseif ($width == 0 && $height != 0) {
$width_forced = false;
$width = ($height / $img_height) * $img_width; //keep aspect ratio
}
}
// Handle min/max width/height
if ($style->min_width !== "none" ||
$style->max_width !== "none" ||
$style->min_height !== "none" ||
$style->max_height !== "none"
) {
list( /*$x*/, /*$y*/, $w, $h) = $this->_frame->get_containing_block();
$min_width = $style->length_in_pt($style->min_width, $w);
$max_width = $style->length_in_pt($style->max_width, $w);
$min_height = $style->length_in_pt($style->min_height, $h);
$max_height = $style->length_in_pt($style->max_height, $h);
if ($max_width !== "none" && $width > $max_width) {
if (!$height_forced) {
$height *= $max_width / $width;
}
$width = $max_width;
}
if ($min_width !== "none" && $width < $min_width) {
if (!$height_forced) {
$height *= $min_width / $width;
}
$width = $min_width;
}
if ($max_height !== "none" && $height > $max_height) {
if (!$width_forced) {
$width *= $max_height / $height;
}
$height = $max_height;
}
if ($min_height !== "none" && $height < $min_height) {
if (!$width_forced) {
$width *= $min_height / $height;
}
$height = $min_height;
}
}
if ($this->get_dompdf()->getOptions()->getDebugPng()) print $width . ' ' . $height . ';';
$style->width = $width . "pt";
$style->height = $height . "pt";
$style->min_width = "none";
$style->max_width = "none";
$style->min_height = "none";
$style->max_height = "none";
return array($width, $width, "min" => $width, "max" => $width);
}
}
@@ -0,0 +1,103 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Text as TextFrameDecorator;
/**
* Reflows inline frames
*
* @package dompdf
*/
class Inline extends AbstractFrameReflower
{
/**
* Inline constructor.
* @param Frame $frame
*/
function __construct(Frame $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$frame = $this->_frame;
// Check if a page break is forced
$page = $frame->get_root();
$page->check_forced_page_break($frame);
if ($page->is_full()) {
return;
}
$style = $frame->get_style();
// Generated content
$this->_set_content();
$frame->position();
$cb = $frame->get_containing_block();
// Add our margin, padding & border to the first and last children
if (($f = $frame->get_first_child()) && $f instanceof TextFrameDecorator) {
$f_style = $f->get_style();
$f_style->margin_left = $style->margin_left;
$f_style->padding_left = $style->padding_left;
$f_style->border_left = $style->border_left;
}
if (($l = $frame->get_last_child()) && $l instanceof TextFrameDecorator) {
$l_style = $l->get_style();
$l_style->margin_right = $style->margin_right;
$l_style->padding_right = $style->padding_right;
$l_style->border_right = $style->border_right;
}
if ($block) {
$block->add_frame_to_line($this->_frame);
}
// Set the containing blocks and reflow each child. The containing
// block is not changed by line boxes.
foreach ($frame->get_children() as $child) {
$child->set_containing_block($cb);
$child->reflow($block);
}
}
/**
* Determine current frame width based on contents
*
* @return float
*/
public function calculate_auto_width()
{
$width = 0;
foreach ($this->_frame->get_children() as $child) {
if ($child->get_original_style()->width == 'auto') {
$width += $child->calculate_auto_width();
} else {
$width += $child->get_margin_width();
}
}
$this->_frame->get_style()->width = $width;
return $this->_frame->get_margin_width();
}
}
@@ -0,0 +1,45 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Reflows list bullets
*
* @package dompdf
*/
class ListBullet extends AbstractFrameReflower
{
/**
* ListBullet constructor.
* @param AbstractFrameDecorator $frame
*/
function __construct(AbstractFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$style = $this->_frame->get_style();
$style->width = $this->_frame->get_width();
$this->_frame->position();
if ($style->list_style_position === "inside") {
$p = $this->_frame->find_block_parent();
$p->add_frame_to_line($this->_frame);
}
}
}
@@ -0,0 +1,39 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
/**
* Dummy reflower
*
* @package dompdf
*/
class NullFrameReflower extends AbstractFrameReflower
{
/**
* NullFrameReflower constructor.
* @param Frame $frame
*/
function __construct(Frame $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
return;
}
}
@@ -0,0 +1,205 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Page as PageFrameDecorator;
/**
* Reflows pages
*
* @package dompdf
*/
class Page extends AbstractFrameReflower
{
/**
* Cache of the callbacks array
*
* @var array
*/
private $_callbacks;
/**
* Cache of the canvas
*
* @var \Dompdf\Canvas
*/
private $_canvas;
/**
* Page constructor.
* @param PageFrameDecorator $frame
*/
function __construct(PageFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param Frame $frame
* @param $page_number
*/
function apply_page_style(Frame $frame, $page_number)
{
$style = $frame->get_style();
$page_styles = $style->get_stylesheet()->get_page_styles();
// http://www.w3.org/TR/CSS21/page.html#page-selectors
if (count($page_styles) > 1) {
$odd = $page_number % 2 == 1;
$first = $page_number == 1;
$style = clone $page_styles["base"];
// FIXME RTL
if ($odd && isset($page_styles[":right"])) {
$style->merge($page_styles[":right"]);
}
if ($odd && isset($page_styles[":odd"])) {
$style->merge($page_styles[":odd"]);
}
// FIXME RTL
if (!$odd && isset($page_styles[":left"])) {
$style->merge($page_styles[":left"]);
}
if (!$odd && isset($page_styles[":even"])) {
$style->merge($page_styles[":even"]);
}
if ($first && isset($page_styles[":first"])) {
$style->merge($page_styles[":first"]);
}
$frame->set_style($style);
}
}
/**
* Paged layout:
* http://www.w3.org/TR/CSS21/page.html
*
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$fixed_children = array();
$prev_child = null;
$child = $this->_frame->get_first_child();
$current_page = 0;
while ($child) {
$this->apply_page_style($this->_frame, $current_page + 1);
$style = $this->_frame->get_style();
// Pages are only concerned with margins
$cb = $this->_frame->get_containing_block();
$left = (float)$style->length_in_pt($style->margin_left, $cb["w"]);
$right = (float)$style->length_in_pt($style->margin_right, $cb["w"]);
$top = (float)$style->length_in_pt($style->margin_top, $cb["h"]);
$bottom = (float)$style->length_in_pt($style->margin_bottom, $cb["h"]);
$content_x = $cb["x"] + $left;
$content_y = $cb["y"] + $top;
$content_width = $cb["w"] - $left - $right;
$content_height = $cb["h"] - $top - $bottom;
// Only if it's the first page, we save the nodes with a fixed position
if ($current_page == 0) {
$children = $child->get_children();
foreach ($children as $onechild) {
if ($onechild->get_style()->position === "fixed") {
$fixed_children[] = $onechild->deep_copy();
}
}
$fixed_children = array_reverse($fixed_children);
}
$child->set_containing_block($content_x, $content_y, $content_width, $content_height);
// Check for begin reflow callback
$this->_check_callbacks("begin_page_reflow", $child);
//Insert a copy of each node which have a fixed position
if ($current_page >= 1) {
foreach ($fixed_children as $fixed_child) {
$child->insert_child_before($fixed_child->deep_copy(), $child->get_first_child());
}
}
$child->reflow();
$next_child = $child->get_next_sibling();
// Check for begin render callback
$this->_check_callbacks("begin_page_render", $child);
// Render the page
$this->_frame->get_renderer()->render($child);
// Check for end render callback
$this->_check_callbacks("end_page_render", $child);
if ($next_child) {
$this->_frame->next_page();
}
// Wait to dispose of all frames on the previous page
// so callback will have access to them
if ($prev_child) {
$prev_child->dispose(true);
}
$prev_child = $child;
$child = $next_child;
$current_page++;
}
// Dispose of previous page if it still exists
if ($prev_child) {
$prev_child->dispose(true);
}
}
/**
* Check for callbacks that need to be performed when a given event
* gets triggered on a page
*
* @param string $event the type of event
* @param Frame $frame the frame that event is triggered on
*/
protected function _check_callbacks($event, $frame)
{
if (!isset($this->_callbacks)) {
$dompdf = $this->_frame->get_dompdf();
$this->_callbacks = $dompdf->get_callbacks();
$this->_canvas = $dompdf->get_canvas();
}
if (is_array($this->_callbacks) && isset($this->_callbacks[$event])) {
$info = array(
0 => $this->_canvas, "canvas" => $this->_canvas,
1 => $frame, "frame" => $frame,
);
$fs = $this->_callbacks[$event];
foreach ($fs as $f) {
if (is_callable($f)) {
if (is_array($f)) {
$f[0]->{$f[1]}($info);
} else {
$f($info);
}
}
}
}
}
}
@@ -0,0 +1,121 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
/**
* Reflows table cells
*
* @package dompdf
*/
class TableCell extends Block
{
/**
* TableCell constructor.
* @param BlockFrameDecorator $frame
*/
function __construct(BlockFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$style = $this->_frame->get_style();
$table = TableFrameDecorator::find_parent_table($this->_frame);
$cellmap = $table->get_cellmap();
list($x, $y) = $cellmap->get_frame_position($this->_frame);
$this->_frame->set_position($x, $y);
$cells = $cellmap->get_spanned_cells($this->_frame);
$w = 0;
foreach ($cells["columns"] as $i) {
$col = $cellmap->get_column($i);
$w += $col["used-width"];
}
//FIXME?
$h = $this->_frame->get_containing_block("h");
$left_space = (float)$style->length_in_pt(array($style->margin_left,
$style->padding_left,
$style->border_left_width),
$w);
$right_space = (float)$style->length_in_pt(array($style->padding_right,
$style->margin_right,
$style->border_right_width),
$w);
$top_space = (float)$style->length_in_pt(array($style->margin_top,
$style->padding_top,
$style->border_top_width),
$h);
$bottom_space = (float)$style->length_in_pt(array($style->margin_bottom,
$style->padding_bottom,
$style->border_bottom_width),
$h);
$style->width = $cb_w = $w - $left_space - $right_space;
$content_x = $x + $left_space;
$content_y = $line_y = $y + $top_space;
// Adjust the first line based on the text-indent property
$indent = (float)$style->length_in_pt($style->text_indent, $w);
$this->_frame->increase_line_width($indent);
$page = $this->_frame->get_root();
// Set the y position of the first line in the cell
$line_box = $this->_frame->get_current_line_box();
$line_box->y = $line_y;
// Set the containing blocks and reflow each child
foreach ($this->_frame->get_children() as $child) {
if ($page->is_full()) {
break;
}
$child->set_containing_block($content_x, $content_y, $cb_w, $h);
$this->process_clear($child);
$child->reflow($this->_frame);
$this->process_float($child, $x + $left_space, $w - $right_space - $left_space);
}
// Determine our height
$style_height = (float)$style->length_in_pt($style->height, $h);
$this->_frame->set_content_height($this->_calculate_content_height());
$height = max($style_height, (float)$this->_frame->get_content_height());
// Let the cellmap know our height
$cell_height = $height / count($cells["rows"]);
if ($style_height <= $height) {
$cell_height += $top_space + $bottom_space;
}
foreach ($cells["rows"] as $i) {
$cellmap->set_row_height($i, $cell_height);
}
$style->height = $height;
$this->_text_align();
$this->vertical_align();
}
}
@@ -0,0 +1,72 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
/**
* Reflows table row groups (e.g. tbody tags)
*
* @package dompdf
*/
class TableRowGroup extends AbstractFrameReflower
{
/**
* TableRowGroup constructor.
* @param \Dompdf\Frame $frame
*/
function __construct($frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$page = $this->_frame->get_root();
$style = $this->_frame->get_style();
// Our width is equal to the width of our parent table
$table = TableFrameDecorator::find_parent_table($this->_frame);
$cb = $this->_frame->get_containing_block();
foreach ($this->_frame->get_children() as $child) {
// Bail if the page is full
if ($page->is_full()) {
return;
}
$child->set_containing_block($cb["x"], $cb["y"], $cb["w"], $cb["h"]);
$child->reflow();
// Check if a split has occured
$page->check_page_break($child);
}
if ($page->is_full()) {
return;
}
$cellmap = $table->get_cellmap();
$style->width = $cellmap->get_frame_width($this->_frame);
$style->height = $cellmap->get_frame_height($this->_frame);
$this->_frame->set_position($cellmap->get_frame_position($this->_frame));
if ($table->get_style()->border_collapse === "collapse") {
// Unset our borders because our cells are now using them
$style->border_style = "none";
}
}
}
@@ -0,0 +1,74 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
use Dompdf\FrameDecorator\TableRow as TableRowFrameDecorator;
use Dompdf\Exception;
/**
* Reflows table rows
*
* @package dompdf
*/
class TableRow extends AbstractFrameReflower
{
/**
* TableRow constructor.
* @param TableRowFrameDecorator $frame
*/
function __construct(TableRowFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$page = $this->_frame->get_root();
if ($page->is_full()) {
return;
}
$this->_frame->position();
$style = $this->_frame->get_style();
$cb = $this->_frame->get_containing_block();
foreach ($this->_frame->get_children() as $child) {
if ($page->is_full()) {
return;
}
$child->set_containing_block($cb);
$child->reflow();
}
if ($page->is_full()) {
return;
}
$table = TableFrameDecorator::find_parent_table($this->_frame);
$cellmap = $table->get_cellmap();
$style->width = $cellmap->get_frame_width($this->_frame);
$style->height = $cellmap->get_frame_height($this->_frame);
$this->_frame->set_position($cellmap->get_frame_position($this->_frame));
}
/**
* @throws Exception
*/
function get_min_max_width()
{
throw new Exception("Min/max width is undefined for table rows");
}
}
@@ -0,0 +1,588 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Table as TableFrameDecorator;
/**
* Reflows tables
*
* @access private
* @package dompdf
*/
class Table extends AbstractFrameReflower
{
/**
* Frame for this reflower
*
* @var TableFrameDecorator
*/
protected $_frame;
/**
* Cache of results between call to get_min_max_width and assign_widths
*
* @var array
*/
protected $_state;
/**
* Table constructor.
* @param TableFrameDecorator $frame
*/
function __construct(TableFrameDecorator $frame)
{
$this->_state = null;
parent::__construct($frame);
}
/**
* State is held here so it needs to be reset along with the decorator
*/
function reset()
{
$this->_state = null;
$this->_min_max_cache = null;
}
protected function _assign_widths()
{
$style = $this->_frame->get_style();
// Find the min/max width of the table and sort the columns into
// absolute/percent/auto arrays
$min_width = $this->_state["min_width"];
$max_width = $this->_state["max_width"];
$percent_used = $this->_state["percent_used"];
$absolute_used = $this->_state["absolute_used"];
$auto_min = $this->_state["auto_min"];
$absolute =& $this->_state["absolute"];
$percent =& $this->_state["percent"];
$auto =& $this->_state["auto"];
// Determine the actual width of the table
$cb = $this->_frame->get_containing_block();
$columns =& $this->_frame->get_cellmap()->get_columns();
$width = $style->width;
// Calculate padding & border fudge factor
$left = $style->margin_left;
$right = $style->margin_right;
$centered = ($left === "auto" && $right === "auto");
$left = (float)($left === "auto" ? 0 : $style->length_in_pt($left, $cb["w"]));
$right = (float)($right === "auto" ? 0 : $style->length_in_pt($right, $cb["w"]));
$delta = $left + $right;
if (!$centered) {
$delta += (float)$style->length_in_pt(array(
$style->padding_left,
$style->border_left_width,
$style->border_right_width,
$style->padding_right),
$cb["w"]);
}
$min_table_width = (float)$style->length_in_pt($style->min_width, $cb["w"] - $delta);
// min & max widths already include borders & padding
$min_width -= $delta;
$max_width -= $delta;
if ($width !== "auto") {
$preferred_width = (float)$style->length_in_pt($width, $cb["w"]) - $delta;
if ($preferred_width < $min_table_width) {
$preferred_width = $min_table_width;
}
if ($preferred_width > $min_width) {
$width = $preferred_width;
} else {
$width = $min_width;
}
} else {
if ($max_width + $delta < $cb["w"]) {
$width = $max_width;
} else if ($cb["w"] - $delta > $min_width) {
$width = $cb["w"] - $delta;
} else {
$width = $min_width;
}
if ($width < $min_table_width) {
$width = $min_table_width;
}
}
// Store our resolved width
$style->width = $width;
$cellmap = $this->_frame->get_cellmap();
if ($cellmap->is_columns_locked()) {
return;
}
// If the whole table fits on the page, then assign each column it's max width
if ($width == $max_width) {
foreach (array_keys($columns) as $i) {
$cellmap->set_column_width($i, $columns[$i]["max-width"]);
}
return;
}
// Determine leftover and assign it evenly to all columns
if ($width > $min_width) {
// We have four cases to deal with:
//
// 1. All columns are auto--no widths have been specified. In this
// case we distribute extra space across all columns weighted by max-width.
//
// 2. Only absolute widths have been specified. In this case we
// distribute any extra space equally among 'width: auto' columns, or all
// columns if no auto columns have been specified.
//
// 3. Only percentage widths have been specified. In this case we
// normalize the percentage values and distribute any remaining % to
// width: auto columns. We then proceed to assign widths as fractions
// of the table width.
//
// 4. Both absolute and percentage widths have been specified.
$increment = 0;
// Case 1:
if ($absolute_used == 0 && $percent_used == 0) {
$increment = $width - $min_width;
foreach (array_keys($columns) as $i) {
$cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment * ($columns[$i]["max-width"] / $max_width));
}
return;
}
// Case 2
if ($absolute_used > 0 && $percent_used == 0) {
if (count($auto) > 0) {
$increment = ($width - $auto_min - $absolute_used) / count($auto);
}
// Use the absolutely specified width or the increment
foreach (array_keys($columns) as $i) {
if ($columns[$i]["absolute"] > 0 && count($auto)) {
$cellmap->set_column_width($i, $columns[$i]["min-width"]);
} else if (count($auto)) {
$cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
} else {
// All absolute columns
$increment = ($width - $absolute_used) * $columns[$i]["absolute"] / $absolute_used;
$cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
}
}
return;
}
// Case 3:
if ($absolute_used == 0 && $percent_used > 0) {
$scale = null;
$remaining = null;
// Scale percent values if the total percentage is > 100, or if all
// values are specified as percentages.
if ($percent_used > 100 || count($auto) == 0) {
$scale = 100 / $percent_used;
} else {
$scale = 1;
}
// Account for the minimum space used by the unassigned auto columns
$used_width = $auto_min;
foreach ($percent as $i) {
$columns[$i]["percent"] *= $scale;
$slack = $width - $used_width;
$w = min($columns[$i]["percent"] * $width / 100, $slack);
if ($w < $columns[$i]["min-width"]) {
$w = $columns[$i]["min-width"];
}
$cellmap->set_column_width($i, $w);
$used_width += $w;
}
// This works because $used_width includes the min-width of each
// unassigned column
if (count($auto) > 0) {
$increment = ($width - $used_width) / count($auto);
foreach ($auto as $i) {
$cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
}
}
return;
}
// Case 4:
// First-come, first served
if ($absolute_used > 0 && $percent_used > 0) {
$used_width = $auto_min;
foreach ($absolute as $i) {
$cellmap->set_column_width($i, $columns[$i]["min-width"]);
$used_width += $columns[$i]["min-width"];
}
// Scale percent values if the total percentage is > 100 or there
// are no auto values to take up slack
if ($percent_used > 100 || count($auto) == 0) {
$scale = 100 / $percent_used;
} else {
$scale = 1;
}
$remaining_width = $width - $used_width;
foreach ($percent as $i) {
$slack = $remaining_width - $used_width;
$columns[$i]["percent"] *= $scale;
$w = min($columns[$i]["percent"] * $remaining_width / 100, $slack);
if ($w < $columns[$i]["min-width"])
$w = $columns[$i]["min-width"];
$columns[$i]["used-width"] = $w;
$used_width += $w;
}
if (count($auto) > 0) {
$increment = ($width - $used_width) / count($auto);
foreach ($auto as $i) {
$cellmap->set_column_width($i, $columns[$i]["min-width"] + $increment);
}
}
return;
}
} else { // we are over constrained
// Each column gets its minimum width
foreach (array_keys($columns) as $i) {
$cellmap->set_column_width($i, $columns[$i]["min-width"]);
}
}
}
/**
* Determine the frame's height based on min/max height
*
* @return float|int|mixed|string
*/
protected function _calculate_height()
{
$style = $this->_frame->get_style();
$height = $style->height;
$cellmap = $this->_frame->get_cellmap();
$cellmap->assign_frame_heights();
$rows = $cellmap->get_rows();
// Determine our content height
$content_height = 0;
foreach ($rows as $r) {
$content_height += $r["height"];
}
$cb = $this->_frame->get_containing_block();
if (!($style->overflow === "visible" ||
($style->overflow === "hidden" && $height === "auto"))
) {
// Only handle min/max height if the height is independent of the frame's content
$min_height = $style->min_height;
$max_height = $style->max_height;
if (isset($cb["h"])) {
$min_height = $style->length_in_pt($min_height, $cb["h"]);
$max_height = $style->length_in_pt($max_height, $cb["h"]);
} else if (isset($cb["w"])) {
if (mb_strpos($min_height, "%") !== false) {
$min_height = 0;
} else {
$min_height = $style->length_in_pt($min_height, $cb["w"]);
}
if (mb_strpos($max_height, "%") !== false) {
$max_height = "none";
} else {
$max_height = $style->length_in_pt($max_height, $cb["w"]);
}
}
if ($max_height !== "none" && $min_height > $max_height) {
// Swap 'em
list($max_height, $min_height) = array($min_height, $max_height);
}
if ($max_height !== "none" && $height > $max_height) {
$height = $max_height;
}
if ($height < $min_height) {
$height = $min_height;
}
} else {
// Use the content height or the height value, whichever is greater
if ($height !== "auto") {
$height = $style->length_in_pt($height, $cb["h"]);
if ($height <= $content_height) {
$height = $content_height;
} else {
$cellmap->set_frame_heights($height, $content_height);
}
} else {
$height = $content_height;
}
}
return $height;
}
/**
* @param BlockFrameDecorator $block
*/
function reflow(BlockFrameDecorator $block = null)
{
/** @var TableFrameDecorator */
$frame = $this->_frame;
// Check if a page break is forced
$page = $frame->get_root();
$page->check_forced_page_break($frame);
// Bail if the page is full
if ($page->is_full()) {
return;
}
// Let the page know that we're reflowing a table so that splits
// are suppressed (simply setting page-break-inside: avoid won't
// work because we may have an arbitrary number of block elements
// inside tds.)
$page->table_reflow_start();
// Collapse vertical margins, if required
$this->_collapse_margins();
$frame->position();
// Table layout algorithm:
// http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
if (is_null($this->_state)) {
$this->get_min_max_width();
}
$cb = $frame->get_containing_block();
$style = $frame->get_style();
// This is slightly inexact, but should be okay. Add half the
// border-spacing to the table as padding. The other half is added to
// the cells themselves.
if ($style->border_collapse === "separate") {
list($h, $v) = $style->border_spacing;
$v = (float)$style->length_in_pt($v) / 2;
$h = (float)$style->length_in_pt($h) / 2;
$style->padding_left = (float)$style->length_in_pt($style->padding_left, $cb["w"]) + $h;
$style->padding_right = (float)$style->length_in_pt($style->padding_right, $cb["w"]) + $h;
$style->padding_top = (float)$style->length_in_pt($style->padding_top, $cb["h"]) + $v;
$style->padding_bottom = (float)$style->length_in_pt($style->padding_bottom, $cb["h"]) + $v;
}
$this->_assign_widths();
// Adjust left & right margins, if they are auto
$width = $style->width;
$left = $style->margin_left;
$right = $style->margin_right;
$diff = $cb["w"] - $width;
if ($left === "auto" && $right === "auto") {
if ($diff < 0) {
$left = 0;
$right = $diff;
} else {
$left = $right = $diff / 2;
}
$style->margin_left = sprintf("%Fpt", $left);
$style->margin_right = sprintf("%Fpt", $right);;
} else {
if ($left === "auto") {
$left = (float)$style->length_in_pt($cb["w"] - $right - $width, $cb["w"]);
}
if ($right === "auto") {
$left = (float)$style->length_in_pt($left, $cb["w"]);
}
}
list($x, $y) = $frame->get_position();
// Determine the content edge
$content_x = $x + (float)$left + (float)$style->length_in_pt(array($style->padding_left,
$style->border_left_width), $cb["w"]);
$content_y = $y + (float)$style->length_in_pt(array($style->margin_top,
$style->border_top_width,
$style->padding_top), $cb["h"]);
if (isset($cb["h"])) {
$h = $cb["h"];
} else {
$h = null;
}
$cellmap = $frame->get_cellmap();
$col =& $cellmap->get_column(0);
$col["x"] = $content_x;
$row =& $cellmap->get_row(0);
$row["y"] = $content_y;
$cellmap->assign_x_positions();
// Set the containing block of each child & reflow
foreach ($frame->get_children() as $child) {
// Bail if the page is full
if (!$page->in_nested_table() && $page->is_full()) {
break;
}
$child->set_containing_block($content_x, $content_y, $width, $h);
$child->reflow();
if (!$page->in_nested_table()) {
// Check if a split has occured
$page->check_page_break($child);
}
}
// Assign heights to our cells:
$style->height = $this->_calculate_height();
if ($style->border_collapse === "collapse") {
// Unset our borders because our cells are now using them
$style->border_style = "none";
}
$page->table_reflow_end();
// Debugging:
//echo ($this->_frame->get_cellmap());
if ($block && $style->float === "none" && $frame->is_in_flow()) {
$block->add_frame_to_line($frame);
$block->add_line();
}
}
/**
* @return array|null
*/
function get_min_max_width()
{
if (!is_null($this->_min_max_cache)) {
return $this->_min_max_cache;
}
$style = $this->_frame->get_style();
$this->_frame->normalise();
// Add the cells to the cellmap (this will calcluate column widths as
// frames are added)
$this->_frame->get_cellmap()->add_frame($this->_frame);
// Find the min/max width of the table and sort the columns into
// absolute/percent/auto arrays
$this->_state = array();
$this->_state["min_width"] = 0;
$this->_state["max_width"] = 0;
$this->_state["percent_used"] = 0;
$this->_state["absolute_used"] = 0;
$this->_state["auto_min"] = 0;
$this->_state["absolute"] = array();
$this->_state["percent"] = array();
$this->_state["auto"] = array();
$columns =& $this->_frame->get_cellmap()->get_columns();
foreach (array_keys($columns) as $i) {
$this->_state["min_width"] += $columns[$i]["min-width"];
$this->_state["max_width"] += $columns[$i]["max-width"];
if ($columns[$i]["absolute"] > 0) {
$this->_state["absolute"][] = $i;
$this->_state["absolute_used"] += $columns[$i]["absolute"];
} else if ($columns[$i]["percent"] > 0) {
$this->_state["percent"][] = $i;
$this->_state["percent_used"] += $columns[$i]["percent"];
} else {
$this->_state["auto"][] = $i;
$this->_state["auto_min"] += $columns[$i]["min-width"];
}
}
// Account for margins & padding
$dims = array($style->border_left_width,
$style->border_right_width,
$style->padding_left,
$style->padding_right,
$style->margin_left,
$style->margin_right);
if ($style->border_collapse !== "collapse") {
list($dims[]) = $style->border_spacing;
}
$delta = (float)$style->length_in_pt($dims, $this->_frame->get_containing_block("w"));
$this->_state["min_width"] += $delta;
$this->_state["max_width"] += $delta;
return $this->_min_max_cache = array(
$this->_state["min_width"],
$this->_state["max_width"],
"min" => $this->_state["min_width"],
"max" => $this->_state["max_width"],
);
}
}
@@ -0,0 +1,500 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\FrameDecorator\Text as TextFrameDecorator;
use Dompdf\FontMetrics;
/**
* Reflows text frames.
*
* @package dompdf
*/
class Text extends AbstractFrameReflower
{
/**
* @var BlockFrameDecorator
*/
protected $_block_parent; // Nearest block-level ancestor
/**
* @var TextFrameDecorator
*/
protected $_frame;
public static $_whitespace_pattern = "/[ \t\r\n\f]+/u";
/**
* @var FontMetrics
*/
private $fontMetrics;
/**
* @param TextFrameDecorator $frame
* @param FontMetrics $fontMetrics
*/
public function __construct(TextFrameDecorator $frame, FontMetrics $fontMetrics)
{
parent::__construct($frame);
$this->setFontMetrics($fontMetrics);
}
/**
* @param $text
* @return mixed
*/
protected function _collapse_white_space($text)
{
//$text = $this->_frame->get_text();
// if ( $this->_block_parent->get_current_line_box->w == 0 )
// $text = ltrim($text, " \n\r\t");
return preg_replace(self::$_whitespace_pattern, " ", $text);
}
/**
* @param $text
* @return bool|int
*/
protected function _line_break($text)
{
$style = $this->_frame->get_style();
$size = $style->font_size;
$font = $style->font_family;
$current_line = $this->_block_parent->get_current_line_box();
// Determine the available width
$line_width = $this->_frame->get_containing_block("w");
$current_line_width = $current_line->left + $current_line->w + $current_line->right;
$available_width = $line_width - $current_line_width;
// Account for word-spacing
$word_spacing = (float)$style->length_in_pt($style->word_spacing);
$char_spacing = (float)$style->length_in_pt($style->letter_spacing);
// Determine the frame width including margin, padding & border
$text_width = $this->getFontMetrics()->getTextWidth($text, $font, $size, $word_spacing, $char_spacing);
$mbp_width =
(float)$style->length_in_pt(array($style->margin_left,
$style->border_left_width,
$style->padding_left,
$style->padding_right,
$style->border_right_width,
$style->margin_right), $line_width);
$frame_width = $text_width + $mbp_width;
// Debugging:
// Helpers::pre_r("Text: '" . htmlspecialchars($text). "'");
// Helpers::pre_r("width: " .$frame_width);
// Helpers::pre_r("textwidth + delta: $text_width + $mbp_width");
// Helpers::pre_r("font-size: $size");
// Helpers::pre_r("cb[w]: " .$line_width);
// Helpers::pre_r("available width: " . $available_width);
// Helpers::pre_r("current line width: " . $current_line_width);
// Helpers::pre_r($words);
if ($frame_width <= $available_width) {
return false;
}
// split the text into words
$words = preg_split('/([\s-]+)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$wc = count($words);
// Determine the split point
$width = 0;
$str = "";
reset($words);
// @todo support <shy>, <wbr>
for ($i = 0; $i < $wc; $i += 2) {
$word = $words[$i] . (isset($words[$i + 1]) ? $words[$i + 1] : "");
$word_width = $this->getFontMetrics()->getTextWidth($word, $font, $size, $word_spacing, $char_spacing);
if ($width + $word_width + $mbp_width > $available_width) {
break;
}
$width += $word_width;
$str .= $word;
}
$break_word = ($style->word_wrap === "break-word");
// The first word has overflowed. Force it onto the line
if ($current_line_width == 0 && $width == 0) {
$s = "";
$last_width = 0;
if ($break_word) {
for ($j = 0; $j < strlen($word); $j++) {
$s .= $word[$j];
$_width = $this->getFontMetrics()->getTextWidth($s, $font, $size, $word_spacing, $char_spacing);
if ($_width > $available_width) {
break;
}
$last_width = $_width;
}
}
if ($break_word && $last_width > 0) {
//$width += $last_width;
$str .= substr($s, 0, -1);
} else {
//$width += $word_width;
$str .= $word;
}
}
$offset = mb_strlen($str);
// More debugging:
// var_dump($str);
// print_r("Width: ". $width);
// print_r("Offset: " . $offset);
return $offset;
}
//........................................................................
/**
* @param $text
* @return bool|int
*/
protected function _newline_break($text)
{
if (($i = mb_strpos($text, "\n")) === false) {
return false;
}
return $i + 1;
}
/**
*
*/
protected function _layout_line()
{
$frame = $this->_frame;
$style = $frame->get_style();
$text = $frame->get_text();
$size = $style->font_size;
$font = $style->font_family;
// Determine the text height
$style->height = $this->getFontMetrics()->getFontHeight($font, $size);
$split = false;
$add_line = false;
// Handle text transform:
// http://www.w3.org/TR/CSS21/text.html#propdef-text-transform
switch (strtolower($style->text_transform)) {
default:
break;
case "capitalize":
$text = mb_convert_case($text, MB_CASE_TITLE);
break;
case "uppercase":
$text = mb_convert_case($text, MB_CASE_UPPER);
break;
case "lowercase":
$text = mb_convert_case($text, MB_CASE_LOWER);
break;
}
// Handle white-space property:
// http://www.w3.org/TR/CSS21/text.html#propdef-white-space
switch ($style->white_space) {
default:
case "normal":
$frame->set_text($text = $this->_collapse_white_space($text));
if ($text == "")
break;
$split = $this->_line_break($text);
break;
case "pre":
$split = $this->_newline_break($text);
$add_line = $split !== false;
break;
case "nowrap":
$frame->set_text($text = $this->_collapse_white_space($text));
break;
case "pre-wrap":
$split = $this->_newline_break($text);
if (($tmp = $this->_line_break($text)) !== false) {
$add_line = $split < $tmp;
$split = min($tmp, $split);
} else
$add_line = true;
break;
case "pre-line":
// Collapse white-space except for \n
$frame->set_text($text = preg_replace("/[ \t]+/u", " ", $text));
if ($text == "") {
break;
}
$split = $this->_newline_break($text);
if (($tmp = $this->_line_break($text)) !== false) {
$add_line = $split < $tmp;
$split = min($tmp, $split);
} else {
$add_line = true;
}
break;
}
// Handle degenerate case
if ($text === "") {
return;
}
if ($split !== false) {
// Handle edge cases
if ($split == 0 && $text === " ") {
$frame->set_text("");
return;
}
if ($split == 0) {
// Trim newlines from the beginning of the line
//$this->_frame->set_text(ltrim($text, "\n\r"));
$this->_block_parent->maximize_line_height($style->height, $frame);
$this->_block_parent->add_line();
$frame->position();
// Layout the new line
$this->_layout_line();
} else if ($split < mb_strlen($frame->get_text())) {
// split the line if required
$frame->split_text($split);
$t = $frame->get_text();
// Remove any trailing newlines
if ($split > 1 && $t[$split - 1] === "\n" && !$frame->is_pre())
$frame->set_text(mb_substr($t, 0, -1));
// Do we need to trim spaces on wrapped lines? This might be desired, however, we
// can't trim the lines here or the layout will be affected if trimming the line
// leaves enough space to fit the next word in the text stream (because pdf layout
// is performed elsewhere).
/*if (!$this->_frame->get_prev_sibling() && !$this->_frame->get_next_sibling()) {
$t = $this->_frame->get_text();
$this->_frame->set_text( trim($t) );
}*/
}
if ($add_line) {
$this->_block_parent->add_line();
$frame->position();
}
} else {
// Remove empty space from start and end of line, but only where there isn't an inline sibling
// and the parent node isn't an inline element with siblings
// FIXME: Include non-breaking spaces?
$t = $frame->get_text();
$parent = $frame->get_parent();
$is_inline_frame = ($parent instanceof \Dompdf\FrameDecorator\Inline);
if ((!$is_inline_frame && !$frame->get_next_sibling()) /* ||
( $is_inline_frame && !$parent->get_next_sibling())*/
) { // fails <b>BOLD <u>UNDERLINED</u></b> becomes <b>BOLD<u>UNDERLINED</u></b>
$t = rtrim($t);
}
if ((!$is_inline_frame && !$frame->get_prev_sibling()) /* ||
( $is_inline_frame && !$parent->get_prev_sibling())*/
) { // <span><span>A<span>B</span> C</span></span> fails (the whitespace is removed)
$t = ltrim($t);
}
$frame->set_text($t);
}
// Set our new width
$width = $frame->recalculate_width();
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$frame = $this->_frame;
$page = $frame->get_root();
$page->check_forced_page_break($this->_frame);
if ($page->is_full()) {
return;
}
$this->_block_parent = /*isset($block) ? $block : */
$frame->find_block_parent();
// Left trim the text if this is the first text on the line and we're
// collapsing white space
// if ( $this->_block_parent->get_current_line()->w == 0 &&
// ($frame->get_style()->white_space !== "pre" ||
// $frame->get_style()->white_space !== "pre-wrap") ) {
// $frame->set_text( ltrim( $frame->get_text() ) );
// }
$frame->position();
$this->_layout_line();
if ($block) {
$block->add_frame_to_line($frame);
}
}
//........................................................................
// Returns an array(0 => min, 1 => max, "min" => min, "max" => max) of the
// minimum and maximum widths of this frame
function get_min_max_width()
{
/*if ( !is_null($this->_min_max_cache) )
return $this->_min_max_cache;*/
$frame = $this->_frame;
$style = $frame->get_style();
$this->_block_parent = $frame->find_block_parent();
$line_width = $frame->get_containing_block("w");
$str = $text = $frame->get_text();
$size = $style->font_size;
$font = $style->font_family;
$word_spacing = (float)$style->length_in_pt($style->word_spacing);
$char_spacing = (float)$style->length_in_pt($style->letter_spacing);
switch ($style->white_space) {
default:
case "normal":
$str = preg_replace(self::$_whitespace_pattern, " ", $str);
case "pre-wrap":
case "pre-line":
// Find the longest word (i.e. minimum length)
// This technique (using arrays & an anonymous function) is actually
// faster than doing a single-pass character by character scan. Heh,
// yes I took the time to bench it ;)
$words = array_flip(preg_split("/[\s-]+/u", $str, -1, PREG_SPLIT_DELIM_CAPTURE));
$root = $this;
array_walk($words, function(&$val, $str) use ($font, $size, $word_spacing, $char_spacing, $root) {
$val = $root->getFontMetrics()->getTextWidth($str, $font, $size, $word_spacing, $char_spacing);
});
arsort($words);
$min = reset($words);
break;
case "pre":
$lines = array_flip(preg_split("/\n/u", $str));
$root = $this;
array_walk($lines, function(&$val, $str) use ($font, $size, $word_spacing, $char_spacing, $root) {
$val = $root->getFontMetrics()->getTextWidth($str, $font, $size, $word_spacing, $char_spacing);
});
arsort($lines);
$min = reset($lines);
break;
case "nowrap":
$min = $this->getFontMetrics()->getTextWidth($this->_collapse_white_space($str), $font, $size, $word_spacing, $char_spacing);
break;
}
switch ($style->white_space) {
default:
case "normal":
case "nowrap":
$str = preg_replace(self::$_whitespace_pattern, " ", $text);
break;
case "pre-line":
//XXX: Is this correct?
$str = preg_replace("/[ \t]+/u", " ", $text);
case "pre-wrap":
// Find the longest word (i.e. minimum length)
$lines = array_flip(preg_split("/\n/", $text));
$root = $this;
array_walk($lines, function(&$val, $str) use ($font, $size, $word_spacing, $char_spacing, $root) {
$val = $root->getFontMetrics()->getTextWidth($str, $font, $size, $word_spacing, $char_spacing);
});
arsort($lines);
reset($lines);
$str = key($lines);
break;
}
$max = $this->getFontMetrics()->getTextWidth($str, $font, $size, $word_spacing, $char_spacing);
$delta = (float)$style->length_in_pt(array($style->margin_left,
$style->border_left_width,
$style->padding_left,
$style->padding_right,
$style->border_right_width,
$style->margin_right), $line_width);
$min += $delta;
$max += $delta;
return $this->_min_max_cache = array($min, $max, "min" => $min, "max" => $max);
}
/**
* @param FontMetrics $fontMetrics
* @return $this
*/
public function setFontMetrics(FontMetrics $fontMetrics)
{
$this->fontMetrics = $fontMetrics;
return $this;
}
/**
* @return FontMetrics
*/
public function getFontMetrics()
{
return $this->fontMetrics;
}
/**
* Determine current frame width based on contents
*
* @return float
*/
public function calculate_auto_width()
{
return $this->_frame->recalculate_width();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,828 @@
<?php
namespace Dompdf;
class Helpers
{
/**
* print_r wrapper for html/cli output
*
* Wraps print_r() output in < pre > tags if the current sapi is not 'cli'.
* Returns the output string instead of displaying it if $return is true.
*
* @param mixed $mixed variable or expression to display
* @param bool $return
*
* @return string|null
*/
public static function pre_r($mixed, $return = false)
{
if ($return) {
return "<pre>" . print_r($mixed, true) . "</pre>";
}
if (php_sapi_name() !== "cli") {
echo "<pre>";
}
print_r($mixed);
if (php_sapi_name() !== "cli") {
echo "</pre>";
} else {
echo "\n";
}
flush();
return null;
}
/**
* builds a full url given a protocol, hostname, base path and url
*
* @param string $protocol
* @param string $host
* @param string $base_path
* @param string $url
* @return string
*
* Initially the trailing slash of $base_path was optional, and conditionally appended.
* However on dynamically created sites, where the page is given as url parameter,
* the base path might not end with an url.
* Therefore do not append a slash, and **require** the $base_url to ending in a slash
* when needed.
* Vice versa, on using the local file system path of a file, make sure that the slash
* is appended (o.k. also for Windows)
*/
public static function build_url($protocol, $host, $base_path, $url)
{
$protocol = mb_strtolower($protocol);
if (strlen($url) == 0) {
//return $protocol . $host . rtrim($base_path, "/\\") . "/";
return $protocol . $host . $base_path;
}
// Is the url already fully qualified, a Data URI, or a reference to a named anchor?
if (mb_strpos($url, "://") !== false || mb_substr($url, 0, 1) === "#" || mb_strpos($url, "data:") === 0 || mb_strpos($url, "mailto:") === 0) {
return $url;
}
$ret = $protocol;
if (!in_array(mb_strtolower($protocol), array("http://", "https://", "ftp://", "ftps://"))) {
//On Windows local file, an abs path can begin also with a '\' or a drive letter and colon
//drive: followed by a relative path would be a drive specific default folder.
//not known in php app code, treat as abs path
//($url[1] !== ':' || ($url[2]!=='\\' && $url[2]!=='/'))
if ($url[0] !== '/' && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN' || (mb_strlen($url) > 1 && $url[0] !== '\\' && $url[1] !== ':'))) {
// For rel path and local acess we ignore the host, and run the path through realpath()
$ret .= realpath($base_path) . '/';
}
$ret .= $url;
$ret = preg_replace('/\?(.*)$/', "", $ret);
return $ret;
}
// Protocol relative urls (e.g. "//example.org/style.css")
if (strpos($url, '//') === 0) {
$ret .= substr($url, 2);
//remote urls with backslash in html/css are not really correct, but lets be genereous
} elseif ($url[0] === '/' || $url[0] === '\\') {
// Absolute path
$ret .= $host . $url;
} else {
// Relative path
//$base_path = $base_path !== "" ? rtrim($base_path, "/\\") . "/" : "";
$ret .= $host . $base_path . $url;
}
return $ret;
}
/**
* Converts decimal numbers to roman numerals
*
* @param int $num
*
* @throws Exception
* @return string
*/
public static function dec2roman($num)
{
static $ones = array("", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix");
static $tens = array("", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc");
static $hund = array("", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm");
static $thou = array("", "m", "mm", "mmm");
if (!is_numeric($num)) {
throw new Exception("dec2roman() requires a numeric argument.");
}
if ($num > 4000 || $num < 0) {
return "(out of range)";
}
$num = strrev((string)$num);
$ret = "";
switch (mb_strlen($num)) {
/** @noinspection PhpMissingBreakStatementInspection */
case 4:
$ret .= $thou[$num[3]];
/** @noinspection PhpMissingBreakStatementInspection */
case 3:
$ret .= $hund[$num[2]];
/** @noinspection PhpMissingBreakStatementInspection */
case 2:
$ret .= $tens[$num[1]];
/** @noinspection PhpMissingBreakStatementInspection */
case 1:
$ret .= $ones[$num[0]];
default:
break;
}
return $ret;
}
/**
* Determines whether $value is a percentage or not
*
* @param float $value
*
* @return bool
*/
public static function is_percent($value)
{
return false !== mb_strpos($value, "%");
}
/**
* Parses a data URI scheme
* http://en.wikipedia.org/wiki/Data_URI_scheme
*
* @param string $data_uri The data URI to parse
*
* @return array|bool The result with charset, mime type and decoded data
*/
public static function parse_data_uri($data_uri)
{
if (!preg_match('/^data:(?P<mime>[a-z0-9\/+-.]+)(;charset=(?P<charset>[a-z0-9-])+)?(?P<base64>;base64)?\,(?P<data>.*)?/is', $data_uri, $match)) {
return false;
}
$match['data'] = rawurldecode($match['data']);
$result = array(
'charset' => $match['charset'] ? $match['charset'] : 'US-ASCII',
'mime' => $match['mime'] ? $match['mime'] : 'text/plain',
'data' => $match['base64'] ? base64_decode($match['data']) : $match['data'],
);
return $result;
}
/**
* Encodes a Uniform Resource Identifier (URI) by replacing non-alphanumeric
* characters with a percent (%) sign followed by two hex digits, excepting
* characters in the URI reserved character set.
*
* Assumes that the URI is a complete URI, so does not encode reserved
* characters that have special meaning in the URI.
*
* Simulates the encodeURI function available in JavaScript
* https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURI
*
* Source: http://stackoverflow.com/q/4929584/264628
*
* @param string $uri The URI to encode
* @return string The original URL with special characters encoded
*/
public static function encodeURI($uri) {
$unescaped = array(
'%2D'=>'-','%5F'=>'_','%2E'=>'.','%21'=>'!', '%7E'=>'~',
'%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')'
);
$reserved = array(
'%3B'=>';','%2C'=>',','%2F'=>'/','%3F'=>'?','%3A'=>':',
'%40'=>'@','%26'=>'&','%3D'=>'=','%2B'=>'+','%24'=>'$'
);
$score = array(
'%23'=>'#'
);
return strtr(rawurlencode(rawurldecode($uri)), array_merge($reserved,$unescaped,$score));
}
/**
* Decoder for RLE8 compression in windows bitmaps
* http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp
*
* @param string $str Data to decode
* @param integer $width Image width
*
* @return string
*/
public static function rle8_decode($str, $width)
{
$lineWidth = $width + (3 - ($width - 1) % 4);
$out = '';
$cnt = strlen($str);
for ($i = 0; $i < $cnt; $i++) {
$o = ord($str[$i]);
switch ($o) {
case 0: # ESCAPE
$i++;
switch (ord($str[$i])) {
case 0: # NEW LINE
$padCnt = $lineWidth - strlen($out) % $lineWidth;
if ($padCnt < $lineWidth) $out .= str_repeat(chr(0), $padCnt); # pad line
break;
case 1: # END OF FILE
$padCnt = $lineWidth - strlen($out) % $lineWidth;
if ($padCnt < $lineWidth) $out .= str_repeat(chr(0), $padCnt); # pad line
break 3;
case 2: # DELTA
$i += 2;
break;
default: # ABSOLUTE MODE
$num = ord($str[$i]);
for ($j = 0; $j < $num; $j++)
$out .= $str[++$i];
if ($num % 2) $i++;
}
break;
default:
$out .= str_repeat($str[++$i], $o);
}
}
return $out;
}
/**
* Decoder for RLE4 compression in windows bitmaps
* see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp
*
* @param string $str Data to decode
* @param integer $width Image width
*
* @return string
*/
public static function rle4_decode($str, $width)
{
$w = floor($width / 2) + ($width % 2);
$lineWidth = $w + (3 - (($width - 1) / 2) % 4);
$pixels = array();
$cnt = strlen($str);
$c = 0;
for ($i = 0; $i < $cnt; $i++) {
$o = ord($str[$i]);
switch ($o) {
case 0: # ESCAPE
$i++;
switch (ord($str[$i])) {
case 0: # NEW LINE
while (count($pixels) % $lineWidth != 0) {
$pixels[] = 0;
}
break;
case 1: # END OF FILE
while (count($pixels) % $lineWidth != 0) {
$pixels[] = 0;
}
break 3;
case 2: # DELTA
$i += 2;
break;
default: # ABSOLUTE MODE
$num = ord($str[$i]);
for ($j = 0; $j < $num; $j++) {
if ($j % 2 == 0) {
$c = ord($str[++$i]);
$pixels[] = ($c & 240) >> 4;
} else {
$pixels[] = $c & 15;
}
}
if ($num % 2 == 0) {
$i++;
}
}
break;
default:
$c = ord($str[++$i]);
for ($j = 0; $j < $o; $j++) {
$pixels[] = ($j % 2 == 0 ? ($c & 240) >> 4 : $c & 15);
}
}
}
$out = '';
if (count($pixels) % 2) {
$pixels[] = 0;
}
$cnt = count($pixels) / 2;
for ($i = 0; $i < $cnt; $i++) {
$out .= chr(16 * $pixels[2 * $i] + $pixels[2 * $i + 1]);
}
return $out;
}
/**
* parse a full url or pathname and return an array(protocol, host, path,
* file + query + fragment)
*
* @param string $url
* @return array
*/
public static function explode_url($url)
{
$protocol = "";
$host = "";
$path = "";
$file = "";
$arr = parse_url($url);
if ( isset($arr["scheme"]) ) {
$arr["scheme"] = mb_strtolower($arr["scheme"]);
}
// Exclude windows drive letters...
if (isset($arr["scheme"]) && $arr["scheme"] !== "file" && strlen($arr["scheme"]) > 1) {
$protocol = $arr["scheme"] . "://";
if (isset($arr["user"])) {
$host .= $arr["user"];
if (isset($arr["pass"])) {
$host .= ":" . $arr["pass"];
}
$host .= "@";
}
if (isset($arr["host"])) {
$host .= $arr["host"];
}
if (isset($arr["port"])) {
$host .= ":" . $arr["port"];
}
if (isset($arr["path"]) && $arr["path"] !== "") {
// Do we have a trailing slash?
if ($arr["path"][mb_strlen($arr["path"]) - 1] === "/") {
$path = $arr["path"];
$file = "";
} else {
$path = rtrim(dirname($arr["path"]), '/\\') . "/";
$file = basename($arr["path"]);
}
}
if (isset($arr["query"])) {
$file .= "?" . $arr["query"];
}
if (isset($arr["fragment"])) {
$file .= "#" . $arr["fragment"];
}
} else {
$i = mb_stripos($url, "file://");
if ($i !== false) {
$url = mb_substr($url, $i + 7);
}
$protocol = ""; // "file://"; ? why doesn't this work... It's because of
// network filenames like //COMPU/SHARENAME
$host = ""; // localhost, really
$file = basename($url);
$path = dirname($url);
// Check that the path exists
if ($path !== false) {
$path .= '/';
} else {
// generate a url to access the file if no real path found.
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
$host = isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : php_uname("n");
if (substr($arr["path"], 0, 1) === '/') {
$path = dirname($arr["path"]);
} else {
$path = '/' . rtrim(dirname($_SERVER["SCRIPT_NAME"]), '/') . '/' . $arr["path"];
}
}
}
$ret = array($protocol, $host, $path, $file,
"protocol" => $protocol,
"host" => $host,
"path" => $path,
"file" => $file);
return $ret;
}
/**
* Print debug messages
*
* @param string $type The type of debug messages to print
* @param string $msg The message to show
*/
public static function dompdf_debug($type, $msg)
{
global $_DOMPDF_DEBUG_TYPES, $_dompdf_show_warnings, $_dompdf_debug;
if (isset($_DOMPDF_DEBUG_TYPES[$type]) && ($_dompdf_show_warnings || $_dompdf_debug)) {
$arr = debug_backtrace();
echo basename($arr[0]["file"]) . " (" . $arr[0]["line"] . "): " . $arr[1]["function"] . ": ";
Helpers::pre_r($msg);
}
}
/**
* Stores warnings in an array for display later
* This function allows warnings generated by the DomDocument parser
* and CSS loader ({@link Stylesheet}) to be captured and displayed
* later. Without this function, errors are displayed immediately and
* PDF streaming is impossible.
* @see http://www.php.net/manual/en/function.set-error_handler.php
*
* @param int $errno
* @param string $errstr
* @param string $errfile
* @param string $errline
*
* @throws Exception
*/
public static function record_warnings($errno, $errstr, $errfile, $errline)
{
// Not a warning or notice
if (!($errno & (E_WARNING | E_NOTICE | E_USER_NOTICE | E_USER_WARNING))) {
throw new Exception($errstr . " $errno");
}
global $_dompdf_warnings;
global $_dompdf_show_warnings;
if ($_dompdf_show_warnings) {
echo $errstr . "\n";
}
$_dompdf_warnings[] = $errstr;
}
/**
* @param $c
* @return bool|string
*/
public static function unichr($c)
{
if ($c <= 0x7F) {
return chr($c);
} else if ($c <= 0x7FF) {
return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
} else if ($c <= 0xFFFF) {
return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
. chr(0x80 | $c & 0x3F);
} else if ($c <= 0x10FFFF) {
return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
. chr(0x80 | $c >> 6 & 0x3F)
. chr(0x80 | $c & 0x3F);
}
return false;
}
/**
* Converts a CMYK color to RGB
*
* @param float|float[] $c
* @param float $m
* @param float $y
* @param float $k
*
* @return float[]
*/
public static function cmyk_to_rgb($c, $m = null, $y = null, $k = null)
{
if (is_array($c)) {
list($c, $m, $y, $k) = $c;
}
$c *= 255;
$m *= 255;
$y *= 255;
$k *= 255;
$r = (1 - round(2.55 * ($c + $k)));
$g = (1 - round(2.55 * ($m + $k)));
$b = (1 - round(2.55 * ($y + $k)));
if ($r < 0) $r = 0;
if ($g < 0) $g = 0;
if ($b < 0) $b = 0;
return array(
$r, $g, $b,
"r" => $r, "g" => $g, "b" => $b
);
}
/**
* getimagesize doesn't give a good size for 32bit BMP image v5
*
* @param string $filename
* @return array The same format as getimagesize($filename)
*/
public static function dompdf_getimagesize($filename, $context = null)
{
static $cache = array();
if (isset($cache[$filename])) {
return $cache[$filename];
}
list($width, $height, $type) = getimagesize($filename);
// Custom types
$types = array(
IMAGETYPE_JPEG => "jpeg",
IMAGETYPE_GIF => "gif",
IMAGETYPE_BMP => "bmp",
IMAGETYPE_PNG => "png",
);
$type = isset($types[$type]) ? $types[$type] : null;
if ($width == null || $height == null) {
list($data, $headers) = Helpers::getFileContent($filename, $context);
if (substr($data, 0, 2) === "BM") {
$meta = unpack('vtype/Vfilesize/Vreserved/Voffset/Vheadersize/Vwidth/Vheight', $data);
$width = (int)$meta['width'];
$height = (int)$meta['height'];
$type = "bmp";
}
else {
if (strpos($data, "<svg") !== false) {
$doc = new \Svg\Document();
$doc->loadFile($filename);
list($width, $height) = $doc->getDimensions();
$type = "svg";
}
}
}
return $cache[$filename] = array($width, $height, $type);
}
/**
* Credit goes to mgutt
* http://www.programmierer-forum.de/function-imagecreatefrombmp-welche-variante-laeuft-t143137.htm
* Modified by Fabien Menager to support RGB555 BMP format
*/
public static function imagecreatefrombmp($filename, $context = null)
{
if (!function_exists("imagecreatetruecolor")) {
trigger_error("The PHP GD extension is required, but is not installed.", E_ERROR);
return false;
}
// version 1.00
if (!($fh = fopen($filename, 'rb'))) {
trigger_error('imagecreatefrombmp: Can not open ' . $filename, E_USER_WARNING);
return false;
}
$bytes_read = 0;
// read file header
$meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14));
// check for bitmap
if ($meta['type'] != 19778) {
trigger_error('imagecreatefrombmp: ' . $filename . ' is not a bitmap!', E_USER_WARNING);
return false;
}
// read image header
$meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40));
$bytes_read += 40;
// read additional bitfield header
if ($meta['compression'] == 3) {
$meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12));
$bytes_read += 12;
}
// set bytes and padding
$meta['bytes'] = $meta['bits'] / 8;
$meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4)));
if ($meta['decal'] == 4) {
$meta['decal'] = 0;
}
// obtain imagesize
if ($meta['imagesize'] < 1) {
$meta['imagesize'] = $meta['filesize'] - $meta['offset'];
// in rare cases filesize is equal to offset so we need to read physical size
if ($meta['imagesize'] < 1) {
$meta['imagesize'] = @filesize($filename) - $meta['offset'];
if ($meta['imagesize'] < 1) {
trigger_error('imagecreatefrombmp: Can not obtain filesize of ' . $filename . '!', E_USER_WARNING);
return false;
}
}
}
// calculate colors
$meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors'];
// read color palette
$palette = array();
if ($meta['bits'] < 16) {
$palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4));
// in rare cases the color value is signed
if ($palette[1] < 0) {
foreach ($palette as $i => $color) {
$palette[$i] = $color + 16777216;
}
}
}
// ignore extra bitmap headers
if ($meta['headersize'] > $bytes_read) {
fread($fh, $meta['headersize'] - $bytes_read);
}
// create gd image
$im = imagecreatetruecolor($meta['width'], $meta['height']);
$data = fread($fh, $meta['imagesize']);
// uncompress data
switch ($meta['compression']) {
case 1:
$data = Helpers::rle8_decode($data, $meta['width']);
break;
case 2:
$data = Helpers::rle4_decode($data, $meta['width']);
break;
}
$p = 0;
$vide = chr(0);
$y = $meta['height'] - 1;
$error = 'imagecreatefrombmp: ' . $filename . ' has not enough data!';
// loop through the image data beginning with the lower left corner
while ($y >= 0) {
$x = 0;
while ($x < $meta['width']) {
switch ($meta['bits']) {
case 32:
case 24:
if (!($part = substr($data, $p, 3 /*$meta['bytes']*/))) {
trigger_error($error, E_USER_WARNING);
return $im;
}
$color = unpack('V', $part . $vide);
break;
case 16:
if (!($part = substr($data, $p, 2 /*$meta['bytes']*/))) {
trigger_error($error, E_USER_WARNING);
return $im;
}
$color = unpack('v', $part);
if (empty($meta['rMask']) || $meta['rMask'] != 0xf800) {
$color[1] = (($color[1] & 0x7c00) >> 7) * 65536 + (($color[1] & 0x03e0) >> 2) * 256 + (($color[1] & 0x001f) << 3); // 555
} else {
$color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3); // 565
}
break;
case 8:
$color = unpack('n', $vide . substr($data, $p, 1));
$color[1] = $palette[$color[1] + 1];
break;
case 4:
$color = unpack('n', $vide . substr($data, floor($p), 1));
$color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F;
$color[1] = $palette[$color[1] + 1];
break;
case 1:
$color = unpack('n', $vide . substr($data, floor($p), 1));
switch (($p * 8) % 8) {
case 0:
$color[1] = $color[1] >> 7;
break;
case 1:
$color[1] = ($color[1] & 0x40) >> 6;
break;
case 2:
$color[1] = ($color[1] & 0x20) >> 5;
break;
case 3:
$color[1] = ($color[1] & 0x10) >> 4;
break;
case 4:
$color[1] = ($color[1] & 0x8) >> 3;
break;
case 5:
$color[1] = ($color[1] & 0x4) >> 2;
break;
case 6:
$color[1] = ($color[1] & 0x2) >> 1;
break;
case 7:
$color[1] = ($color[1] & 0x1);
break;
}
$color[1] = $palette[$color[1] + 1];
break;
default:
trigger_error('imagecreatefrombmp: ' . $filename . ' has ' . $meta['bits'] . ' bits and this is not supported!', E_USER_WARNING);
return false;
}
imagesetpixel($im, $x, $y, $color[1]);
$x++;
$p += $meta['bytes'];
}
$y--;
$p += $meta['decal'];
}
fclose($fh);
return $im;
}
/**
* Gets the content of the file at the specified path using one of
* the following methods, in preferential order:
* - file_get_contents: if allow_url_fopen is true or the file is local
* - curl: if allow_url_fopen is false and curl is available
*
* @param string $uri
* @param resource $context (ignored if curl is used)
* @param int $offset
* @param int $maxlen (ignored if curl is used)
* @return bool|array
*/
public static function getFileContent($uri, $context = null, $offset = 0, $maxlen = null)
{
$result = false;
$headers = null;
list($proto, $host, $path, $file) = Helpers::explode_url($uri);
$is_local_path = ($proto == "" || $proto === "file://");
set_error_handler(array("\\Dompdf\\Helpers", "record_warnings"));
if ($is_local_path || ini_get("allow_url_fopen")) {
if ($is_local_path === false) {
$uri = Helpers::encodeURI($uri);
}
if (isset($maxlen)) {
$result = file_get_contents($uri, null, $context, $offset, $maxlen);
} else {
$result = file_get_contents($uri, null, $context, $offset);
}
if (isset($http_response_header))
{
$headers = $http_response_header;
}
} elseif (function_exists("curl_exec")) {
$curl = curl_init($uri);
//TODO: use $context to define additional curl options
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, true);
if ($offset > 0) {
curl_setopt($curl, CURLOPT_RESUME_FROM, $offset);
}
$data = curl_exec($curl);
$raw_headers = substr($data, 0, curl_getinfo($curl, CURLINFO_HEADER_SIZE));
$headers = preg_split("/[\n\r]+/", trim($raw_headers));
$result = substr($data, curl_getinfo($curl, CURLINFO_HEADER_SIZE));
curl_close($curl);
}
restore_error_handler();
return array($result, $headers);
}
}
@@ -0,0 +1,184 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Helmut Tischer <htischer@weihenstephan.org>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Image;
use Dompdf\Dompdf;
use Dompdf\Helpers;
use Dompdf\Exception\ImageException;
/**
* Static class that resolves image urls and downloads and caches
* remote images if required.
*
* @package dompdf
*/
class Cache
{
/**
* Array of downloaded images. Cached so that identical images are
* not needlessly downloaded.
*
* @var array
*/
protected static $_cache = array();
/**
* The url to the "broken image" used when images can't be loaded
*
* @var string
*/
public static $broken_image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAA3NCSVQICAjb4U/gAAAAHlBMVEWZmZn////g4OCkpKS1tbXv7++9vb2tra3m5ub5+fkFnN6oAAAACXBIWXMAAAsSAAALEgHS3X78AAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M0BrLToAAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNC8xMi8xMRPnI58AAAGZSURBVEiJhZbPasMwDMbTw2DHKhDQcbDQPsEge4BAjg0Mxh5gkBcY7Niwkpx32PvOjv9JspX60It/+fxJsqxW1b11gN11rA7N3v6vAd5nfR9fDYCTDiyzAeA6qgKd9QDNoAtsAKyKCxzAAfhdBuyHGwC3oovNvQOaxxJwnSNg3ZQFAlBy4ax7AG6ZBLrgA5Cn038SAPgREiaJHJASwXYEhEQQIACyikTTCWCBJJoANBfpPAKQdBLHFMBYkctcBKIE9lAGggt6gRjgA2GV44CL7m1WgS08fAAdsPHxyyMAIyHujgRwEldHArCKy5cBz90+gNOyf8TTyKOUQN2LPEmgnWWPcKD+sr+rnuqTK1avAcHfRSv3afTgVAbqmCPiggLtGM8aSkBNOidVjADrmIDYebT1PoGsWJEE8Oc0b96aZoe4iMBZPiADB6RAzEUA2vwRmyiAL3Lfv6MBSEmUEg7ALt/3LhxwLgj4QNw4UCbKEsaBNpPsyRbgVRASFig78BIGyJNIJQyQTwIi0RvgT98H+Mi6W67j3X8H/427u5bfpQGVAAAAAElFTkSuQmCC";
/**
* Current dompdf instance
*
* @var Dompdf
*/
protected static $_dompdf;
/**
* Resolve and fetch an image for use.
*
* @param string $url The url of the image
* @param string $protocol Default protocol if none specified in $url
* @param string $host Default host if none specified in $url
* @param string $base_path Default path if none specified in $url
* @param Dompdf $dompdf The Dompdf instance
*
* @throws ImageException
* @return array An array with two elements: The local path to the image and the image extension
*/
static function resolve_url($url, $protocol, $host, $base_path, Dompdf $dompdf)
{
self::$_dompdf = $dompdf;
$protocol = mb_strtolower($protocol);
$parsed_url = Helpers::explode_url($url);
$message = null;
$remote = ($protocol && $protocol !== "file://") || ($parsed_url['protocol'] != "");
$data_uri = strpos($parsed_url['protocol'], "data:") === 0;
$full_url = null;
$enable_remote = $dompdf->getOptions()->getIsRemoteEnabled();
try {
// Remote not allowed and is not DataURI
if (!$enable_remote && $remote && !$data_uri) {
throw new ImageException("Remote file access is disabled.", E_WARNING);
} // Remote allowed or DataURI
else {
if ($enable_remote && $remote || $data_uri) {
// Download remote files to a temporary directory
$full_url = Helpers::build_url($protocol, $host, $base_path, $url);
// From cache
if (isset(self::$_cache[$full_url])) {
$resolved_url = self::$_cache[$full_url];
} // From remote
else {
$tmp_dir = $dompdf->getOptions()->getTempDir();
$resolved_url = tempnam($tmp_dir, "ca_dompdf_img_");
$image = "";
if ($data_uri) {
if ($parsed_data_uri = Helpers::parse_data_uri($url)) {
$image = $parsed_data_uri['data'];
}
} else {
list($image, $http_response_header) = Helpers::getFileContent($full_url, $dompdf->getHttpContext());
}
// Image not found or invalid
if (strlen($image) == 0) {
$msg = ($data_uri ? "Data-URI could not be parsed" : "Image not found");
throw new ImageException($msg, E_WARNING);
} // Image found, put in cache and process
else {
//e.g. fetch.php?media=url.jpg&cache=1
//- Image file name might be one of the dynamic parts of the url, don't strip off!
//- a remote url does not need to have a file extension at all
//- local cached file does not have a matching file extension
//Therefore get image type from the content
file_put_contents($resolved_url, $image);
}
}
} // Not remote, local image
else {
$resolved_url = Helpers::build_url($protocol, $host, $base_path, $url);
}
}
// Check if the local file is readable
if (!is_readable($resolved_url) || !filesize($resolved_url)) {
throw new ImageException("Image not readable or empty", E_WARNING);
} // Check is the file is an image
else {
list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $dompdf->getHttpContext());
// Known image type
if ($width && $height && in_array($type, array("gif", "png", "jpeg", "bmp", "svg"))) {
//Don't put replacement image into cache - otherwise it will be deleted on cache cleanup.
//Only execute on successful caching of remote image.
if ($enable_remote && $remote || $data_uri) {
self::$_cache[$full_url] = $resolved_url;
}
} // Unknown image type
else {
throw new ImageException("Image type unknown", E_WARNING);
}
}
} catch (ImageException $e) {
$resolved_url = self::$broken_image;
$type = "png";
$message = "Image not found or type unknown";
Helpers::record_warnings($e->getCode(), $e->getMessage() . " \n $url", $e->getFile(), $e->getLine());
}
return array($resolved_url, $type, $message);
}
/**
* Unlink all cached images (i.e. temporary images either downloaded
* or converted)
*/
static function clear()
{
if (empty(self::$_cache) || self::$_dompdf->getOptions()->getDebugKeepTemp()) {
return;
}
foreach (self::$_cache as $file) {
if (self::$_dompdf->getOptions()->getDebugPng()) {
print "[clear unlink $file]";
}
unlink($file);
}
self::$_cache = array();
}
static function detect_type($file, $context = null)
{
list(, , $type) = Helpers::dompdf_getimagesize($file, $context);
return $type;
}
static function is_broken($url)
{
return $url === self::$broken_image;
}
}
if (file_exists(realpath(__DIR__ . "/../../lib/res/broken_image.png"))) {
Cache::$broken_image = realpath(__DIR__ . "/../../lib/res/broken_image.png");
}
@@ -0,0 +1,54 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
use Dompdf\Frame;
/**
* Embeds Javascript into the PDF document
*
* @package dompdf
*/
class JavascriptEmbedder
{
/**
* @var Dompdf
*/
protected $_dompdf;
/**
* JavascriptEmbedder constructor.
*
* @param Dompdf $dompdf
*/
public function __construct(Dompdf $dompdf)
{
$this->_dompdf = $dompdf;
}
/**
* @param $script
*/
public function insert($script)
{
$this->_dompdf->getCanvas()->javascript($script);
}
/**
* @param \Dompdf\Frame $frame
*/
public function render(Frame $frame)
{
if (!$this->_dompdf->getOptions()->getIsJavascriptEnabled()) {
return;
}
$this->insert($frame->get_node()->nodeValue);
}
}
@@ -0,0 +1,304 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block;
use Dompdf\FrameDecorator\Page;
/**
* The line box class
*
* This class represents a line box
* http://www.w3.org/TR/CSS2/visuren.html#line-box
*
* @package dompdf
*/
class LineBox
{
/**
* @var Block
*/
protected $_block_frame;
/**
* @var Frame[]
*/
protected $_frames = array();
/**
* @var integer
*/
public $wc = 0;
/**
* @var float
*/
public $y = null;
/**
* @var float
*/
public $w = 0.0;
/**
* @var float
*/
public $h = 0.0;
/**
* @var float
*/
public $left = 0.0;
/**
* @var float
*/
public $right = 0.0;
/**
* @var Frame
*/
public $tallest_frame = null;
/**
* @var bool[]
*/
public $floating_blocks = array();
/**
* @var bool
*/
public $br = false;
/**
* Class constructor
*
* @param Block $frame the Block containing this line
* @param int $y
*/
public function __construct(Block $frame, $y = 0)
{
$this->_block_frame = $frame;
$this->_frames = array();
$this->y = $y;
$this->get_float_offsets();
}
/**
* Returns the floating elements inside the first floating parent
*
* @param Page $root
*
* @return Frame[]
*/
public function get_floats_inside(Page $root)
{
$floating_frames = $root->get_floating_frames();
if (count($floating_frames) == 0) {
return $floating_frames;
}
// Find nearest floating element
$p = $this->_block_frame;
while ($p->get_style()->float === "none") {
$parent = $p->get_parent();
if (!$parent) {
break;
}
$p = $parent;
}
if ($p == $root) {
return $floating_frames;
}
$parent = $p;
$childs = array();
foreach ($floating_frames as $_floating) {
$p = $_floating->get_parent();
while (($p = $p->get_parent()) && $p !== $parent) ;
if ($p) {
$childs[] = $p;
}
}
return $childs;
}
/**
*
*/
public function get_float_offsets()
{
static $anti_infinite_loop = 10000; // FIXME smelly hack
$reflower = $this->_block_frame->get_reflower();
if (!$reflower) {
return;
}
$cb_w = null;
$block = $this->_block_frame;
$root = $block->get_root();
if (!$root) {
return;
}
$style = $this->_block_frame->get_style();
$floating_frames = $this->get_floats_inside($root);
$inside_left_floating_width = 0;
$inside_right_floating_width = 0;
$outside_left_floating_width = 0;
$outside_right_floating_width = 0;
foreach ($floating_frames as $child_key => $floating_frame) {
$floating_frame_parent = $floating_frame->get_parent();
$id = $floating_frame->get_id();
if (isset($this->floating_blocks[$id])) {
continue;
}
$float = $floating_frame->get_style()->float;
$floating_width = $floating_frame->get_margin_width();
if (!$cb_w) {
$cb_w = $floating_frame->get_containing_block("w");
}
$line_w = $this->get_width();
if (!$floating_frame->_float_next_line && ($cb_w <= $line_w + $floating_width) && ($cb_w > $line_w)) {
$floating_frame->_float_next_line = true;
continue;
}
// If the child is still shifted by the floating element
if ($anti_infinite_loop-- > 0 &&
$floating_frame->get_position("y") + $floating_frame->get_margin_height() >= $this->y &&
$block->get_position("x") + $block->get_margin_width() >= $floating_frame->get_position("x")
) {
if ($float === "left") {
if ($floating_frame_parent === $this->_block_frame) {
$inside_left_floating_width += $floating_width;
} else {
$outside_left_floating_width += $floating_width;
}
} elseif ($float === "right") {
if ($floating_frame_parent === $this->_block_frame) {
$inside_right_floating_width += $floating_width;
} else {
$outside_right_floating_width += $floating_width;
}
}
$this->floating_blocks[$id] = true;
} // else, the floating element won't shift anymore
else {
$root->remove_floating_frame($child_key);
}
}
$this->left += $inside_left_floating_width;
if ($outside_left_floating_width > (float)$style->length_in_pt($style->margin_left) + (float)$style->length_in_pt($style->padding_left)) {
$this->left += $outside_left_floating_width - (float)$style->length_in_pt($style->margin_left) - (float)$style->length_in_pt($style->padding_left);
}
$this->right += $inside_right_floating_width;
if ($outside_right_floating_width > (float)$style->length_in_pt($style->margin_left) + (float)$style->length_in_pt($style->padding_right)) {
$this->right += $outside_right_floating_width - (float)$style->length_in_pt($style->margin_right) - (float)$style->length_in_pt($style->padding_right);
}
}
/**
* @return float
*/
public function get_width()
{
return $this->left + $this->w + $this->right;
}
/**
* @return Block
*/
public function get_block_frame()
{
return $this->_block_frame;
}
/**
* @return Frame[]
*/
function &get_frames()
{
return $this->_frames;
}
/**
* @param Frame $frame
*/
public function add_frame(Frame $frame)
{
$this->_frames[] = $frame;
}
/**
* Recalculate LineBox width based on the contained frames total width.
*
* @return float
*/
public function recalculate_width()
{
$width = 0;
foreach ($this->get_frames() as $frame) {
$width += $frame->calculate_auto_width();
}
return $this->w = $width;
}
/**
* @return string
*/
public function __toString()
{
$props = array("wc", "y", "w", "h", "left", "right", "br");
$s = "";
foreach ($props as $prop) {
$s .= "$prop: " . $this->$prop . "\n";
}
$s .= count($this->_frames) . " frames\n";
return $s;
}
/*function __get($prop) {
if (!isset($this->{"_$prop"})) return;
return $this->{"_$prop"};
}*/
}
/*
class LineBoxList implements Iterator {
private $_p = 0;
private $_lines = array();
}
*/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
use Dompdf\Frame;
/**
* Executes inline PHP code during the rendering process
*
* @package dompdf
*/
class PhpEvaluator
{
/**
* @var Canvas
*/
protected $_canvas;
/**
* PhpEvaluator constructor.
* @param Canvas $canvas
*/
public function __construct(Canvas $canvas)
{
$this->_canvas = $canvas;
}
/**
* @param $code
* @param array $vars
*/
public function evaluate($code, $vars = array())
{
if (!$this->_canvas->get_dompdf()->getOptions()->getIsPhpEnabled()) {
return;
}
// Set up some variables for the inline code
$pdf = $this->_canvas;
$fontMetrics = $pdf->get_dompdf()->getFontMetrics();
$PAGE_NUM = $pdf->get_page_number();
$PAGE_COUNT = $pdf->get_page_count();
// Override those variables if passed in
foreach ($vars as $k => $v) {
$$k = $v;
}
eval($code);
}
/**
* @param \Dompdf\Frame $frame
*/
public function render(Frame $frame)
{
$this->evaluate($frame->get_node()->nodeValue);
}
}
@@ -0,0 +1,118 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Positions absolutely positioned frames
*/
class Absolute extends AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
*/
function position(AbstractFrameDecorator $frame)
{
$style = $frame->get_style();
$p = $frame->find_positionned_parent();
list($x, $y, $w, $h) = $frame->get_containing_block();
$top = $style->length_in_pt($style->top, $h);
$right = $style->length_in_pt($style->right, $w);
$bottom = $style->length_in_pt($style->bottom, $h);
$left = $style->length_in_pt($style->left, $w);
if ($p && !($left === "auto" && $right === "auto")) {
// Get the parent's padding box (see http://www.w3.org/TR/CSS21/visuren.html#propdef-top)
list($x, $y, $w, $h) = $p->get_padding_box();
}
list($width, $height) = array($frame->get_margin_width(), $frame->get_margin_height());
$orig_style = $frame->get_original_style();
$orig_width = $orig_style->width;
$orig_height = $orig_style->height;
/****************************
*
* Width auto:
* ____________| left=auto | left=fixed |
* right=auto | A | B |
* right=fixed | C | D |
*
* Width fixed:
* ____________| left=auto | left=fixed |
* right=auto | E | F |
* right=fixed | G | H |
*****************************/
if ($left === "auto") {
if ($right === "auto") {
// A or E - Keep the frame at the same position
$x = $x + $frame->find_block_parent()->get_current_line_box()->w;
} else {
if ($orig_width === "auto") {
// C
$x += $w - $width - $right;
} else {
// G
$x += $w - $width - $right;
}
}
} else {
if ($right === "auto") {
// B or F
$x += (float)$left;
} else {
if ($orig_width === "auto") {
// D - TODO change width
$x += (float)$left;
} else {
// H - Everything is fixed: left + width win
$x += (float)$left;
}
}
}
// The same vertically
if ($top === "auto") {
if ($bottom === "auto") {
// A or E - Keep the frame at the same position
$y = $frame->find_block_parent()->get_current_line_box()->y;
} else {
if ($orig_height === "auto") {
// C
$y += (float)$h - $height - (float)$bottom;
} else {
// G
$y += (float)$h - $height - (float)$bottom;
}
}
} else {
if ($bottom === "auto") {
// B or F
$y += (float)$top;
} else {
if ($orig_height === "auto") {
// D - TODO change height
$y += (float)$top;
} else {
// H - Everything is fixed: top + height win
$y += (float)$top;
}
}
}
$frame->set_position($x, $y);
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Base AbstractPositioner class
*
* Defines postioner interface
*
* @access private
* @package dompdf
*/
abstract class AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
* @return mixed
*/
abstract function position(AbstractFrameDecorator $frame);
/**
* @param AbstractFrameDecorator $frame
* @param $offset_x
* @param $offset_y
* @param bool $ignore_self
*/
function move(AbstractFrameDecorator $frame, $offset_x, $offset_y, $ignore_self = false)
{
list($x, $y) = $frame->get_position();
if (!$ignore_self) {
$frame->set_position($x + $offset_x, $y + $offset_y);
}
foreach ($frame->get_children() as $child) {
$child->move($offset_x, $offset_y);
}
}
}
@@ -0,0 +1,54 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Positions block frames
*
* @access private
* @package dompdf
*/
class Block extends AbstractPositioner {
function position(AbstractFrameDecorator $frame)
{
$style = $frame->get_style();
$cb = $frame->get_containing_block();
$p = $frame->find_block_parent();
if ($p) {
$float = $style->float;
if (!$float || $float === "none") {
$p->add_line(true);
}
$y = $p->get_current_line_box()->y;
} else {
$y = $cb["y"];
}
$x = $cb["x"];
// Relative positionning
if ($style->position === "relative") {
$top = (float)$style->length_in_pt($style->top, $cb["h"]);
//$right = (float)$style->length_in_pt($style->right, $cb["w"]);
//$bottom = (float)$style->length_in_pt($style->bottom, $cb["h"]);
$left = (float)$style->length_in_pt($style->left, $cb["w"]);
$x += $left;
$y += $top;
}
$frame->set_position($x, $y);
}
}
@@ -0,0 +1,89 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Positions fixely positioned frames
*/
class Fixed extends AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
*/
function position(AbstractFrameDecorator $frame)
{
$style = $frame->get_original_style();
$root = $frame->get_root();
$initialcb = $root->get_containing_block();
$initialcb_style = $root->get_style();
$p = $frame->find_block_parent();
if ($p) {
$p->add_line();
}
// Compute the margins of the @page style
$margin_top = (float)$initialcb_style->length_in_pt($initialcb_style->margin_top, $initialcb["h"]);
$margin_right = (float)$initialcb_style->length_in_pt($initialcb_style->margin_right, $initialcb["w"]);
$margin_bottom = (float)$initialcb_style->length_in_pt($initialcb_style->margin_bottom, $initialcb["h"]);
$margin_left = (float)$initialcb_style->length_in_pt($initialcb_style->margin_left, $initialcb["w"]);
// The needed computed style of the element
$height = (float)$style->length_in_pt($style->height, $initialcb["h"]);
$width = (float)$style->length_in_pt($style->width, $initialcb["w"]);
$top = $style->length_in_pt($style->top, $initialcb["h"]);
$right = $style->length_in_pt($style->right, $initialcb["w"]);
$bottom = $style->length_in_pt($style->bottom, $initialcb["h"]);
$left = $style->length_in_pt($style->left, $initialcb["w"]);
$y = $margin_top;
if (isset($top)) {
$y = (float)$top + $margin_top;
if ($top === "auto") {
$y = $margin_top;
if (isset($bottom) && $bottom !== "auto") {
$y = $initialcb["h"] - $bottom - $margin_bottom;
if ($frame->is_auto_height()) {
$y -= $height;
} else {
$y -= $frame->get_margin_height();
}
}
}
}
$x = $margin_left;
if (isset($left)) {
$x = (float)$left + $margin_left;
if ($left === "auto") {
$x = $margin_left;
if (isset($right) && $right !== "auto") {
$x = $initialcb["w"] - $right - $margin_right;
if ($frame->is_auto_width()) {
$x -= $width;
} else {
$x -= $frame->get_margin_width();
}
}
}
}
$frame->set_position($x, $y);
$children = $frame->get_children();
foreach ($children as $child) {
$child->set_position($x, $y);
}
}
}
@@ -0,0 +1,78 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameDecorator\Inline as InlineFrameDecorator;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
use Dompdf\Exception;
/**
* Positions inline frames
*
* @package dompdf
*/
class Inline extends AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
* @throws Exception
*/
function position(AbstractFrameDecorator $frame)
{
/**
* Find our nearest block level parent and access its lines property.
* @var BlockFrameDecorator
*/
$p = $frame->find_block_parent();
// Debugging code:
// Helpers::pre_r("\nPositioning:");
// Helpers::pre_r("Me: " . $frame->get_node()->nodeName . " (" . spl_object_hash($frame->get_node()) . ")");
// Helpers::pre_r("Parent: " . $p->get_node()->nodeName . " (" . spl_object_hash($p->get_node()) . ")");
// End debugging
if (!$p) {
throw new Exception("No block-level parent found. Not good.");
}
$f = $frame;
$cb = $f->get_containing_block();
$line = $p->get_current_line_box();
// Skip the page break if in a fixed position element
$is_fixed = false;
while ($f = $f->get_parent()) {
if ($f->get_style()->position === "fixed") {
$is_fixed = true;
break;
}
}
$f = $frame;
if (!$is_fixed && $f->get_parent() &&
$f->get_parent() instanceof InlineFrameDecorator &&
$f->is_text_node()
) {
$min_max = $f->get_reflower()->get_min_max_width();
// If the frame doesn't fit in the current line, a line break occurs
if ($min_max["min"] > ($cb["w"] - $line->left - $line->w - $line->right)) {
$p->add_line();
}
}
$f->set_position($cb["x"] + $line->w, $line->y);
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Helmut Tischer <htischer@weihenstephan.org>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Positions list bullets
*
* @package dompdf
*/
class ListBullet extends AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
*/
function position(AbstractFrameDecorator $frame)
{
// Bullets & friends are positioned an absolute distance to the left of
// the content edge of their parent element
$cb = $frame->get_containing_block();
// Note: this differs from most frames in that we must position
// ourselves after determining our width
$x = $cb["x"] - $frame->get_width();
$p = $frame->find_block_parent();
$y = $p->get_current_line_box()->y;
// This is a bit of a hack...
$n = $frame->get_next_sibling();
if ($n) {
$style = $n->get_style();
$line_height = $style->length_in_pt($style->line_height, $style->get_font_size());
$offset = (float)$style->length_in_pt($line_height, $n->get_containing_block("h")) - $frame->get_height();
$y += $offset / 2;
}
// Now the position is the left top of the block which should be marked with the bullet.
// We tried to find out the y of the start of the first text character within the block.
// But the top margin/padding does not fit, neither from this nor from the next sibling
// The "bit of a hack" above does not work also.
// Instead let's position the bullet vertically centered to the block which should be marked.
// But for get_next_sibling() the get_containing_block is all zero, and for find_block_parent()
// the get_containing_block is paper width and the entire list as height.
// if ($p) {
// //$cb = $n->get_containing_block();
// $cb = $p->get_containing_block();
// $y += $cb["h"]/2;
// print 'cb:'.$cb["x"].':'.$cb["y"].':'.$cb["w"].':'.$cb["h"].':';
// }
// Todo:
// For now give up on the above. Use Guesswork with font y-pos in the middle of the line spacing
/*$style = $p->get_style();
$font_size = $style->get_font_size();
$line_height = (float)$style->length_in_pt($style->line_height, $font_size);
$y += ($line_height - $font_size) / 2; */
//Position is x-end y-top of character position of the bullet.
$frame->set_position($x, $y);
}
}
@@ -0,0 +1,28 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Dummy positioner
*
* @package dompdf
*/
class NullPositioner extends AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
*/
function position(AbstractFrameDecorator $frame)
{
return;
}
}
@@ -0,0 +1,31 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameDecorator\Table;
/**
* Positions table cells
*
* @package dompdf
*/
class TableCell extends AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
*/
function position(AbstractFrameDecorator $frame)
{
$table = Table::find_parent_table($frame);
$cellmap = $table->get_cellmap();
$frame->set_position($cellmap->get_frame_position($frame));
}
}
@@ -0,0 +1,36 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Positions table rows
*
* @package dompdf
*/
class TableRow extends AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
*/
function position(AbstractFrameDecorator $frame)
{
$cb = $frame->get_containing_block();
$p = $frame->get_prev_sibling();
if ($p) {
$y = $p->get_position("y") + $p->get_margin_height();
} else {
$y = $cb["y"];
}
$frame->set_position($cb["x"], $y);
}
}
@@ -0,0 +1,920 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Helmut Tischer <htischer@weihenstephan.org>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Adapter\CPDF;
use Dompdf\Css\Color;
use Dompdf\Css\Style;
use Dompdf\Dompdf;
use Dompdf\Helpers;
use Dompdf\Frame;
use Dompdf\Image\Cache;
/**
* Base renderer class
*
* @package dompdf
*/
abstract class AbstractRenderer
{
/**
* Rendering backend
*
* @var \Dompdf\Canvas
*/
protected $_canvas;
/**
* Current dompdf instance
*
* @var Dompdf
*/
protected $_dompdf;
/**
* Class constructor
*
* @param Dompdf $dompdf The current dompdf instance
*/
function __construct(Dompdf $dompdf)
{
$this->_dompdf = $dompdf;
$this->_canvas = $dompdf->getCanvas();
}
/**
* Render a frame.
*
* Specialized in child classes
*
* @param Frame $frame The frame to render
*/
abstract function render(Frame $frame);
/**
* Render a background image over a rectangular area
*
* @param string $url The background image to load
* @param float $x The left edge of the rectangular area
* @param float $y The top edge of the rectangular area
* @param float $width The width of the rectangular area
* @param float $height The height of the rectangular area
* @param Style $style The associated Style object
*
* @throws \Exception
*/
protected function _background_image($url, $x, $y, $width, $height, $style)
{
if (!function_exists("imagecreatetruecolor")) {
throw new \Exception("The PHP GD extension is required, but is not installed.");
}
$sheet = $style->get_stylesheet();
// Skip degenerate cases
if ($width == 0 || $height == 0) {
return;
}
$box_width = $width;
$box_height = $height;
//debugpng
if ($this->_dompdf->getOptions()->getDebugPng()) {
print '[_background_image ' . $url . ']';
}
list($img, $type, /*$msg*/) = Cache::resolve_url(
$url,
$sheet->get_protocol(),
$sheet->get_host(),
$sheet->get_base_path(),
$this->_dompdf
);
// Bail if the image is no good
if (Cache::is_broken($img)) {
return;
}
//Try to optimize away reading and composing of same background multiple times
//Postponing read with imagecreatefrom ...()
//final composition parameters and name not known yet
//Therefore read dimension directly from file, instead of creating gd object first.
//$img_w = imagesx($src); $img_h = imagesy($src);
list($img_w, $img_h) = Helpers::dompdf_getimagesize($img, $this->_dompdf->getHttpContext());
if (!isset($img_w) || $img_w == 0 || !isset($img_h) || $img_h == 0) {
return;
}
$repeat = $style->background_repeat;
$dpi = $this->_dompdf->getOptions()->getDpi();
//Increase background resolution and dependent box size according to image resolution to be placed in
//Then image can be copied in without resize
$bg_width = round((float)($width * $dpi) / 72);
$bg_height = round((float)($height * $dpi) / 72);
//Need %bg_x, $bg_y as background pos, where img starts, converted to pixel
list($bg_x, $bg_y) = $style->background_position;
if (Helpers::is_percent($bg_x)) {
// The point $bg_x % from the left edge of the image is placed
// $bg_x % from the left edge of the background rectangle
$p = ((float)$bg_x) / 100.0;
$x1 = $p * $img_w;
$x2 = $p * $bg_width;
$bg_x = $x2 - $x1;
} else {
$bg_x = (float)($style->length_in_pt($bg_x) * $dpi) / 72;
}
$bg_x = round($bg_x + (float)$style->length_in_pt($style->border_left_width) * $dpi / 72);
if (Helpers::is_percent($bg_y)) {
// The point $bg_y % from the left edge of the image is placed
// $bg_y % from the left edge of the background rectangle
$p = ((float)$bg_y) / 100.0;
$y1 = $p * $img_h;
$y2 = $p * $bg_height;
$bg_y = $y2 - $y1;
} else {
$bg_y = (float)($style->length_in_pt($bg_y) * $dpi) / 72;
}
$bg_y = round($bg_y + (float)$style->length_in_pt($style->border_top_width) * $dpi / 72);
//clip background to the image area on partial repeat. Nothing to do if img off area
//On repeat, normalize start position to the tile at immediate left/top or 0/0 of area
//On no repeat with positive offset: move size/start to have offset==0
//Handle x/y Dimensions separately
if ($repeat !== "repeat" && $repeat !== "repeat-x") {
//No repeat x
if ($bg_x < 0) {
$bg_width = $img_w + $bg_x;
} else {
$x += ($bg_x * 72) / $dpi;
$bg_width = $bg_width - $bg_x;
if ($bg_width > $img_w) {
$bg_width = $img_w;
}
$bg_x = 0;
}
if ($bg_width <= 0) {
return;
}
$width = (float)($bg_width * 72) / $dpi;
} else {
//repeat x
if ($bg_x < 0) {
$bg_x = -((-$bg_x) % $img_w);
} else {
$bg_x = $bg_x % $img_w;
if ($bg_x > 0) {
$bg_x -= $img_w;
}
}
}
if ($repeat !== "repeat" && $repeat !== "repeat-y") {
//no repeat y
if ($bg_y < 0) {
$bg_height = $img_h + $bg_y;
} else {
$y += ($bg_y * 72) / $dpi;
$bg_height = $bg_height - $bg_y;
if ($bg_height > $img_h) {
$bg_height = $img_h;
}
$bg_y = 0;
}
if ($bg_height <= 0) {
return;
}
$height = (float)($bg_height * 72) / $dpi;
} else {
//repeat y
if ($bg_y < 0) {
$bg_y = -((-$bg_y) % $img_h);
} else {
$bg_y = $bg_y % $img_h;
if ($bg_y > 0) {
$bg_y -= $img_h;
}
}
}
//Optimization, if repeat has no effect
if ($repeat === "repeat" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height) {
$repeat = "repeat-x";
}
if ($repeat === "repeat" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width) {
$repeat = "repeat-y";
}
if (($repeat === "repeat-x" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width) ||
($repeat === "repeat-y" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height)
) {
$repeat = "no-repeat";
}
//Use filename as indicator only
//different names for different variants to have different copies in the pdf
//This is not dependent of background color of box! .'_'.(is_array($bg_color) ? $bg_color["hex"] : $bg_color)
//Note: Here, bg_* are the start values, not end values after going through the tile loops!
$filedummy = $img;
$is_png = false;
$filedummy .= '_' . $bg_width . '_' . $bg_height . '_' . $bg_x . '_' . $bg_y . '_' . $repeat;
//Optimization to avoid multiple times rendering the same image.
//If check functions are existing and identical image already cached,
//then skip creation of duplicate, because it is not needed by addImagePng
if ($this->_canvas instanceof CPDF &&
$this->_canvas->get_cpdf()->image_iscached($filedummy)
) {
$bg = null;
} else {
// Create a new image to fit over the background rectangle
$bg = imagecreatetruecolor($bg_width, $bg_height);
switch (strtolower($type)) {
case "png":
$is_png = true;
imagesavealpha($bg, true);
imagealphablending($bg, false);
$src = imagecreatefrompng($img);
break;
case "jpeg":
$src = imagecreatefromjpeg($img);
break;
case "gif":
$src = imagecreatefromgif($img);
break;
case "bmp":
$src = Helpers::imagecreatefrombmp($img);
break;
default:
return; // Unsupported image type
}
if ($src == null) {
return;
}
//Background color if box is not relevant here
//Non transparent image: box clipped to real size. Background non relevant.
//Transparent image: The image controls the transparency and lets shine through whatever background.
//However on transparent image preset the composed image with the transparency color,
//to keep the transparency when copying over the non transparent parts of the tiles.
$ti = imagecolortransparent($src);
if ($ti >= 0) {
$tc = imagecolorsforindex($src, $ti);
$ti = imagecolorallocate($bg, $tc['red'], $tc['green'], $tc['blue']);
imagefill($bg, 0, 0, $ti);
imagecolortransparent($bg, $ti);
}
//This has only an effect for the non repeatable dimension.
//compute start of src and dest coordinates of the single copy
if ($bg_x < 0) {
$dst_x = 0;
$src_x = -$bg_x;
} else {
$src_x = 0;
$dst_x = $bg_x;
}
if ($bg_y < 0) {
$dst_y = 0;
$src_y = -$bg_y;
} else {
$src_y = 0;
$dst_y = $bg_y;
}
//For historical reasons exchange meanings of variables:
//start_* will be the start values, while bg_* will be the temporary start values in the loops
$start_x = $bg_x;
$start_y = $bg_y;
// Copy regions from the source image to the background
if ($repeat === "no-repeat") {
// Simply place the image on the background
imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $img_h);
} else if ($repeat === "repeat-x") {
for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) {
if ($bg_x < 0) {
$dst_x = 0;
$src_x = -$bg_x;
$w = $img_w + $bg_x;
} else {
$dst_x = $bg_x;
$src_x = 0;
$w = $img_w;
}
imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $img_h);
}
} else if ($repeat === "repeat-y") {
for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) {
if ($bg_y < 0) {
$dst_y = 0;
$src_y = -$bg_y;
$h = $img_h + $bg_y;
} else {
$dst_y = $bg_y;
$src_y = 0;
$h = $img_h;
}
imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $h);
}
} else if ($repeat === "repeat") {
for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) {
for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) {
if ($bg_x < 0) {
$dst_x = 0;
$src_x = -$bg_x;
$w = $img_w + $bg_x;
} else {
$dst_x = $bg_x;
$src_x = 0;
$w = $img_w;
}
if ($bg_y < 0) {
$dst_y = 0;
$src_y = -$bg_y;
$h = $img_h + $bg_y;
} else {
$dst_y = $bg_y;
$src_y = 0;
$h = $img_h;
}
imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $h);
}
}
} else {
print 'Unknown repeat!';
}
imagedestroy($src);
} /* End optimize away creation of duplicates */
$this->_canvas->clipping_rectangle($x, $y, $box_width, $box_height);
//img: image url string
//img_w, img_h: original image size in px
//width, height: box size in pt
//bg_width, bg_height: box size in px
//x, y: left/top edge of box on page in pt
//start_x, start_y: placement of image relative to pattern
//$repeat: repeat mode
//$bg: GD object of result image
//$src: GD object of original image
//When using cpdf and optimization to direct png creation from gd object is available,
//don't create temp file, but place gd object directly into the pdf
if (!$is_png && $this->_canvas instanceof CPDF) {
// Note: CPDF_Adapter image converts y position
$this->_canvas->get_cpdf()->addImagePng($filedummy, $x, $this->_canvas->get_height() - $y - $height, $width, $height, $bg);
} else {
$tmp_dir = $this->_dompdf->getOptions()->getTempDir();
$tmp_name = tempnam($tmp_dir, "bg_dompdf_img_");
@unlink($tmp_name);
$tmp_file = "$tmp_name.png";
//debugpng
if ($this->_dompdf->getOptions()->getDebugPng()) print '[_background_image ' . $tmp_file . ']';
imagepng($bg, $tmp_file);
$this->_canvas->image($tmp_file, $x, $y, $width, $height);
imagedestroy($bg);
//debugpng
if ($this->_dompdf->getOptions()->getDebugPng()) {
print '[_background_image unlink ' . $tmp_file . ']';
}
if (!$this->_dompdf->getOptions()->getDebugKeepTemp()) {
unlink($tmp_file);
}
}
$this->_canvas->clipping_end();
}
/**
* @param $style
* @param $width
* @return array
*/
protected function _get_dash_pattern($style, $width)
{
$pattern = array();
switch ($style) {
default:
/*case "solid":
case "double":
case "groove":
case "inset":
case "outset":
case "ridge":*/
case "none":
break;
case "dotted":
if ($width <= 1)
$pattern = array($width, $width * 2);
else
$pattern = array($width);
break;
case "dashed":
$pattern = array(3 * $width);
break;
}
return $pattern;
}
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_none($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
return;
}
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_hidden($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
return;
}
// Border rendering functions
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_dotted($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
$this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "dotted", $r1, $r2);
}
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_dashed($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
$this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "dashed", $r1, $r2);
}
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_solid($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
// TODO: Solve rendering where one corner is beveled (radius == 0), one corner isn't.
if ($corner_style !== "bevel" || $r1 > 0 || $r2 > 0) {
// do it the simple way
$this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "solid", $r1, $r2);
return;
}
list($top, $right, $bottom, $left) = $widths;
// All this polygon business is for beveled corners...
switch ($side) {
case "top":
$points = array($x, $y,
$x + $length, $y,
$x + $length - $right, $y + $top,
$x + $left, $y + $top);
$this->_canvas->polygon($points, $color, null, null, true);
break;
case "bottom":
$points = array($x, $y,
$x + $length, $y,
$x + $length - $right, $y - $bottom,
$x + $left, $y - $bottom);
$this->_canvas->polygon($points, $color, null, null, true);
break;
case "left":
$points = array($x, $y,
$x, $y + $length,
$x + $left, $y + $length - $bottom,
$x + $left, $y + $top);
$this->_canvas->polygon($points, $color, null, null, true);
break;
case "right":
$points = array($x, $y,
$x, $y + $length,
$x - $right, $y + $length - $bottom,
$x - $right, $y + $top);
$this->_canvas->polygon($points, $color, null, null, true);
break;
default:
return;
}
}
/**
* @param $side
* @param $ratio
* @param $top
* @param $right
* @param $bottom
* @param $left
* @param $x
* @param $y
* @param $length
* @param $r1
* @param $r2
*/
protected function _apply_ratio($side, $ratio, $top, $right, $bottom, $left, &$x, &$y, &$length, &$r1, &$r2)
{
switch ($side) {
case "top":
$r1 -= $left * $ratio;
$r2 -= $right * $ratio;
$x += $left * $ratio;
$y += $top * $ratio;
$length -= $left * $ratio + $right * $ratio;
break;
case "bottom":
$r1 -= $right * $ratio;
$r2 -= $left * $ratio;
$x += $left * $ratio;
$y -= $bottom * $ratio;
$length -= $left * $ratio + $right * $ratio;
break;
case "left":
$r1 -= $top * $ratio;
$r2 -= $bottom * $ratio;
$x += $left * $ratio;
$y += $top * $ratio;
$length -= $top * $ratio + $bottom * $ratio;
break;
case "right":
$r1 -= $bottom * $ratio;
$r2 -= $top * $ratio;
$x -= $right * $ratio;
$y += $top * $ratio;
$length -= $top * $ratio + $bottom * $ratio;
break;
default:
return;
}
}
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_double($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
list($top, $right, $bottom, $left) = $widths;
$third_widths = array($top / 3, $right / 3, $bottom / 3, $left / 3);
// draw the outer border
$this->_border_solid($x, $y, $length, $color, $third_widths, $side, $corner_style, $r1, $r2);
$this->_apply_ratio($side, 2 / 3, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2);
$this->_border_solid($x, $y, $length, $color, $third_widths, $side, $corner_style, $r1, $r2);
}
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_groove($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
list($top, $right, $bottom, $left) = $widths;
$half_widths = array($top / 2, $right / 2, $bottom / 2, $left / 2);
$this->_border_inset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2);
$this->_apply_ratio($side, 0.5, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2);
$this->_border_outset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2);
}
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_ridge($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
list($top, $right, $bottom, $left) = $widths;
$half_widths = array($top / 2, $right / 2, $bottom / 2, $left / 2);
$this->_border_outset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2);
$this->_apply_ratio($side, 0.5, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2);
$this->_border_inset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2);
}
/**
* @param $c
* @return mixed
*/
protected function _tint($c)
{
if (!is_numeric($c))
return $c;
return min(1, $c + 0.16);
}
/**
* @param $c
* @return mixed
*/
protected function _shade($c)
{
if (!is_numeric($c))
return $c;
return max(0, $c - 0.33);
}
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_inset($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
switch ($side) {
case "top":
case "left":
$shade = array_map(array($this, "_shade"), $color);
$this->_border_solid($x, $y, $length, $shade, $widths, $side, $corner_style, $r1, $r2);
break;
case "bottom":
case "right":
$tint = array_map(array($this, "_tint"), $color);
$this->_border_solid($x, $y, $length, $tint, $widths, $side, $corner_style, $r1, $r2);
break;
default:
return;
}
}
/**
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param int $r1
* @param int $r2
*/
protected function _border_outset($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
switch ($side) {
case "top":
case "left":
$tint = array_map(array($this, "_tint"), $color);
$this->_border_solid($x, $y, $length, $tint, $widths, $side, $corner_style, $r1, $r2);
break;
case "bottom":
case "right":
$shade = array_map(array($this, "_shade"), $color);
$this->_border_solid($x, $y, $length, $shade, $widths, $side, $corner_style, $r1, $r2);
break;
default:
return;
}
}
/**
* Draws a solid, dotted, or dashed line, observing the border radius
*
* @param $x
* @param $y
* @param $length
* @param $color
* @param $widths
* @param $side
* @param string $corner_style
* @param $pattern_name
* @param int $r1
* @param int $r2
*
* @var $top
*/
protected function _border_line($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $pattern_name, $r1 = 0, $r2 = 0)
{
/** used by $$side */
list($top, $right, $bottom, $left) = $widths;
$width = $$side;
$pattern = $this->_get_dash_pattern($pattern_name, $width);
$half_width = $width / 2;
$r1 -= $half_width;
$r2 -= $half_width;
$adjust = $r1 / 80;
$length -= $width;
switch ($side) {
case "top":
$x += $half_width;
$y += $half_width;
if ($r1 > 0) {
$this->_canvas->arc($x + $r1, $y + $r1, $r1, $r1, 90 - $adjust, 135 + $adjust, $color, $width, $pattern);
}
$this->_canvas->line($x + $r1, $y, $x + $length - $r2, $y, $color, $width, $pattern);
if ($r2 > 0) {
$this->_canvas->arc($x + $length - $r2, $y + $r2, $r2, $r2, 45 - $adjust, 90 + $adjust, $color, $width, $pattern);
}
break;
case "bottom":
$x += $half_width;
$y -= $half_width;
if ($r1 > 0) {
$this->_canvas->arc($x + $r1, $y - $r1, $r1, $r1, 225 - $adjust, 270 + $adjust, $color, $width, $pattern);
}
$this->_canvas->line($x + $r1, $y, $x + $length - $r2, $y, $color, $width, $pattern);
if ($r2 > 0) {
$this->_canvas->arc($x + $length - $r2, $y - $r2, $r2, $r2, 270 - $adjust, 315 + $adjust, $color, $width, $pattern);
}
break;
case "left":
$y += $half_width;
$x += $half_width;
if ($r1 > 0) {
$this->_canvas->arc($x + $r1, $y + $r1, $r1, $r1, 135 - $adjust, 180 + $adjust, $color, $width, $pattern);
}
$this->_canvas->line($x, $y + $r1, $x, $y + $length - $r2, $color, $width, $pattern);
if ($r2 > 0) {
$this->_canvas->arc($x + $r2, $y + $length - $r2, $r2, $r2, 180 - $adjust, 225 + $adjust, $color, $width, $pattern);
}
break;
case "right":
$y += $half_width;
$x -= $half_width;
if ($r1 > 0) {
$this->_canvas->arc($x - $r1, $y + $r1, $r1, $r1, 0 - $adjust, 45 + $adjust, $color, $width, $pattern);
}
$this->_canvas->line($x, $y + $r1, $x, $y + $length - $r2, $color, $width, $pattern);
if ($r2 > 0) {
$this->_canvas->arc($x - $r2, $y + $length - $r2, $r2, $r2, 315 - $adjust, 360 + $adjust, $color, $width, $pattern);
}
break;
}
}
/**
* @param $opacity
*/
protected function _set_opacity($opacity)
{
if (is_numeric($opacity) && $opacity <= 1.0 && $opacity >= 0.0) {
$this->_canvas->set_opacity($opacity);
}
}
/**
* @param $box
* @param string $color
* @param array $style
*/
protected function _debug_layout($box, $color = "red", $style = array())
{
$this->_canvas->rectangle($box[0], $box[1], $box[2], $box[3], Color::parse($color), 0.1, $style);
}
}
@@ -0,0 +1,262 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Frame;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\Helpers;
/**
* Renders block frames
*
* @package dompdf
*/
class Block extends AbstractRenderer
{
/**
* @param Frame $frame
*/
function render(Frame $frame)
{
$style = $frame->get_style();
$node = $frame->get_node();
list($x, $y, $w, $h) = $frame->get_border_box();
$this->_set_opacity($frame->get_opacity($style->opacity));
if ($node->nodeName === "body") {
$h = $frame->get_containing_block("h") - (float)$style->length_in_pt(array(
$style->margin_top,
$style->border_top_width,
$style->border_bottom_width,
$style->margin_bottom),
$style->width);
}
// Handle anchors & links
if ($node->nodeName === "a" && $href = $node->getAttribute("href")) {
$href = Helpers::build_url($this->_dompdf->getProtocol(), $this->_dompdf->getBaseHost(), $this->_dompdf->getBasePath(), $href);
$this->_canvas->add_link($href, $x, $y, (float)$w, (float)$h);
}
// Draw our background, border and content
list($tl, $tr, $br, $bl) = $style->get_computed_border_radius($w, $h);
if ($tl + $tr + $br + $bl > 0) {
$this->_canvas->clipping_roundrectangle($x, $y, (float)$w, (float)$h, $tl, $tr, $br, $bl);
}
if (($bg = $style->background_color) !== "transparent") {
$this->_canvas->filled_rectangle($x, $y, (float)$w, (float)$h, $bg);
}
if (($url = $style->background_image) && $url !== "none") {
$this->_background_image($url, $x, $y, $w, $h, $style);
}
if ($tl + $tr + $br + $bl > 0) {
$this->_canvas->clipping_end();
}
$border_box = array($x, $y, $w, $h);
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
if ($this->_dompdf->getOptions()->getDebugLayout() && $this->_dompdf->getOptions()->getDebugLayoutBlocks()) {
$this->_debug_layout($frame->get_border_box(), "red");
if ($this->_dompdf->getOptions()->getDebugLayoutPaddingBox()) {
$this->_debug_layout($frame->get_padding_box(), "red", array(0.5, 0.5));
}
}
if ($this->_dompdf->getOptions()->getDebugLayout() && $this->_dompdf->getOptions()->getDebugLayoutLines() && $frame->get_decorator()) {
foreach ($frame->get_decorator()->get_line_boxes() as $line) {
$frame->_debug_layout(array($line->x, $line->y, $line->w, $line->h), "orange");
}
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
}
/**
* @param AbstractFrameDecorator $frame
* @param null $border_box
* @param string $corner_style
*/
protected function _render_border(AbstractFrameDecorator $frame, $border_box = null, $corner_style = "bevel")
{
$style = $frame->get_style();
$bp = $style->get_border_properties();
if (empty($border_box)) {
$border_box = $frame->get_border_box();
}
// find the radius
$radius = $style->get_computed_border_radius($border_box[2], $border_box[3]); // w, h
// Short-cut: If all the borders are "solid" with the same color and style, and no radius, we'd better draw a rectangle
if (
in_array($bp["top"]["style"], array("solid", "dashed", "dotted")) &&
$bp["top"] == $bp["right"] &&
$bp["right"] == $bp["bottom"] &&
$bp["bottom"] == $bp["left"] &&
array_sum($radius) == 0
) {
$props = $bp["top"];
if ($props["color"] === "transparent" || $props["width"] <= 0) {
return;
}
list($x, $y, $w, $h) = $border_box;
$width = (float)$style->length_in_pt($props["width"]);
$pattern = $this->_get_dash_pattern($props["style"], $width);
$this->_canvas->rectangle($x + $width / 2, $y + $width / 2, (float)$w - $width, (float)$h - $width, $props["color"], $width, $pattern);
return;
}
// Do it the long way
$widths = array(
(float)$style->length_in_pt($bp["top"]["width"]),
(float)$style->length_in_pt($bp["right"]["width"]),
(float)$style->length_in_pt($bp["bottom"]["width"]),
(float)$style->length_in_pt($bp["left"]["width"])
);
foreach ($bp as $side => $props) {
list($x, $y, $w, $h) = $border_box;
$length = 0;
$r1 = 0;
$r2 = 0;
if (!$props["style"] ||
$props["style"] === "none" ||
$props["width"] <= 0 ||
$props["color"] == "transparent"
) {
continue;
}
switch ($side) {
case "top":
$length = (float)$w;
$r1 = $radius["top-left"];
$r2 = $radius["top-right"];
break;
case "bottom":
$length = (float)$w;
$y += (float)$h;
$r1 = $radius["bottom-left"];
$r2 = $radius["bottom-right"];
break;
case "left":
$length = (float)$h;
$r1 = $radius["top-left"];
$r2 = $radius["bottom-left"];
break;
case "right":
$length = (float)$h;
$x += (float)$w;
$r1 = $radius["top-right"];
$r2 = $radius["bottom-right"];
break;
default:
break;
}
$method = "_border_" . $props["style"];
// draw rounded corners
$this->$method($x, $y, $length, $props["color"], $widths, $side, $corner_style, $r1, $r2);
}
}
/**
* @param AbstractFrameDecorator $frame
* @param null $border_box
* @param string $corner_style
*/
protected function _render_outline(AbstractFrameDecorator $frame, $border_box = null, $corner_style = "bevel")
{
$style = $frame->get_style();
$props = array(
"width" => $style->outline_width,
"style" => $style->outline_style,
"color" => $style->outline_color,
);
if (!$props["style"] || $props["style"] === "none" || $props["width"] <= 0) {
return;
}
if (empty($border_box)) {
$border_box = $frame->get_border_box();
}
$offset = (float)$style->length_in_pt($props["width"]);
$pattern = $this->_get_dash_pattern($props["style"], $offset);
// If the outline style is "solid" we'd better draw a rectangle
if (in_array($props["style"], array("solid", "dashed", "dotted"))) {
$border_box[0] -= $offset / 2;
$border_box[1] -= $offset / 2;
$border_box[2] += $offset;
$border_box[3] += $offset;
list($x, $y, $w, $h) = $border_box;
$this->_canvas->rectangle($x, $y, (float)$w, (float)$h, $props["color"], $offset, $pattern);
return;
}
$border_box[0] -= $offset;
$border_box[1] -= $offset;
$border_box[2] += $offset * 2;
$border_box[3] += $offset * 2;
$method = "_border_" . $props["style"];
$widths = array_fill(0, 4, (float)$style->length_in_pt($props["width"]));
$sides = array("top", "right", "left", "bottom");
$length = 0;
foreach ($sides as $side) {
list($x, $y, $w, $h) = $border_box;
switch ($side) {
case "top":
$length = (float)$w;
break;
case "bottom":
$length = (float)$w;
$y += (float)$h;
break;
case "left":
$length = (float)$h;
break;
case "right":
$length = (float)$h;
$x += (float)$w;
break;
default:
break;
}
$this->$method($x, $y, $length, $props["color"], $widths, $side, $corner_style);
}
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Frame;
use Dompdf\Image\Cache;
/**
* Image renderer
*
* @access private
* @package dompdf
*/
class Image extends Block
{
/**
* @param Frame $frame
*/
function render(Frame $frame)
{
// Render background & borders
$style = $frame->get_style();
$cb = $frame->get_containing_block();
list($x, $y, $w, $h) = $frame->get_border_box();
$this->_set_opacity($frame->get_opacity($style->opacity));
list($tl, $tr, $br, $bl) = $style->get_computed_border_radius($w, $h);
$has_border_radius = $tl + $tr + $br + $bl > 0;
if ($has_border_radius) {
$this->_canvas->clipping_roundrectangle($x, $y, (float)$w, (float)$h, $tl, $tr, $br, $bl);
}
if (($bg = $style->background_color) !== "transparent") {
$this->_canvas->filled_rectangle($x, $y, (float)$w, (float)$h, $bg);
}
if (($url = $style->background_image) && $url !== "none") {
$this->_background_image($url, $x, $y, $w, $h, $style);
}
if ($has_border_radius) {
$this->_canvas->clipping_end();
}
$this->_render_border($frame);
$this->_render_outline($frame);
list($x, $y) = $frame->get_padding_box();
$x += (float)$style->length_in_pt($style->padding_left, $cb["w"]);
$y += (float)$style->length_in_pt($style->padding_top, $cb["h"]);
$w = (float)$style->length_in_pt($style->width, $cb["w"]);
$h = (float)$style->length_in_pt($style->height, $cb["h"]);
if ($has_border_radius) {
list($wt, $wr, $wb, $wl) = array(
$style->border_top_width,
$style->border_right_width,
$style->border_bottom_width,
$style->border_left_width,
);
// we have to get the "inner" radius
if ($tl > 0) {
$tl -= ($wt + $wl) / 2;
}
if ($tr > 0) {
$tr -= ($wt + $wr) / 2;
}
if ($br > 0) {
$br -= ($wb + $wr) / 2;
}
if ($bl > 0) {
$bl -= ($wb + $wl) / 2;
}
$this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl);
}
$src = $frame->get_image_url();
$alt = null;
if (Cache::is_broken($src) &&
$alt = $frame->get_node()->getAttribute("alt")
) {
$font = $style->font_family;
$size = $style->font_size;
$spacing = $style->word_spacing;
$this->_canvas->text(
$x,
$y,
$alt,
$font,
$size,
$style->color,
$spacing
);
} else {
$this->_canvas->image($src, $x, $y, $w, $h, $style->image_resolution);
}
if ($has_border_radius) {
$this->_canvas->clipping_end();
}
if ($msg = $frame->get_image_msg()) {
$parts = preg_split("/\s*\n\s*/", $msg);
$height = 10;
$_y = $alt ? $y + $h - count($parts) * $height : $y;
foreach ($parts as $i => $_part) {
$this->_canvas->text($x, $_y + $i * $height, $_part, "times", $height * 0.8, array(0.5, 0.5, 0.5));
}
}
if ($this->_dompdf->getOptions()->getDebugLayout() && $this->_dompdf->getOptions()->getDebugLayoutBlocks()) {
$this->_debug_layout($frame->get_border_box(), "blue");
if ($this->_dompdf->getOptions()->getDebugLayoutPaddingBox()) {
$this->_debug_layout($frame->get_padding_box(), "blue", array(0.5, 0.5));
}
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
}
}
@@ -0,0 +1,211 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Frame;
use Dompdf\Helpers;
/**
* Renders inline frames
*
* @access private
* @package dompdf
*/
class Inline extends AbstractRenderer
{
/**
* @param Frame $frame
*/
function render(Frame $frame)
{
$style = $frame->get_style();
if (!$frame->get_first_child()) {
return; // No children, no service
}
// Draw the left border if applicable
$bp = $style->get_border_properties();
$widths = array(
(float)$style->length_in_pt($bp["top"]["width"]),
(float)$style->length_in_pt($bp["right"]["width"]),
(float)$style->length_in_pt($bp["bottom"]["width"]),
(float)$style->length_in_pt($bp["left"]["width"])
);
// Draw the background & border behind each child. To do this we need
// to figure out just how much space each child takes:
list($x, $y) = $frame->get_first_child()->get_position();
$w = null;
$h = 0;
// $x += $widths[3];
// $y += $widths[0];
$this->_set_opacity($frame->get_opacity($style->opacity));
$first_row = true;
$DEBUGLAYOUTINLINE = $this->_dompdf->getOptions()->getDebugLayout() && $this->_dompdf->getOptions()->getDebugLayoutInline();
foreach ($frame->get_children() as $child) {
list($child_x, $child_y, $child_w, $child_h) = $child->get_padding_box();
if (!is_null($w) && $child_x < $x + $w) {
//This branch seems to be supposed to being called on the first part
//of an inline html element, and the part after the if clause for the
//parts after a line break.
//But because $w initially mostly is 0, and gets updated only on the next
//round, this seem to be never executed and the common close always.
// The next child is on another line. Draw the background &
// borders on this line.
// Background:
if (($bg = $style->background_color) !== "transparent") {
$this->_canvas->filled_rectangle($x, $y, $w, $h, $bg);
}
if (($url = $style->background_image) && $url !== "none") {
$this->_background_image($url, $x, $y, $w, $h, $style);
}
// If this is the first row, draw the left border
if ($first_row) {
if ($bp["left"]["style"] !== "none" && $bp["left"]["color"] !== "transparent" && $bp["left"]["width"] > 0) {
$method = "_border_" . $bp["left"]["style"];
$this->$method($x, $y, $h + $widths[0] + $widths[2], $bp["left"]["color"], $widths, "left");
}
$first_row = false;
}
// Draw the top & bottom borders
if ($bp["top"]["style"] !== "none" && $bp["top"]["color"] !== "transparent" && $bp["top"]["width"] > 0) {
$method = "_border_" . $bp["top"]["style"];
$this->$method($x, $y, $w + $widths[1] + $widths[3], $bp["top"]["color"], $widths, "top");
}
if ($bp["bottom"]["style"] !== "none" && $bp["bottom"]["color"] !== "transparent" && $bp["bottom"]["width"] > 0) {
$method = "_border_" . $bp["bottom"]["style"];
$this->$method($x, $y + $h + $widths[0] + $widths[2], $w + $widths[1] + $widths[3], $bp["bottom"]["color"], $widths, "bottom");
}
// Handle anchors & links
$link_node = null;
if ($frame->get_node()->nodeName === "a") {
$link_node = $frame->get_node();
} else if ($frame->get_parent()->get_node()->nodeName === "a") {
$link_node = $frame->get_parent()->get_node();
}
if ($link_node && $href = $link_node->getAttribute("href")) {
$href = Helpers::build_url($this->_dompdf->getProtocol(), $this->_dompdf->getBaseHost(), $this->_dompdf->getBasePath(), $href);
$this->_canvas->add_link($href, $x, $y, $w, $h);
}
$x = $child_x;
$y = $child_y;
$w = (float)$child_w;
$h = (float)$child_h;
continue;
}
if (is_null($w)) {
$w = (float)$child_w;
}else {
$w += (float)$child_w;
}
$h = max($h, $child_h);
if ($DEBUGLAYOUTINLINE) {
$this->_debug_layout($child->get_border_box(), "blue");
if ($this->_dompdf->getOptions()->getDebugLayoutPaddingBox()) {
$this->_debug_layout($child->get_padding_box(), "blue", array(0.5, 0.5));
}
}
}
// Handle the last child
if (($bg = $style->background_color) !== "transparent") {
$this->_canvas->filled_rectangle($x + $widths[3], $y + $widths[0], $w, $h, $bg);
}
//On continuation lines (after line break) of inline elements, the style got copied.
//But a non repeatable background image should not be repeated on the next line.
//But removing the background image above has never an effect, and removing it below
//removes it always, even on the initial line.
//Need to handle it elsewhere, e.g. on certain ...clone()... usages.
// Repeat not given: default is Style::__construct
// ... && (!($repeat = $style->background_repeat) || $repeat === "repeat" ...
//different position? $this->_background_image($url, $x, $y, $w, $h, $style);
if (($url = $style->background_image) && $url !== "none") {
$this->_background_image($url, $x + $widths[3], $y + $widths[0], $w, $h, $style);
}
// Add the border widths
$w += (float)$widths[1] + (float)$widths[3];
$h += (float)$widths[0] + (float)$widths[2];
// make sure the border and background start inside the left margin
$left_margin = (float)$style->length_in_pt($style->margin_left);
$x += $left_margin;
// If this is the first row, draw the left border too
if ($first_row && $bp["left"]["style"] !== "none" && $bp["left"]["color"] !== "transparent" && $widths[3] > 0) {
$method = "_border_" . $bp["left"]["style"];
$this->$method($x, $y, $h, $bp["left"]["color"], $widths, "left");
}
// Draw the top & bottom borders
if ($bp["top"]["style"] !== "none" && $bp["top"]["color"] !== "transparent" && $widths[0] > 0) {
$method = "_border_" . $bp["top"]["style"];
$this->$method($x, $y, $w, $bp["top"]["color"], $widths, "top");
}
if ($bp["bottom"]["style"] !== "none" && $bp["bottom"]["color"] !== "transparent" && $widths[2] > 0) {
$method = "_border_" . $bp["bottom"]["style"];
$this->$method($x, $y + $h, $w, $bp["bottom"]["color"], $widths, "bottom");
}
// Helpers::var_dump(get_class($frame->get_next_sibling()));
// $last_row = get_class($frame->get_next_sibling()) !== 'Inline';
// Draw the right border if this is the last row
if ($bp["right"]["style"] !== "none" && $bp["right"]["color"] !== "transparent" && $widths[1] > 0) {
$method = "_border_" . $bp["right"]["style"];
$this->$method($x + $w, $y, $h, $bp["right"]["color"], $widths, "right");
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
// Only two levels of links frames
$link_node = null;
if ($frame->get_node()->nodeName === "a") {
$link_node = $frame->get_node();
if (($name = $link_node->getAttribute("name"))) {
$this->_canvas->add_named_dest($name);
}
}
if ($frame->get_parent() && $frame->get_parent()->get_node()->nodeName === "a") {
$link_node = $frame->get_parent()->get_node();
}
// Handle anchors & links
if ($link_node) {
if ($href = $link_node->getAttribute("href")) {
$href = Helpers::build_url($this->_dompdf->getProtocol(), $this->_dompdf->getBaseHost(), $this->_dompdf->getBasePath(), $href);
$this->_canvas->add_link($href, $x, $y, $w, $h);
}
}
}
}
@@ -0,0 +1,257 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Helmut Tischer <htischer@weihenstephan.org>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Helpers;
use Dompdf\FontMetrics;
use Dompdf\Frame;
use Dompdf\Image\Cache;
use Dompdf\FrameDecorator\ListBullet as ListBulletFrameDecorator;
/**
* Renders list bullets
*
* @access private
* @package dompdf
*/
class ListBullet extends AbstractRenderer
{
/**
* @param $type
* @return mixed|string
*/
static function get_counter_chars($type)
{
static $cache = array();
if (isset($cache[$type])) {
return $cache[$type];
}
$uppercase = false;
$text = "";
switch ($type) {
case "decimal-leading-zero":
case "decimal":
case "1":
return "0123456789";
case "upper-alpha":
case "upper-latin":
case "A":
$uppercase = true;
case "lower-alpha":
case "lower-latin":
case "a":
$text = "abcdefghijklmnopqrstuvwxyz";
break;
case "upper-roman":
case "I":
$uppercase = true;
case "lower-roman":
case "i":
$text = "ivxlcdm";
break;
case "lower-greek":
for ($i = 0; $i < 24; $i++) {
$text .= Helpers::unichr($i + 944);
}
break;
}
if ($uppercase) {
$text = strtoupper($text);
}
return $cache[$type] = "$text.";
}
/**
* @param integer $n
* @param string $type
* @param integer $pad
*
* @return string
*/
private function make_counter($n, $type, $pad = null)
{
$n = intval($n);
$text = "";
$uppercase = false;
switch ($type) {
case "decimal-leading-zero":
case "decimal":
case "1":
if ($pad)
$text = str_pad($n, $pad, "0", STR_PAD_LEFT);
else
$text = $n;
break;
case "upper-alpha":
case "upper-latin":
case "A":
$uppercase = true;
case "lower-alpha":
case "lower-latin":
case "a":
$text = chr(($n % 26) + ord('a') - 1);
break;
case "upper-roman":
case "I":
$uppercase = true;
case "lower-roman":
case "i":
$text = Helpers::dec2roman($n);
break;
case "lower-greek":
$text = Helpers::unichr($n + 944);
break;
}
if ($uppercase) {
$text = strtoupper($text);
}
return "$text.";
}
/**
* @param Frame $frame
*/
function render(Frame $frame)
{
$style = $frame->get_style();
$font_size = $style->get_font_size();
$line_height = (float)$style->length_in_pt($style->line_height, $frame->get_containing_block("w"));
$this->_set_opacity($frame->get_opacity($style->opacity));
$li = $frame->get_parent();
// Don't render bullets twice if if was split
if ($li->_splitted) {
return;
}
// Handle list-style-image
// If list style image is requested but missing, fall back to predefined types
if ($style->list_style_image !== "none" &&
!Cache::is_broken($img = $frame->get_image_url())
) {
list($x, $y) = $frame->get_position();
//For expected size and aspect, instead of box size, use image natural size scaled to DPI.
// Resample the bullet image to be consistent with 'auto' sized images
// See also Image::get_min_max_width
// Tested php ver: value measured in px, suffix "px" not in value: rtrim unnecessary.
//$w = $frame->get_width();
//$h = $frame->get_height();
list($width, $height) = Helpers::dompdf_getimagesize($img, $this->_dompdf->getHttpContext());
$dpi = $this->_dompdf->getOptions()->getDpi();
$w = ((float)rtrim($width, "px") * 72) / $dpi;
$h = ((float)rtrim($height, "px") * 72) / $dpi;
$x -= $w;
$y -= ($line_height - $font_size) / 2; //Reverse hinting of list_bullet_positioner
$this->_canvas->image($img, $x, $y, $w, $h);
} else {
$bullet_style = $style->list_style_type;
$fill = false;
switch ($bullet_style) {
default:
/** @noinspection PhpMissingBreakStatementInspection */
case "disc":
$fill = true;
case "circle":
list($x, $y) = $frame->get_position();
$r = ($font_size * (ListBulletFrameDecorator::BULLET_SIZE /*-ListBulletFrameDecorator::BULLET_THICKNESS*/)) / 2;
$x -= $font_size * (ListBulletFrameDecorator::BULLET_SIZE / 2);
$y += ($font_size * (1 - ListBulletFrameDecorator::BULLET_DESCENT)) / 2;
$o = $font_size * ListBulletFrameDecorator::BULLET_THICKNESS;
$this->_canvas->circle($x, $y, $r, $style->color, $o, null, $fill);
break;
case "square":
list($x, $y) = $frame->get_position();
$w = $font_size * ListBulletFrameDecorator::BULLET_SIZE;
$x -= $w;
$y += ($font_size * (1 - ListBulletFrameDecorator::BULLET_DESCENT - ListBulletFrameDecorator::BULLET_SIZE)) / 2;
$this->_canvas->filled_rectangle($x, $y, $w, $w, $style->color);
break;
case "decimal-leading-zero":
case "decimal":
case "lower-alpha":
case "lower-latin":
case "lower-roman":
case "lower-greek":
case "upper-alpha":
case "upper-latin":
case "upper-roman":
case "1": // HTML 4.0 compatibility
case "a":
case "i":
case "A":
case "I":
$pad = null;
if ($bullet_style === "decimal-leading-zero") {
$pad = strlen($li->get_parent()->get_node()->getAttribute("dompdf-children-count"));
}
$node = $frame->get_node();
if (!$node->hasAttribute("dompdf-counter")) {
return;
}
$index = $node->getAttribute("dompdf-counter");
$text = $this->make_counter($index, $bullet_style, $pad);
if (trim($text) == "") {
return;
}
$spacing = 0;
$font_family = $style->font_family;
$line = $li->get_containing_line();
list($x, $y) = array($frame->get_position("x"), $line->y);
$x -= $this->_dompdf->getFontMetrics()->getTextWidth($text, $font_family, $font_size, $spacing);
// Take line-height into account
$line_height = $style->line_height;
$y += ($line_height - $font_size) / 4; // FIXME I thought it should be 2, but 4 gives better results
$this->_canvas->text($x, $y, $text,
$font_family, $font_size,
$style->color, $spacing);
case "none":
break;
}
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
}
}
@@ -0,0 +1,173 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Table;
/**
* Renders table cells
*
* @package dompdf
*/
class TableCell extends Block
{
/**
* @param Frame $frame
*/
function render(Frame $frame)
{
$style = $frame->get_style();
if (trim($frame->get_node()->nodeValue) === "" && $style->empty_cells === "hide") {
return;
}
$this->_set_opacity($frame->get_opacity($style->opacity));
list($x, $y, $w, $h) = $frame->get_border_box();
// Draw our background, border and content
if (($bg = $style->background_color) !== "transparent") {
$this->_canvas->filled_rectangle($x, $y, (float)$w, (float)$h, $bg);
}
if (($url = $style->background_image) && $url !== "none") {
$this->_background_image($url, $x, $y, $w, $h, $style);
}
$table = Table::find_parent_table($frame);
if ($table->get_style()->border_collapse !== "collapse") {
$this->_render_border($frame);
$this->_render_outline($frame);
return;
}
// The collapsed case is slightly complicated...
// @todo Add support for outlines here
$cellmap = $table->get_cellmap();
$cells = $cellmap->get_spanned_cells($frame);
$num_rows = $cellmap->get_num_rows();
$num_cols = $cellmap->get_num_cols();
// Determine the top row spanned by this cell
$i = $cells["rows"][0];
$top_row = $cellmap->get_row($i);
// Determine if this cell borders on the bottom of the table. If so,
// then we draw its bottom border. Otherwise the next row down will
// draw its top border instead.
if (in_array($num_rows - 1, $cells["rows"])) {
$draw_bottom = true;
$bottom_row = $cellmap->get_row($num_rows - 1);
} else {
$draw_bottom = false;
}
// Draw the horizontal borders
foreach ($cells["columns"] as $j) {
$bp = $cellmap->get_border_properties($i, $j);
$y = $top_row["y"] - $bp["top"]["width"] / 2;
$col = $cellmap->get_column($j);
$x = $col["x"] - $bp["left"]["width"] / 2;
$w = $col["used-width"] + ($bp["left"]["width"] + $bp["right"]["width"]) / 2;
if ($bp["top"]["style"] !== "none" && $bp["top"]["width"] > 0) {
$widths = array(
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
);
$method = "_border_" . $bp["top"]["style"];
$this->$method($x, $y, $w, $bp["top"]["color"], $widths, "top", "square");
}
if ($draw_bottom) {
$bp = $cellmap->get_border_properties($num_rows - 1, $j);
if ($bp["bottom"]["style"] === "none" || $bp["bottom"]["width"] <= 0)
continue;
$y = $bottom_row["y"] + $bottom_row["height"] + $bp["bottom"]["width"] / 2;
$widths = array(
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
);
$method = "_border_" . $bp["bottom"]["style"];
$this->$method($x, $y, $w, $bp["bottom"]["color"], $widths, "bottom", "square");
}
}
$j = $cells["columns"][0];
$left_col = $cellmap->get_column($j);
if (in_array($num_cols - 1, $cells["columns"])) {
$draw_right = true;
$right_col = $cellmap->get_column($num_cols - 1);
} else {
$draw_right = false;
}
// Draw the vertical borders
foreach ($cells["rows"] as $i) {
$bp = $cellmap->get_border_properties($i, $j);
$x = $left_col["x"] - $bp["left"]["width"] / 2;
$row = $cellmap->get_row($i);
$y = $row["y"] - $bp["top"]["width"] / 2;
$h = $row["height"] + ($bp["top"]["width"] + $bp["bottom"]["width"]) / 2;
if ($bp["left"]["style"] !== "none" && $bp["left"]["width"] > 0) {
$widths = array(
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
);
$method = "_border_" . $bp["left"]["style"];
$this->$method($x, $y, $h, $bp["left"]["color"], $widths, "left", "square");
}
if ($draw_right) {
$bp = $cellmap->get_border_properties($i, $num_cols - 1);
if ($bp["right"]["style"] === "none" || $bp["right"]["width"] <= 0) {
continue;
}
$x = $right_col["x"] + $right_col["used-width"] + $bp["right"]["width"] / 2;
$widths = array(
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
);
$method = "_border_" . $bp["right"]["style"];
$this->$method($x, $y, $h, $bp["right"]["color"], $widths, "right", "square");
}
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
}
}
@@ -0,0 +1,50 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Frame;
/**
* Renders block frames
*
* @package dompdf
*/
class TableRowGroup extends Block
{
/**
* @param Frame $frame
*/
function render(Frame $frame)
{
$style = $frame->get_style();
$this->_set_opacity($frame->get_opacity($style->opacity));
$this->_render_border($frame);
$this->_render_outline($frame);
if ($this->_dompdf->getOptions()->getDebugLayout() && $this->_dompdf->getOptions()->getDebugLayoutBlocks()) {
$this->_debug_layout($frame->get_border_box(), "red");
if ($this->_dompdf->getOptions()->getDebugLayoutPaddingBox()) {
$this->_debug_layout($frame->get_padding_box(), "red", array(0.5, 0.5));
}
}
if ($this->_dompdf->getOptions()->getDebugLayout() && $this->_dompdf->getOptions()->getDebugLayoutLines() && $frame->get_decorator()) {
foreach ($frame->get_decorator()->get_line_boxes() as $line) {
$frame->_debug_layout(array($line->x, $line->y, $line->w, $line->h), "orange");
}
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
}
}
@@ -0,0 +1,167 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @author Helmut Tischer <htischer@weihenstephan.org>
* @author Fabien Ménager <fabien.menager@gmail.com>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Adapter\CPDF;
use Dompdf\FontMetrics;
use Dompdf\Frame;
/**
* Renders text frames
*
* @package dompdf
*/
class Text extends AbstractRenderer
{
/** Thickness of underline. Screen: 0.08, print: better less, e.g. 0.04 */
const DECO_THICKNESS = 0.02;
//Tweaking if $base and $descent are not accurate.
//Check method_exists( $this->_canvas, "get_cpdf" )
//- For cpdf these can and must stay 0, because font metrics are used directly.
//- For other renderers, if different values are wanted, separate the parameter sets.
// But $size and $size-$height seem to be accurate enough
/** Relative to bottom of text, as fraction of height */
const UNDERLINE_OFFSET = 0.0;
/** Relative to top of text */
const OVERLINE_OFFSET = 0.0;
/** Relative to centre of text. */
const LINETHROUGH_OFFSET = 0.0;
/** How far to extend lines past either end, in pt */
const DECO_EXTENSION = 0.0;
/**
* @param \Dompdf\FrameDecorator\Text $frame
*/
function render(Frame $frame)
{
$text = $frame->get_text();
if (trim($text) === "") {
return;
}
$style = $frame->get_style();
list($x, $y) = $frame->get_position();
$cb = $frame->get_containing_block();
if (($ml = $style->margin_left) === "auto" || $ml === "none") {
$ml = 0;
}
if (($pl = $style->padding_left) === "auto" || $pl === "none") {
$pl = 0;
}
if (($bl = $style->border_left_width) === "auto" || $bl === "none") {
$bl = 0;
}
$x += (float)$style->length_in_pt(array($ml, $pl, $bl), $cb["w"]);
$font = $style->font_family;
$size = $frame_font_size = $style->font_size;
$word_spacing = $frame->get_text_spacing() + (float)$style->length_in_pt($style->word_spacing);
$char_spacing = (float)$style->length_in_pt($style->letter_spacing);
$width = $style->width;
/*$text = str_replace(
array("{PAGE_NUM}"),
array($this->_canvas->get_page_number()),
$text
);*/
$this->_canvas->text($x, $y, $text,
$font, $size,
$style->color, $word_spacing, $char_spacing);
$line = $frame->get_containing_line();
// FIXME Instead of using the tallest frame to position,
// the decoration, the text should be well placed
if (false && $line->tallest_frame) {
$base_frame = $line->tallest_frame;
$style = $base_frame->get_style();
$size = $style->font_size;
}
$line_thickness = $size * self::DECO_THICKNESS;
$underline_offset = $size * self::UNDERLINE_OFFSET;
$overline_offset = $size * self::OVERLINE_OFFSET;
$linethrough_offset = $size * self::LINETHROUGH_OFFSET;
$underline_position = -0.08;
if ($this->_canvas instanceof CPDF) {
$cpdf_font = $this->_canvas->get_cpdf()->fonts[$style->font_family];
if (isset($cpdf_font["UnderlinePosition"])) {
$underline_position = $cpdf_font["UnderlinePosition"] / 1000;
}
if (isset($cpdf_font["UnderlineThickness"])) {
$line_thickness = $size * ($cpdf_font["UnderlineThickness"] / 1000);
}
}
$descent = $size * $underline_position;
$base = $size;
// Handle text decoration:
// http://www.w3.org/TR/CSS21/text.html#propdef-text-decoration
// Draw all applicable text-decorations. Start with the root and work our way down.
$p = $frame;
$stack = array();
while ($p = $p->get_parent()) {
$stack[] = $p;
}
while (isset($stack[0])) {
$f = array_pop($stack);
if (($text_deco = $f->get_style()->text_decoration) === "none") {
continue;
}
$deco_y = $y; //$line->y;
$color = $f->get_style()->color;
switch ($text_deco) {
default:
continue;
case "underline":
$deco_y += $base - $descent + $underline_offset + $line_thickness / 2;
break;
case "overline":
$deco_y += $overline_offset + $line_thickness / 2;
break;
case "line-through":
$deco_y += $base * 0.7 + $linethrough_offset;
break;
}
$dx = 0;
$x1 = $x - self::DECO_EXTENSION;
$x2 = $x + $width + $dx + self::DECO_EXTENSION;
$this->_canvas->line($x1, $deco_y, $x2, $deco_y, $color, $line_thickness);
}
if ($this->_dompdf->getOptions()->getDebugLayout() && $this->_dompdf->getOptions()->getDebugLayoutLines()) {
$text_width = $this->_dompdf->getFontMetrics()->getTextWidth($text, $font, $frame_font_size);
$this->_debug_layout(array($x, $y, $text_width + ($line->wc - 1) * $word_spacing, $frame_font_size), "orange", array(0.5, 0.5));
}
}
}
@@ -0,0 +1,295 @@
<?php
/**
* @package dompdf
* @link http://dompdf.github.com/
* @author Benj Carson <benjcarson@digitaljunkies.ca>
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
use Dompdf\Renderer\AbstractRenderer;
use Dompdf\Renderer\Block;
use Dompdf\Renderer\Image;
use Dompdf\Renderer\ListBullet;
use Dompdf\Renderer\TableCell;
use Dompdf\Renderer\TableRowGroup;
use Dompdf\Renderer\Text;
use Dompdf\Frame;
/**
* Concrete renderer
*
* Instantiates several specific renderers in order to render any given frame.
*
* @package dompdf
*/
class Renderer extends AbstractRenderer
{
/**
* Array of renderers for specific frame types
*
* @var AbstractRenderer[]
*/
protected $_renderers;
/**
* Cache of the callbacks array
*
* @var array
*/
private $_callbacks;
/**
* Advance the canvas to the next page
*/
function new_page()
{
$this->_canvas->new_page();
}
/**
* Render frames recursively
*
* @param Frame $frame the frame to render
*/
public function render(Frame $frame)
{
global $_dompdf_debug;
if ($_dompdf_debug) {
echo $frame;
flush();
}
$style = $frame->get_style();
if (in_array($style->visibility, array("hidden", "collapse"))) {
return;
}
$display = $style->display;
// Starts the CSS transformation
if ($style->transform && is_array($style->transform)) {
$this->_canvas->save();
list($x, $y) = $frame->get_padding_box();
$origin = $style->transform_origin;
foreach ($style->transform as $transform) {
list($function, $values) = $transform;
if ($function === "matrix") {
$function = "transform";
}
$values = array_map("floatval", $values);
$values[] = $x + (float)$style->length_in_pt($origin[0], $style->width);
$values[] = $y + (float)$style->length_in_pt($origin[1], $style->height);
call_user_func_array(array($this->_canvas, $function), $values);
}
}
switch ($display) {
case "block":
case "list-item":
case "inline-block":
case "table":
case "inline-table":
$this->_render_frame("block", $frame);
break;
case "inline":
if ($frame->is_text_node()) {
$this->_render_frame("text", $frame);
} else {
$this->_render_frame("inline", $frame);
}
break;
case "table-cell":
$this->_render_frame("table-cell", $frame);
break;
case "table-row-group":
case "table-header-group":
case "table-footer-group":
$this->_render_frame("table-row-group", $frame);
break;
case "-dompdf-list-bullet":
$this->_render_frame("list-bullet", $frame);
break;
case "-dompdf-image":
$this->_render_frame("image", $frame);
break;
case "none":
$node = $frame->get_node();
if ($node->nodeName === "script") {
if ($node->getAttribute("type") === "text/php" ||
$node->getAttribute("language") === "php"
) {
// Evaluate embedded php scripts
$this->_render_frame("php", $frame);
} elseif ($node->getAttribute("type") === "text/javascript" ||
$node->getAttribute("language") === "javascript"
) {
// Insert JavaScript
$this->_render_frame("javascript", $frame);
}
}
// Don't render children, so skip to next iter
return;
default:
break;
}
// Starts the overflow: hidden box
if ($style->overflow === "hidden") {
list($x, $y, $w, $h) = $frame->get_padding_box();
// get border radii
$style = $frame->get_style();
list($tl, $tr, $br, $bl) = $style->get_computed_border_radius($w, $h);
if ($tl + $tr + $br + $bl > 0) {
$this->_canvas->clipping_roundrectangle($x, $y, (float)$w, (float)$h, $tl, $tr, $br, $bl);
} else {
$this->_canvas->clipping_rectangle($x, $y, (float)$w, (float)$h);
}
}
$stack = array();
foreach ($frame->get_children() as $child) {
// < 0 : nagative z-index
// = 0 : no z-index, no stacking context
// = 1 : stacking context without z-index
// > 1 : z-index
$child_style = $child->get_style();
$child_z_index = $child_style->z_index;
$z_index = 0;
if ($child_z_index !== "auto") {
$z_index = intval($child_z_index) + 1;
} elseif ($child_style->float !== "none" || $child->is_positionned()) {
$z_index = 1;
}
$stack[$z_index][] = $child;
}
ksort($stack);
foreach ($stack as $by_index) {
foreach ($by_index as $child) {
$this->render($child);
}
}
// Ends the overflow: hidden box
if ($style->overflow === "hidden") {
$this->_canvas->clipping_end();
}
if ($style->transform && is_array($style->transform)) {
$this->_canvas->restore();
}
// Check for end frame callback
$this->_check_callbacks("end_frame", $frame);
}
/**
* Check for callbacks that need to be performed when a given event
* gets triggered on a frame
*
* @param string $event the type of event
* @param Frame $frame the frame that event is triggered on
*/
protected function _check_callbacks($event, $frame)
{
if (!isset($this->_callbacks)) {
$this->_callbacks = $this->_dompdf->getCallbacks();
}
if (is_array($this->_callbacks) && isset($this->_callbacks[$event])) {
$info = array(0 => $this->_canvas, "canvas" => $this->_canvas,
1 => $frame, "frame" => $frame);
$fs = $this->_callbacks[$event];
foreach ($fs as $f) {
if (is_callable($f)) {
if (is_array($f)) {
$f[0]->{$f[1]}($info);
} else {
$f($info);
}
}
}
}
}
/**
* Render a single frame
*
* Creates Renderer objects on demand
*
* @param string $type type of renderer to use
* @param Frame $frame the frame to render
*/
protected function _render_frame($type, $frame)
{
if (!isset($this->_renderers[$type])) {
switch ($type) {
case "block":
$this->_renderers[$type] = new Block($this->_dompdf);
break;
case "inline":
$this->_renderers[$type] = new Renderer\Inline($this->_dompdf);
break;
case "text":
$this->_renderers[$type] = new Text($this->_dompdf);
break;
case "image":
$this->_renderers[$type] = new Image($this->_dompdf);
break;
case "table-cell":
$this->_renderers[$type] = new TableCell($this->_dompdf);
break;
case "table-row-group":
$this->_renderers[$type] = new TableRowGroup($this->_dompdf);
break;
case "list-bullet":
$this->_renderers[$type] = new ListBullet($this->_dompdf);
break;
case "php":
$this->_renderers[$type] = new PhpEvaluator($this->_canvas);
break;
case "javascript":
$this->_renderers[$type] = new JavascriptEmbedder($this->_dompdf);
break;
}
}
$this->_renderers[$type]->render($frame);
}
}