new appraoch

This commit is contained in:
Tim Bendt
2025-11-26 13:22:58 -05:00
parent de3d100844
commit c520b7df89
6760 changed files with 1009780 additions and 0 deletions

View File

@@ -0,0 +1,944 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
// FIXME: Need to sanity check inputs to this class
namespace Dompdf\Adapter;
use Dompdf\Canvas;
use Dompdf\Dompdf;
use Dompdf\Exception;
use Dompdf\FontMetrics;
use Dompdf\Helpers;
use Dompdf\Image\Cache;
use FontLib\Exception\FontNotFoundException;
/**
* PDF rendering interface
*
* Dompdf\Adapter\CPDF provides a simple stateless interface to the stateful one
* provided by the Cpdf class.
*
* Unless otherwise mentioned, all dimensions are in points (1/72 in). The
* coordinate origin is in the top left corner, and y values increase
* downwards.
*
* See {@link http://www.ros.co.nz/pdf/} for more complete documentation
* on the underlying {@link Cpdf} class.
*
* @package dompdf
*/
class CPDF implements Canvas
{
/**
* Dimensions of paper sizes in points
*
* @var array
*/
static $PAPER_SIZES = [
"4a0" => [0.0, 0.0, 4767.87, 6740.79],
"2a0" => [0.0, 0.0, 3370.39, 4767.87],
"a0" => [0.0, 0.0, 2383.94, 3370.39],
"a1" => [0.0, 0.0, 1683.78, 2383.94],
"a2" => [0.0, 0.0, 1190.55, 1683.78],
"a3" => [0.0, 0.0, 841.89, 1190.55],
"a4" => [0.0, 0.0, 595.28, 841.89],
"a5" => [0.0, 0.0, 419.53, 595.28],
"a6" => [0.0, 0.0, 297.64, 419.53],
"a7" => [0.0, 0.0, 209.76, 297.64],
"a8" => [0.0, 0.0, 147.40, 209.76],
"a9" => [0.0, 0.0, 104.88, 147.40],
"a10" => [0.0, 0.0, 73.70, 104.88],
"b0" => [0.0, 0.0, 2834.65, 4008.19],
"b1" => [0.0, 0.0, 2004.09, 2834.65],
"b2" => [0.0, 0.0, 1417.32, 2004.09],
"b3" => [0.0, 0.0, 1000.63, 1417.32],
"b4" => [0.0, 0.0, 708.66, 1000.63],
"b5" => [0.0, 0.0, 498.90, 708.66],
"b6" => [0.0, 0.0, 354.33, 498.90],
"b7" => [0.0, 0.0, 249.45, 354.33],
"b8" => [0.0, 0.0, 175.75, 249.45],
"b9" => [0.0, 0.0, 124.72, 175.75],
"b10" => [0.0, 0.0, 87.87, 124.72],
"c0" => [0.0, 0.0, 2599.37, 3676.54],
"c1" => [0.0, 0.0, 1836.85, 2599.37],
"c2" => [0.0, 0.0, 1298.27, 1836.85],
"c3" => [0.0, 0.0, 918.43, 1298.27],
"c4" => [0.0, 0.0, 649.13, 918.43],
"c5" => [0.0, 0.0, 459.21, 649.13],
"c6" => [0.0, 0.0, 323.15, 459.21],
"c7" => [0.0, 0.0, 229.61, 323.15],
"c8" => [0.0, 0.0, 161.57, 229.61],
"c9" => [0.0, 0.0, 113.39, 161.57],
"c10" => [0.0, 0.0, 79.37, 113.39],
"ra0" => [0.0, 0.0, 2437.80, 3458.27],
"ra1" => [0.0, 0.0, 1729.13, 2437.80],
"ra2" => [0.0, 0.0, 1218.90, 1729.13],
"ra3" => [0.0, 0.0, 864.57, 1218.90],
"ra4" => [0.0, 0.0, 609.45, 864.57],
"sra0" => [0.0, 0.0, 2551.18, 3628.35],
"sra1" => [0.0, 0.0, 1814.17, 2551.18],
"sra2" => [0.0, 0.0, 1275.59, 1814.17],
"sra3" => [0.0, 0.0, 907.09, 1275.59],
"sra4" => [0.0, 0.0, 637.80, 907.09],
"letter" => [0.0, 0.0, 612.00, 792.00],
"half-letter" => [0.0, 0.0, 396.00, 612.00],
"legal" => [0.0, 0.0, 612.00, 1008.00],
"ledger" => [0.0, 0.0, 1224.00, 792.00],
"tabloid" => [0.0, 0.0, 792.00, 1224.00],
"executive" => [0.0, 0.0, 521.86, 756.00],
"folio" => [0.0, 0.0, 612.00, 936.00],
"commercial #10 envelope" => [0.0, 0.0, 684.00, 297.00],
"catalog #10 1/2 envelope" => [0.0, 0.0, 648.00, 864.00],
"8.5x11" => [0.0, 0.0, 612.00, 792.00],
"8.5x14" => [0.0, 0.0, 612.00, 1008.00],
"11x17" => [0.0, 0.0, 792.00, 1224.00],
];
/**
* The Dompdf object
*
* @var Dompdf
*/
protected $_dompdf;
/**
* Instance of Cpdf class
*
* @var \Dompdf\Cpdf
*/
protected $_pdf;
/**
* PDF width, in points
*
* @var float
*/
protected $_width;
/**
* PDF height, in points
*
* @var float
*/
protected $_height;
/**
* Current page number
*
* @var int
*/
protected $_page_number;
/**
* Total number of pages
*
* @var int
*/
protected $_page_count;
/**
* Array of pages for accessing after rendering is initially complete
*
* @var array
*/
protected $_pages;
/**
* Currently-applied opacity level (0 - 1)
*
* @var float
*/
protected $_current_opacity = 1;
public function __construct($paper = "letter", string $orientation = "portrait", ?Dompdf $dompdf = null)
{
if (is_array($paper)) {
$size = array_map("floatval", $paper);
} else {
$paper = strtolower($paper);
$size = self::$PAPER_SIZES[$paper] ?? self::$PAPER_SIZES["letter"];
}
if (strtolower($orientation) === "landscape") {
[$size[2], $size[3]] = [$size[3], $size[2]];
}
if ($dompdf === null) {
$this->_dompdf = new Dompdf();
} else {
$this->_dompdf = $dompdf;
}
$this->_pdf = new \Dompdf\Cpdf(
$size,
true,
$this->_dompdf->getOptions()->getFontCache(),
$this->_dompdf->getOptions()->getTempDir()
);
$this->_pdf->addInfo("Producer", sprintf("%s + CPDF", $this->_dompdf->version));
$time = substr_replace(date('YmdHisO'), '\'', -2, 0) . '\'';
$this->_pdf->addInfo("CreationDate", "D:$time");
$this->_pdf->addInfo("ModDate", "D:$time");
$this->_width = $size[2] - $size[0];
$this->_height = $size[3] - $size[1];
$this->_page_number = $this->_page_count = 1;
$this->_pages = [$this->_pdf->getFirstPageId()];
}
public function get_dompdf()
{
return $this->_dompdf;
}
/**
* Returns the Cpdf instance
*
* @return \Dompdf\Cpdf
*/
public function get_cpdf()
{
return $this->_pdf;
}
public function add_info(string $label, string $value): void
{
$this->_pdf->addInfo($label, $value);
}
/**
* Opens a new 'object'
*
* While an object is open, all drawing actions are recorded in the object,
* as opposed to being drawn on the current page. Objects can be added
* later to a specific page or to several pages.
*
* The return value is an integer ID for the new object.
*
* @see CPDF::close_object()
* @see CPDF::add_object()
*
* @return int
*/
public function open_object()
{
$ret = $this->_pdf->openObject();
$this->_pdf->saveState();
return $ret;
}
/**
* Reopens an existing 'object'
*
* @see CPDF::open_object()
* @param int $object the ID of a previously opened object
*/
public function reopen_object($object)
{
$this->_pdf->reopenObject($object);
$this->_pdf->saveState();
}
/**
* Closes the current 'object'
*
* @see CPDF::open_object()
*/
public function close_object()
{
$this->_pdf->restoreState();
$this->_pdf->closeObject();
}
/**
* Adds a specified 'object' to the document
*
* $object int specifying an object created with {@link
* CPDF::open_object()}. $where can be one of:
* - 'add' add to current page only
* - 'all' add to every page from the current one onwards
* - 'odd' add to all odd numbered pages from now on
* - 'even' add to all even numbered pages from now on
* - 'next' add the object to the next page only
* - 'nextodd' add to all odd numbered pages from the next one
* - 'nexteven' add to all even numbered pages from the next one
*
* @see Cpdf::addObject()
*
* @param int $object
* @param string $where
*/
public function add_object($object, $where = 'all')
{
$this->_pdf->addObject($object, $where);
}
/**
* Stops the specified 'object' from appearing in the document.
*
* The object will stop being displayed on the page following the current
* one.
*
* @param int $object
*/
public function stop_object($object)
{
$this->_pdf->stopObject($object);
}
/**
* Serialize the pdf object's current state for retrieval later
*/
public function serialize_object($id)
{
return $this->_pdf->serializeObject($id);
}
public function reopen_serialized_object($obj)
{
return $this->_pdf->restoreSerializedObject($obj);
}
//........................................................................
public function get_width()
{
return $this->_width;
}
public function get_height()
{
return $this->_height;
}
public function get_page_number()
{
return $this->_page_number;
}
public function get_page_count()
{
return $this->_page_count;
}
/**
* Sets the current page number
*
* @param int $num
*/
public function set_page_number($num)
{
$this->_page_number = $num;
}
public function set_page_count($count)
{
$this->_page_count = $count;
}
/**
* Sets the stroke color
*
* See {@link Style::set_color()} for the format of the color array.
*
* @param array $color
*/
protected function _set_stroke_color($color)
{
$this->_pdf->setStrokeColor($color);
$alpha = isset($color["alpha"]) ? $color["alpha"] : 1;
$alpha *= $this->_current_opacity;
$this->_set_line_transparency("Normal", $alpha);
}
/**
* Sets the fill colour
*
* See {@link Style::set_color()} for the format of the colour array.
*
* @param array $color
*/
protected function _set_fill_color($color)
{
$this->_pdf->setColor($color);
$alpha = isset($color["alpha"]) ? $color["alpha"] : 1;
$alpha *= $this->_current_opacity;
$this->_set_fill_transparency("Normal", $alpha);
}
/**
* Sets line transparency
* @see Cpdf::setLineTransparency()
*
* Valid blend modes are (case-sensitive):
*
* Normal, Multiply, Screen, Overlay, Darken, Lighten,
* ColorDodge, ColorBurn, HardLight, SoftLight, Difference,
* Exclusion
*
* @param string $mode the blending mode to use
* @param float $opacity 0.0 fully transparent, 1.0 fully opaque
*/
protected function _set_line_transparency($mode, $opacity)
{
$this->_pdf->setLineTransparency($mode, $opacity);
}
/**
* Sets fill transparency
* @see Cpdf::setFillTransparency()
*
* Valid blend modes are (case-sensitive):
*
* Normal, Multiply, Screen, Overlay, Darken, Lighten,
* ColorDogde, ColorBurn, HardLight, SoftLight, Difference,
* Exclusion
*
* @param string $mode the blending mode to use
* @param float $opacity 0.0 fully transparent, 1.0 fully opaque
*/
protected function _set_fill_transparency($mode, $opacity)
{
$this->_pdf->setFillTransparency($mode, $opacity);
}
/**
* Sets the line style
*
* @see Cpdf::setLineStyle()
*
* @param float $width
* @param string $cap
* @param string $join
* @param array $dash
*/
protected function _set_line_style($width, $cap, $join, $dash)
{
$this->_pdf->setLineStyle($width, $cap, $join, $dash);
}
public function set_opacity(float $opacity, string $mode = "Normal"): void
{
$this->_set_line_transparency($mode, $opacity);
$this->_set_fill_transparency($mode, $opacity);
$this->_current_opacity = $opacity;
}
public function set_default_view($view, $options = [])
{
array_unshift($options, $view);
call_user_func_array([$this->_pdf, "openHere"], $options);
}
/**
* Remaps y coords from 4th to 1st quadrant
*
* @param float $y
* @return float
*/
protected function y($y)
{
return $this->_height - $y;
}
public function line($x1, $y1, $x2, $y2, $color, $width, $style = [], $cap = "butt")
{
$this->_set_stroke_color($color);
$this->_set_line_style($width, $cap, "", $style);
$this->_pdf->line($x1, $this->y($y1),
$x2, $this->y($y2));
$this->_set_line_transparency("Normal", $this->_current_opacity);
}
public function arc($x, $y, $r1, $r2, $astart, $aend, $color, $width, $style = [], $cap = "butt")
{
$this->_set_stroke_color($color);
$this->_set_line_style($width, $cap, "", $style);
$this->_pdf->ellipse($x, $this->y($y), $r1, $r2, 0, 8, $astart, $aend, false, false, true, false);
$this->_set_line_transparency("Normal", $this->_current_opacity);
}
public function rectangle($x1, $y1, $w, $h, $color, $width, $style = [], $cap = "butt")
{
$this->_set_stroke_color($color);
$this->_set_line_style($width, $cap, "", $style);
$this->_pdf->rectangle($x1, $this->y($y1) - $h, $w, $h);
$this->_set_line_transparency("Normal", $this->_current_opacity);
}
public function filled_rectangle($x1, $y1, $w, $h, $color)
{
$this->_set_fill_color($color);
$this->_pdf->filledRectangle($x1, $this->y($y1) - $h, $w, $h);
$this->_set_fill_transparency("Normal", $this->_current_opacity);
}
public function clipping_rectangle($x1, $y1, $w, $h)
{
$this->_pdf->clippingRectangle($x1, $this->y($y1) - $h, $w, $h);
}
public function clipping_roundrectangle($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL)
{
$this->_pdf->clippingRectangleRounded($x1, $this->y($y1) - $h, $w, $h, $rTL, $rTR, $rBR, $rBL);
}
public function clipping_polygon(array $points): void
{
// Adjust y values
for ($i = 1; $i < count($points); $i += 2) {
$points[$i] = $this->y($points[$i]);
}
$this->_pdf->clippingPolygon($points);
}
public function clipping_end()
{
$this->_pdf->clippingEnd();
}
public function save()
{
$this->_pdf->saveState();
}
public function restore()
{
$this->_pdf->restoreState();
}
public function rotate($angle, $x, $y)
{
$this->_pdf->rotate($angle, $x, $y);
}
public function skew($angle_x, $angle_y, $x, $y)
{
$this->_pdf->skew($angle_x, $angle_y, $x, $y);
}
public function scale($s_x, $s_y, $x, $y)
{
$this->_pdf->scale($s_x, $s_y, $x, $y);
}
public function translate($t_x, $t_y)
{
$this->_pdf->translate($t_x, $t_y);
}
public function transform($a, $b, $c, $d, $e, $f)
{
$this->_pdf->transform([$a, $b, $c, $d, $e, $f]);
}
public function polygon($points, $color, $width = null, $style = [], $fill = false)
{
$this->_set_fill_color($color);
$this->_set_stroke_color($color);
if (!$fill && isset($width)) {
$this->_set_line_style($width, "square", "miter", $style);
}
// Adjust y values
for ($i = 1; $i < count($points); $i += 2) {
$points[$i] = $this->y($points[$i]);
}
$this->_pdf->polygon($points, $fill);
$this->_set_fill_transparency("Normal", $this->_current_opacity);
$this->_set_line_transparency("Normal", $this->_current_opacity);
}
public function circle($x, $y, $r, $color, $width = null, $style = [], $fill = false)
{
$this->_set_fill_color($color);
$this->_set_stroke_color($color);
if (!$fill && isset($width)) {
$this->_set_line_style($width, "round", "round", $style);
}
$this->_pdf->ellipse($x, $this->y($y), $r, 0, 0, 8, 0, 360, 1, $fill);
$this->_set_fill_transparency("Normal", $this->_current_opacity);
$this->_set_line_transparency("Normal", $this->_current_opacity);
}
/**
* Convert image to a PNG image
*
* @param string $image_url
* @param string $type
*
* @return string|null The url of the newly converted image
*/
protected function _convert_to_png($image_url, $type)
{
$filename = Cache::getTempImage($image_url);
if ($filename !== null && file_exists($filename)) {
return $filename;
}
$func_name = "imagecreatefrom$type";
set_error_handler([Helpers::class, "record_warnings"]);
if (!function_exists($func_name)) {
if (!method_exists(Helpers::class, $func_name)) {
throw new Exception("Function $func_name() not found. Cannot convert $type image: $image_url. Please install the image PHP extension.");
}
$func_name = [Helpers::class, $func_name];
}
try {
$im = call_user_func($func_name, $image_url);
if ($im) {
imageinterlace($im, false);
$tmp_dir = $this->_dompdf->getOptions()->getTempDir();
$tmp_name = @tempnam($tmp_dir, "{$type}_dompdf_img_");
@unlink($tmp_name);
$filename = "$tmp_name.png";
imagepng($im, $filename);
imagedestroy($im);
} else {
$filename = null;
}
} finally {
restore_error_handler();
}
if ($filename !== null) {
Cache::addTempImage($image_url, $filename);
}
return $filename;
}
public function image($img, $x, $y, $w, $h, $resolution = "normal")
{
[$width, $height, $type] = Helpers::dompdf_getimagesize($img, $this->get_dompdf()->getHttpContext());
$debug_png = $this->_dompdf->getOptions()->getDebugPng();
if ($debug_png) {
print "[image:$img|$width|$height|$type]";
}
switch ($type) {
case "jpeg":
if ($debug_png) {
print '!!!jpg!!!';
}
$this->_pdf->addJpegFromFile($img, $x, $this->y($y) - $h, $w, $h);
break;
case "webp":
/** @noinspection PhpMissingBreakStatementInspection */
case "gif":
/** @noinspection PhpMissingBreakStatementInspection */
case "bmp":
if ($debug_png) print "!!!{$type}!!!";
$img = $this->_convert_to_png($img, $type);
if ($img === null) {
if ($debug_png) print '!!!conversion to PDF failed!!!';
$this->image(Cache::$broken_image, $x, $y, $w, $h, $resolution);
break;
}
case "png":
if ($debug_png) print '!!!png!!!';
$this->_pdf->addPngFromFile($img, $x, $this->y($y) - $h, $w, $h);
break;
case "svg":
if ($debug_png) print '!!!SVG!!!';
$this->_pdf->addSvgFromFile($img, $x, $this->y($y) - $h, $w, $h);
break;
default:
if ($debug_png) print '!!!unknown!!!';
}
}
public function select($x, $y, $w, $h, $font, $size, $color = [0, 0, 0], $opts = [])
{
$pdf = $this->_pdf;
$font .= ".afm";
$pdf->selectFont($font);
if (!isset($pdf->acroFormId)) {
$pdf->addForm();
}
$ft = \Dompdf\Cpdf::ACROFORM_FIELD_CHOICE;
$ff = \Dompdf\Cpdf::ACROFORM_FIELD_CHOICE_COMBO;
$id = $pdf->addFormField($ft, rand(), $x, $this->y($y) - $h, $x + $w, $this->y($y), $ff, $size, $color);
$pdf->setFormFieldOpt($id, $opts);
}
public function textarea($x, $y, $w, $h, $font, $size, $color = [0, 0, 0])
{
$pdf = $this->_pdf;
$font .= ".afm";
$pdf->selectFont($font);
if (!isset($pdf->acroFormId)) {
$pdf->addForm();
}
$ft = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT;
$ff = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT_MULTILINE;
$pdf->addFormField($ft, rand(), $x, $this->y($y) - $h, $x + $w, $this->y($y), $ff, $size, $color);
}
public function input($x, $y, $w, $h, $type, $font, $size, $color = [0, 0, 0])
{
$pdf = $this->_pdf;
$font .= ".afm";
$pdf->selectFont($font);
if (!isset($pdf->acroFormId)) {
$pdf->addForm();
}
$ft = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT;
$ff = 0;
switch ($type) {
case 'text':
$ft = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT;
break;
case 'password':
$ft = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT;
$ff = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT_PASSWORD;
break;
case 'submit':
$ft = \Dompdf\Cpdf::ACROFORM_FIELD_BUTTON;
break;
}
$pdf->addFormField($ft, rand(), $x, $this->y($y) - $h, $x + $w, $this->y($y), $ff, $size, $color);
}
public function text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0)
{
$pdf = $this->_pdf;
$this->_set_fill_color($color);
$is_font_subsetting = $this->_dompdf->getOptions()->getIsFontSubsettingEnabled();
$pdf->selectFont($font . '.afm', '', true, $is_font_subsetting);
$pdf->addText($x, $this->y($y) - $pdf->getFontHeight($size), $size, $text, $angle, $word_space, $char_space);
$this->_set_fill_transparency("Normal", $this->_current_opacity);
}
public function javascript($code)
{
$this->_pdf->addJavascript($code);
}
//........................................................................
public function add_named_dest($anchorname)
{
$this->_pdf->addDestination($anchorname, "Fit");
}
public function add_link($url, $x, $y, $width, $height)
{
$y = $this->y($y) - $height;
if (strpos($url, '#') === 0) {
// Local link
$name = substr($url, 1);
if ($name) {
$this->_pdf->addInternalLink($name, $x, $y, $x + $width, $y + $height);
}
} else {
$this->_pdf->addLink($url, $x, $y, $x + $width, $y + $height);
}
}
/**
* @throws FontNotFoundException
*/
public function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0)
{
$this->_pdf->selectFont($font, '', true, $this->_dompdf->getOptions()->getIsFontSubsettingEnabled());
return $this->_pdf->getTextWidth($size, $text, $word_spacing, $char_spacing);
}
/**
* @throws FontNotFoundException
*/
public function get_font_height($font, $size)
{
$options = $this->_dompdf->getOptions();
$this->_pdf->selectFont($font, '', true, $options->getIsFontSubsettingEnabled());
return $this->_pdf->getFontHeight($size) * $options->getFontHeightRatio();
}
/*function get_font_x_height($font, $size) {
$this->_pdf->selectFont($font);
$ratio = $this->_dompdf->getOptions()->getFontHeightRatio();
return $this->_pdf->getFontXHeight($size) * $ratio;
}*/
/**
* @throws FontNotFoundException
*/
public function get_font_baseline($font, $size)
{
$ratio = $this->_dompdf->getOptions()->getFontHeightRatio();
return $this->get_font_height($font, $size) / $ratio;
}
/**
* Processes a callback or script on every page.
*
* The callback function receives the four parameters `int $pageNumber`,
* `int $pageCount`, `Canvas $canvas`, and `FontMetrics $fontMetrics`, in
* that order. If a script is passed as string, the variables `$PAGE_NUM`,
* `$PAGE_COUNT`, `$pdf`, and `$fontMetrics` are available instead. Passing
* a script as string is deprecated and will be removed in a future version.
*
* This function can be used to add page numbers to all pages after the
* first one, for example.
*
* @param callable|string $callback The callback function or PHP script to process on every page
*/
public function page_script($callback): void
{
if (is_string($callback)) {
$this->processPageScript(function (
int $PAGE_NUM,
int $PAGE_COUNT,
self $pdf,
FontMetrics $fontMetrics
) use ($callback) {
eval($callback);
});
return;
}
$this->processPageScript($callback);
}
public function page_text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0)
{
$this->processPageScript(function (int $pageNumber, int $pageCount) use ($x, $y, $text, $font, $size, $color, $word_space, $char_space, $angle) {
$text = str_replace(
["{PAGE_NUM}", "{PAGE_COUNT}"],
[$pageNumber, $pageCount],
$text
);
$this->text($x, $y, $text, $font, $size, $color, $word_space, $char_space, $angle);
});
}
public function page_line($x1, $y1, $x2, $y2, $color, $width, $style = [])
{
$this->processPageScript(function () use ($x1, $y1, $x2, $y2, $color, $width, $style) {
$this->line($x1, $y1, $x2, $y2, $color, $width, $style);
});
}
/**
* @return int
*/
public function new_page()
{
$this->_page_number++;
$this->_page_count++;
$ret = $this->_pdf->newPage();
$this->_pages[] = $ret;
return $ret;
}
protected function processPageScript(callable $callback): void
{
$pageNumber = 1;
foreach ($this->_pages as $pid) {
$this->reopen_object($pid);
$fontMetrics = $this->_dompdf->getFontMetrics();
$callback($pageNumber, $this->_page_count, $this, $fontMetrics);
$this->close_object();
$pageNumber++;
}
}
public function stream($filename = "document.pdf", $options = [])
{
if (headers_sent()) {
die("Unable to stream pdf: headers already sent");
}
if (!isset($options["compress"])) $options["compress"] = true;
if (!isset($options["Attachment"])) $options["Attachment"] = true;
$debug = !$options['compress'];
$tmp = ltrim($this->_pdf->output($debug));
header("Cache-Control: private");
header("Content-Type: application/pdf");
header("Content-Length: " . mb_strlen($tmp, "8bit"));
$filename = str_replace(["\n", "'"], "", basename($filename, ".pdf")) . ".pdf";
$attachment = $options["Attachment"] ? "attachment" : "inline";
header(Helpers::buildContentDispositionHeader($attachment, $filename));
echo $tmp;
flush();
}
public function output($options = [])
{
if (!isset($options["compress"])) $options["compress"] = true;
$debug = !$options['compress'];
return $this->_pdf->output($debug);
}
/**
* Returns logging messages generated by the Cpdf class
*
* @return string
*/
public function get_messages()
{
return $this->_pdf->messages;
}
}

View File

@@ -0,0 +1,929 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Adapter;
use Dompdf\Canvas;
use Dompdf\Dompdf;
use Dompdf\Helpers;
use Dompdf\Image\Cache;
/**
* Image rendering interface
*
* Renders to an image format supported by GD (jpeg, gif, png, xpm).
* Not super-useful day-to-day but handy nonetheless
*
* @package dompdf
*/
class GD implements Canvas
{
/**
* @var Dompdf
*/
protected $_dompdf;
/**
* Resource handle for the image
*
* @var \GdImage|resource
*/
protected $_img;
/**
* Resource handle for the image
*
* @var \GdImage[]|resource[]
*/
protected $_imgs;
/**
* Apparent canvas width in pixels
*
* @var int
*/
protected $_width;
/**
* Apparent canvas height in pixels
*
* @var int
*/
protected $_height;
/**
* Actual image width in pixels
*
* @var int
*/
protected $_actual_width;
/**
* Actual image height in pixels
*
* @var int
*/
protected $_actual_height;
/**
* Current page number
*
* @var int
*/
protected $_page_number;
/**
* Total number of pages
*
* @var int
*/
protected $_page_count;
/**
* Image antialias factor
*
* @var float
*/
protected $_aa_factor;
/**
* Allocated colors
*
* @var array
*/
protected $_colors;
/**
* Background color
*
* @var int
*/
protected $_bg_color;
/**
* Background color array
*
* @var array
*/
protected $_bg_color_array;
/**
* Actual DPI
*
* @var int
*/
protected $dpi;
/**
* Amount to scale font sizes
*
* Font sizes are 72 DPI, GD internally uses 96. Scale them proportionally.
* 72 / 96 = 0.75.
*
* @var float
*/
const FONT_SCALE = 0.75;
/**
* @param string|float[] $paper The paper size to use as either a standard paper size (see {@link CPDF::$PAPER_SIZES}) or
* an array of the form `[x1, y1, x2, y2]` (typically `[0, 0, width, height]`).
* @param string $orientation The paper orientation, either `portrait` or `landscape`.
* @param Dompdf|null $dompdf The Dompdf instance.
* @param float $aa_factor Anti-aliasing factor, 1 for no AA
* @param array $bg_color Image background color: array(r,g,b,a), 0 <= r,g,b,a <= 1
*/
public function __construct($paper = "letter", string $orientation = "portrait", ?Dompdf $dompdf = null, float $aa_factor = 1.0, array $bg_color = [1, 1, 1, 0])
{
if (is_array($paper)) {
$size = array_map("floatval", $paper);
} else {
$paper = strtolower($paper);
$size = CPDF::$PAPER_SIZES[$paper] ?? CPDF::$PAPER_SIZES["letter"];
}
if (strtolower($orientation) === "landscape") {
[$size[2], $size[3]] = [$size[3], $size[2]];
}
if ($dompdf === null) {
$this->_dompdf = new Dompdf();
} else {
$this->_dompdf = $dompdf;
}
$this->dpi = $this->get_dompdf()->getOptions()->getDpi();
if ($aa_factor < 1) {
$aa_factor = 1;
}
$this->_aa_factor = $aa_factor;
$size[2] *= $aa_factor;
$size[3] *= $aa_factor;
$this->_width = $size[2] - $size[0];
$this->_height = $size[3] - $size[1];
$this->_actual_width = $this->_upscale($this->_width);
$this->_actual_height = $this->_upscale($this->_height);
$this->_page_number = $this->_page_count = 0;
if (is_null($bg_color) || !is_array($bg_color)) {
// Pure white bg
$bg_color = [1, 1, 1, 0];
}
$this->_bg_color_array = $bg_color;
$this->new_page();
}
public function get_dompdf()
{
return $this->_dompdf;
}
/**
* Return the GD image resource
*
* @return \GdImage|resource
*/
public function get_image()
{
return $this->_img;
}
/**
* Return the image's width in pixels
*
* @return int
*/
public function get_width()
{
return round($this->_width / $this->_aa_factor);
}
/**
* Return the image's height in pixels
*
* @return int
*/
public function get_height()
{
return round($this->_height / $this->_aa_factor);
}
public function get_page_number()
{
return $this->_page_number;
}
public function get_page_count()
{
return $this->_page_count;
}
/**
* Sets the current page number
*
* @param int $num
*/
public function set_page_number($num)
{
$this->_page_number = $num;
}
public function set_page_count($count)
{
$this->_page_count = $count;
}
public function set_opacity(float $opacity, string $mode = "Normal"): void
{
// FIXME
}
/**
* Allocate a new color. Allocate with GD as needed and store
* previously allocated colors in $this->_colors.
*
* @param array $color The new current color
* @return int The allocated color
*/
protected function _allocate_color($color)
{
$a = isset($color["alpha"]) ? $color["alpha"] : 1;
if (isset($color["c"])) {
$color = Helpers::cmyk_to_rgb($color);
}
list($r, $g, $b) = $color;
$r = round($r * 255);
$g = round($g * 255);
$b = round($b * 255);
$a = round(127 - ($a * 127));
// Clip values
$r = $r > 255 ? 255 : $r;
$g = $g > 255 ? 255 : $g;
$b = $b > 255 ? 255 : $b;
$a = $a > 127 ? 127 : $a;
$r = $r < 0 ? 0 : $r;
$g = $g < 0 ? 0 : $g;
$b = $b < 0 ? 0 : $b;
$a = $a < 0 ? 0 : $a;
$key = sprintf("#%02X%02X%02X%02X", $r, $g, $b, $a);
if (isset($this->_colors[$key])) {
return $this->_colors[$key];
}
if ($a != 0) {
$this->_colors[$key] = imagecolorallocatealpha($this->get_image(), $r, $g, $b, $a);
} else {
$this->_colors[$key] = imagecolorallocate($this->get_image(), $r, $g, $b);
}
return $this->_colors[$key];
}
/**
* Scales value up to the current canvas DPI from 72 DPI
*
* @param float $length
* @return int
*/
protected function _upscale($length)
{
return round(($length * $this->dpi) / 72 * $this->_aa_factor);
}
/**
* Scales value down from the current canvas DPI to 72 DPI
*
* @param float $length
* @return float
*/
protected function _downscale($length)
{
return round(($length / $this->dpi * 72) / $this->_aa_factor);
}
protected function convertStyle(array $style, int $color, int $width): array
{
$gdStyle = [];
if (count($style) === 1) {
$style[] = $style[0];
}
foreach ($style as $index => $s) {
$d = $this->_upscale($s);
for ($i = 0; $i < $d; $i++) {
for ($j = 0; $j < $width; $j++) {
$gdStyle[] = $index % 2 === 0
? $color
: IMG_COLOR_TRANSPARENT;
}
}
}
return $gdStyle;
}
public function line($x1, $y1, $x2, $y2, $color, $width, $style = [], $cap = "butt")
{
// Account for the fact that round and square caps are expected to
// extend outwards
if ($cap === "round" || $cap === "square") {
// Shift line by half width
$w = $width / 2;
$a = $x2 - $x1;
$b = $y2 - $y1;
$c = sqrt($a ** 2 + $b ** 2);
$dx = $a * $w / $c;
$dy = $b * $w / $c;
$x1 -= $dx;
$x2 -= $dx;
$y1 -= $dy;
$y2 -= $dy;
// Adapt dash pattern
if (is_array($style)) {
foreach ($style as $index => &$s) {
$s = $index % 2 === 0 ? $s + $width : $s - $width;
}
}
}
// Scale by the AA factor and DPI
$x1 = $this->_upscale($x1);
$y1 = $this->_upscale($y1);
$x2 = $this->_upscale($x2);
$y2 = $this->_upscale($y2);
$width = $this->_upscale($width);
$c = $this->_allocate_color($color);
// Convert the style array if required
if (is_array($style) && count($style) > 0) {
$gd_style = $this->convertStyle($style, $c, $width);
if (!empty($gd_style)) {
imagesetstyle($this->get_image(), $gd_style);
$c = IMG_COLOR_STYLED;
}
}
imagesetthickness($this->get_image(), $width);
imageline($this->get_image(), $x1, $y1, $x2, $y2, $c);
}
public function arc($x, $y, $r1, $r2, $astart, $aend, $color, $width, $style = [], $cap = "butt")
{
// Account for the fact that round and square caps are expected to
// extend outwards
if ($cap === "round" || $cap === "square") {
// Adapt dash pattern
if (is_array($style)) {
foreach ($style as $index => &$s) {
$s = $index % 2 === 0 ? $s + $width : $s - $width;
}
}
}
// Scale by the AA factor and DPI
$x = $this->_upscale($x);
$y = $this->_upscale($y);
$w = $this->_upscale($r1 * 2);
$h = $this->_upscale($r2 * 2);
$width = $this->_upscale($width);
// Adapt angles as imagearc counts clockwise
$start = 360 - $aend;
$end = 360 - $astart;
$c = $this->_allocate_color($color);
// Convert the style array if required
if (is_array($style) && count($style) > 0) {
$gd_style = $this->convertStyle($style, $c, $width);
if (!empty($gd_style)) {
imagesetstyle($this->get_image(), $gd_style);
$c = IMG_COLOR_STYLED;
}
}
imagesetthickness($this->get_image(), $width);
imagearc($this->get_image(), $x, $y, $w, $h, $start, $end, $c);
}
public function rectangle($x1, $y1, $w, $h, $color, $width, $style = [], $cap = "butt")
{
// Account for the fact that round and square caps are expected to
// extend outwards
if ($cap === "round" || $cap === "square") {
// Adapt dash pattern
if (is_array($style)) {
foreach ($style as $index => &$s) {
$s = $index % 2 === 0 ? $s + $width : $s - $width;
}
}
}
// Scale by the AA factor and DPI
$x1 = $this->_upscale($x1);
$y1 = $this->_upscale($y1);
$w = $this->_upscale($w);
$h = $this->_upscale($h);
$width = $this->_upscale($width);
$c = $this->_allocate_color($color);
// Convert the style array if required
if (is_array($style) && count($style) > 0) {
$gd_style = $this->convertStyle($style, $c, $width);
if (!empty($gd_style)) {
imagesetstyle($this->get_image(), $gd_style);
$c = IMG_COLOR_STYLED;
}
}
imagesetthickness($this->get_image(), $width);
if ($c === IMG_COLOR_STYLED) {
imagepolygon($this->get_image(), [
$x1, $y1,
$x1 + $w, $y1,
$x1 + $w, $y1 + $h,
$x1, $y1 + $h
], $c);
} else {
imagerectangle($this->get_image(), $x1, $y1, $x1 + $w, $y1 + $h, $c);
}
}
public function filled_rectangle($x1, $y1, $w, $h, $color)
{
// Scale by the AA factor and DPI
$x1 = $this->_upscale($x1);
$y1 = $this->_upscale($y1);
$w = $this->_upscale($w);
$h = $this->_upscale($h);
$c = $this->_allocate_color($color);
imagefilledrectangle($this->get_image(), $x1, $y1, $x1 + $w, $y1 + $h, $c);
}
public function clipping_rectangle($x1, $y1, $w, $h)
{
// @todo
}
public function clipping_roundrectangle($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL)
{
// @todo
}
public function clipping_polygon(array $points): void
{
// @todo
}
public function clipping_end()
{
// @todo
}
public function save()
{
$this->get_dompdf()->getOptions()->setDpi(72);
}
public function restore()
{
$this->get_dompdf()->getOptions()->setDpi($this->dpi);
}
public function rotate($angle, $x, $y)
{
// @todo
}
public function skew($angle_x, $angle_y, $x, $y)
{
// @todo
}
public function scale($s_x, $s_y, $x, $y)
{
// @todo
}
public function translate($t_x, $t_y)
{
// @todo
}
public function transform($a, $b, $c, $d, $e, $f)
{
// @todo
}
public function polygon($points, $color, $width = null, $style = [], $fill = false)
{
// Scale each point by the AA factor and DPI
foreach (array_keys($points) as $i) {
$points[$i] = $this->_upscale($points[$i]);
}
$width = isset($width) ? $this->_upscale($width) : null;
$c = $this->_allocate_color($color);
// Convert the style array if required
if (is_array($style) && count($style) > 0 && isset($width) && !$fill) {
$gd_style = $this->convertStyle($style, $c, $width);
if (!empty($gd_style)) {
imagesetstyle($this->get_image(), $gd_style);
$c = IMG_COLOR_STYLED;
}
}
imagesetthickness($this->get_image(), isset($width) ? $width : 0);
if ($fill) {
imagefilledpolygon($this->get_image(), $points, $c);
} else {
imagepolygon($this->get_image(), $points, $c);
}
}
public function circle($x, $y, $r, $color, $width = null, $style = [], $fill = false)
{
// Scale by the AA factor and DPI
$x = $this->_upscale($x);
$y = $this->_upscale($y);
$d = $this->_upscale(2 * $r);
$width = isset($width) ? $this->_upscale($width) : null;
$c = $this->_allocate_color($color);
// Convert the style array if required
if (is_array($style) && count($style) > 0 && isset($width) && !$fill) {
$gd_style = $this->convertStyle($style, $c, $width);
if (!empty($gd_style)) {
imagesetstyle($this->get_image(), $gd_style);
$c = IMG_COLOR_STYLED;
}
}
imagesetthickness($this->get_image(), isset($width) ? $width : 0);
if ($fill) {
imagefilledellipse($this->get_image(), $x, $y, $d, $d, $c);
} else {
imageellipse($this->get_image(), $x, $y, $d, $d, $c);
}
}
/**
* @throws \Exception
*/
public function image($img, $x, $y, $w, $h, $resolution = "normal")
{
$img_type = Cache::detect_type($img, $this->get_dompdf()->getHttpContext());
if (!$img_type) {
return;
}
$func_name = "imagecreatefrom$img_type";
if (!function_exists($func_name)) {
if (!method_exists(Helpers::class, $func_name)) {
throw new \Exception("Function $func_name() not found. Cannot convert $img_type image: $img. Please install the image PHP extension.");
}
$func_name = [Helpers::class, $func_name];
}
$src = @call_user_func($func_name, $img);
if (!$src) {
return; // Probably should add to $_dompdf_errors or whatever here
}
// Scale by the AA factor and DPI
$x = $this->_upscale($x);
$y = $this->_upscale($y);
$w = $this->_upscale($w);
$h = $this->_upscale($h);
$img_w = imagesx($src);
$img_h = imagesy($src);
imagecopyresampled($this->get_image(), $src, $x, $y, 0, 0, $w, $h, $img_w, $img_h);
}
public function text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_spacing = 0.0, $char_spacing = 0.0, $angle = 0.0)
{
// Scale by the AA factor and DPI
$x = $this->_upscale($x);
$y = $this->_upscale($y);
$size = $this->_upscale($size) * self::FONT_SCALE;
$h = round($this->get_font_height_actual($font, $size));
$c = $this->_allocate_color($color);
// imagettftext() converts numeric entities to their respective
// character. Preserve any originally double encoded entities to be
// represented as is.
// eg: &amp;#160; will render &#160; rather than its character.
$text = preg_replace('/&(#(?:x[a-fA-F0-9]+|[0-9]+);)/', '&#38;\1', $text);
$text = mb_encode_numericentity($text, [0x0080, 0xff, 0, 0xff], 'UTF-8');
$font = $this->get_ttf_file($font);
// FIXME: word spacing
imagettftext($this->get_image(), $size, $angle, $x, $y + $h, $c, $font, $text);
}
public function javascript($code)
{
// Not implemented
}
public function add_named_dest($anchorname)
{
// Not implemented
}
public function add_link($url, $x, $y, $width, $height)
{
// Not implemented
}
public function add_info(string $label, string $value): void
{
// N/A
}
public function set_default_view($view, $options = [])
{
// N/A
}
public function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0)
{
$font = $this->get_ttf_file($font);
$size = $this->_upscale($size) * self::FONT_SCALE;
// imagettfbbox() converts numeric entities to their respective
// character. Preserve any originally double encoded entities to be
// represented as is.
// eg: &amp;#160; will render &#160; rather than its character.
$text = preg_replace('/&(#(?:x[a-fA-F0-9]+|[0-9]+);)/', '&#38;\1', $text);
$text = mb_encode_numericentity($text, [0x0080, 0xffff, 0, 0xffff], 'UTF-8');
// FIXME: word spacing
list($x1, , $x2) = imagettfbbox($size, 0, $font, $text);
// Add additional 1pt to prevent text overflow issues
return $this->_downscale($x2 - $x1) + 1;
}
/**
* @param string|null $font
* @return string
*/
public function get_ttf_file($font)
{
if ($font === null) {
$font = "";
}
if ( stripos($font, ".ttf") === false ) {
$font .= ".ttf";
}
if (!file_exists($font)) {
$font_metrics = $this->_dompdf->getFontMetrics();
$font = $font_metrics->getFont($this->_dompdf->getOptions()->getDefaultFont()) . ".ttf";
if (!file_exists($font)) {
if (strpos($font, "mono")) {
$font = $font_metrics->getFont("DejaVu Mono") . ".ttf";
} elseif (strpos($font, "sans") !== false) {
$font = $font_metrics->getFont("DejaVu Sans") . ".ttf";
} elseif (strpos($font, "serif")) {
$font = $font_metrics->getFont("DejaVu Serif") . ".ttf";
} else {
$font = $font_metrics->getFont("DejaVu Sans") . ".ttf";
}
}
}
return $font;
}
public function get_font_height($font, $size)
{
$size = $this->_upscale($size) * self::FONT_SCALE;
$height = $this->get_font_height_actual($font, $size);
return $this->_downscale($height);
}
/**
* @param string $font
* @param float $size
*
* @return float
*/
protected function get_font_height_actual($font, $size)
{
$font = $this->get_ttf_file($font);
$ratio = $this->_dompdf->getOptions()->getFontHeightRatio();
// FIXME: word spacing
list(, $y2, , , , $y1) = imagettfbbox($size, 0, $font, "MXjpqytfhl"); // Test string with ascenders, descenders and caps
return ($y2 - $y1) * $ratio;
}
public function get_font_baseline($font, $size)
{
$ratio = $this->_dompdf->getOptions()->getFontHeightRatio();
return $this->get_font_height($font, $size) / $ratio;
}
public function new_page()
{
$this->_page_number++;
$this->_page_count++;
$this->_img = imagecreatetruecolor($this->_actual_width, $this->_actual_height);
$this->_bg_color = $this->_allocate_color($this->_bg_color_array);
imagealphablending($this->_img, true);
imagesavealpha($this->_img, true);
imagefill($this->_img, 0, 0, $this->_bg_color);
$this->_imgs[] = $this->_img;
}
public function open_object()
{
// N/A
}
public function close_object()
{
// N/A
}
public function add_object()
{
// N/A
}
public function page_script($callback): void
{
// N/A
}
public function page_text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0)
{
// N/A
}
public function page_line($x1, $y1, $x2, $y2, $color, $width, $style = [])
{
// N/A
}
/**
* Streams the image to the client.
*
* @param string $filename The filename to present to the client.
* @param array $options Associative array: 'type' => jpeg|jpg|png; 'quality' => 0 - 100 (JPEG only);
* 'page' => Number of the page to output (defaults to the first); 'Attachment': 1 or 0 (default 1).
*/
public function stream($filename, $options = [])
{
if (headers_sent()) {
die("Unable to stream image: headers already sent");
}
if (!isset($options["type"])) $options["type"] = "png";
if (!isset($options["Attachment"])) $options["Attachment"] = true;
$type = strtolower($options["type"]);
switch ($type) {
case "jpg":
case "jpeg":
$contentType = "image/jpeg";
$extension = ".jpg";
break;
case "png":
default:
$contentType = "image/png";
$extension = ".png";
break;
}
header("Cache-Control: private");
header("Content-Type: $contentType");
$filename = str_replace(["\n", "'"], "", basename($filename, ".$type")) . $extension;
$attachment = $options["Attachment"] ? "attachment" : "inline";
header(Helpers::buildContentDispositionHeader($attachment, $filename));
$this->_output($options);
flush();
}
/**
* Returns the image as a string.
*
* @param array $options Associative array: 'type' => jpeg|jpg|png; 'quality' => 0 - 100 (JPEG only);
* 'page' => Number of the page to output (defaults to the first).
* @return string
*/
public function output($options = [])
{
ob_start();
$this->_output($options);
return ob_get_clean();
}
/**
* Outputs the image stream directly.
*
* @param array $options Associative array: 'type' => jpeg|jpg|png; 'quality' => 0 - 100 (JPEG only);
* 'page' => Number of the page to output (defaults to the first).
*/
protected function _output($options = [])
{
if (!isset($options["type"])) $options["type"] = "png";
if (!isset($options["page"])) $options["page"] = 1;
$type = strtolower($options["type"]);
if (isset($this->_imgs[$options["page"] - 1])) {
$img = $this->_imgs[$options["page"] - 1];
} else {
$img = $this->_imgs[0];
}
// Perform any antialiasing
if ($this->_aa_factor != 1) {
$dst_w = round($this->_actual_width / $this->_aa_factor);
$dst_h = round($this->_actual_height / $this->_aa_factor);
$dst = imagecreatetruecolor($dst_w, $dst_h);
imagecopyresampled($dst, $img, 0, 0, 0, 0,
$dst_w, $dst_h,
$this->_actual_width, $this->_actual_height);
} else {
$dst = $img;
}
switch ($type) {
case "jpg":
case "jpeg":
if (!isset($options["quality"])) {
$options["quality"] = 75;
}
imagejpeg($dst, null, $options["quality"]);
break;
case "png":
default:
imagepng($dst);
break;
}
if ($this->_aa_factor != 1) {
imagedestroy($dst);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,477 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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
{
/**
* @param string|float[] $paper The paper size to use as either a standard paper size (see {@link Dompdf\Adapter\CPDF::$PAPER_SIZES})
* or an array of the form `[x1, y1, x2, y2]` (typically `[0, 0, width, height]`).
* @param string $orientation The paper orientation, either `portrait` or `landscape`.
* @param Dompdf|null $dompdf The Dompdf instance.
*/
public function __construct($paper = "letter", string $orientation = "portrait", ?Dompdf $dompdf = null);
/**
* @return Dompdf
*/
function get_dompdf();
/**
* Returns the current page number
*
* @return int
*/
function get_page_number();
/**
* Returns the total number of pages in the document
*
* @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 Cpdf::setLineStyle()} for a description of the format of the
* $style and $cap parameters (aka dash and cap).
*
* @param float $x1
* @param float $y1
* @param float $x2
* @param float $y2
* @param array $color Color array in the format `[r, g, b, "alpha" => alpha]`
* where r, g, b, and alpha are float values between 0 and 1
* @param float $width
* @param array $style
* @param string $cap `butt`, `round`, or `square`
*/
function line($x1, $y1, $x2, $y2, $color, $width, $style = [], $cap = "butt");
/**
* Draws an arc
*
* See {@link Cpdf::setLineStyle()} for a description of the format of the
* $style and $cap parameters (aka dash and cap).
*
* @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 array in the format `[r, g, b, "alpha" => alpha]`
* where r, g, b, and alpha are float values between 0 and 1
* @param float $width
* @param array $style
* @param string $cap `butt`, `round`, or `square`
*/
function arc($x, $y, $r1, $r2, $astart, $aend, $color, $width, $style = [], $cap = "butt");
/**
* Draws a rectangle at x1,y1 with width w and height h
*
* See {@link Cpdf::setLineStyle()} for a description of the format of the
* $style and $cap parameters (aka dash and cap).
*
* @param float $x1
* @param float $y1
* @param float $w
* @param float $h
* @param array $color Color array in the format `[r, g, b, "alpha" => alpha]`
* where r, g, b, and alpha are float values between 0 and 1
* @param float $width
* @param array $style
* @param string $cap `butt`, `round`, or `square`
*/
function rectangle($x1, $y1, $w, $h, $color, $width, $style = [], $cap = "butt");
/**
* Draws a filled rectangle at x1,y1 with width w and height h
*
* @param float $x1
* @param float $y1
* @param float $w
* @param float $h
* @param array $color Color array in the format `[r, g, b, "alpha" => alpha]`
* where r, g, b, and alpha are float values between 0 and 1
*/
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
*/
function clipping_roundrectangle($x1, $y1, $w, $h, $tl, $tr, $br, $bl);
/**
* Starts a clipping polygon
*
* @param float[] $points
*/
public function clipping_polygon(array $points): void;
/**
* Ends the last clipping shape
*/
function clipping_end();
/**
* Processes a callback on every page.
*
* The callback function receives the four parameters `int $pageNumber`,
* `int $pageCount`, `Canvas $canvas`, and `FontMetrics $fontMetrics`, in
* that order.
*
* This function can be used to add page numbers to all pages after the
* first one, for example.
*
* @param callable $callback The callback function to process on every page
*/
public function page_script($callback): void;
/**
* Writes text at the specified x and y coordinates on every page.
*
* The strings '{PAGE_NUM}' and '{PAGE_COUNT}' are automatically replaced
* with their current values.
*
* @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 Color array in the format `[r, g, b, "alpha" => alpha]`
* where r, g, b, and alpha are float values between 0 and 1
* @param float $word_space Word spacing adjustment
* @param float $char_space Char spacing adjustment
* @param float $angle Angle to write the text at, measured clockwise starting from the x-axis
*/
public function page_text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0);
/**
* Draws a line at the specified coordinates on every page.
*
* @param float $x1
* @param float $y1
* @param float $x2
* @param float $y2
* @param array $color Color array in the format `[r, g, b, "alpha" => alpha]`
* where r, g, b, and alpha are float values between 0 and 1
* @param float $width
* @param array $style
*/
public function page_line($x1, $y1, $x2, $y2, $color, $width, $style = []);
/**
* 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 float $a
* @param float $b
* @param float $c
* @param float $d
* @param float $e
* @param float $f
*/
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:
* ```
* array(0 => x1,
* 1 => y1,
* 2 => x2,
* 3 => y2,
* ...
* );
* ```
*
* See {@link Cpdf::setLineStyle()} for a description of the format of the
* $style parameter (aka dash).
*
* @param array $points
* @param array $color Color array in the format `[r, g, b, "alpha" => alpha]`
* where r, g, b, and alpha are float values between 0 and 1
* @param float $width
* @param array $style
* @param bool $fill Fills the polygon if true
*/
function polygon($points, $color, $width = null, $style = [], $fill = false);
/**
* Draws a circle at $x,$y with radius $r
*
* See {@link Cpdf::setLineStyle()} for a description of the format of the
* $style parameter (aka dash).
*
* @param float $x
* @param float $y
* @param float $r
* @param array $color Color array in the format `[r, g, b, "alpha" => alpha]`
* where r, g, b, and alpha are float values between 0 and 1
* @param float $width
* @param array $style
* @param bool $fill Fills the circle if true
*/
function circle($x, $y, $r, $color, $width = null, $style = [], $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 The path to the image
* @param float $x X position
* @param float $y Y position
* @param float $w Width
* @param float $h Height
* @param string $resolution The resolution of the image
*/
function image($img, $x, $y, $w, $h, $resolution = "normal");
/**
* Writes text at the specified x and y coordinates
*
* @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 Color array in the format `[r, g, b, "alpha" => alpha]`
* where r, g, b, and alpha are float values between 0 and 1
* @param float $word_space Word spacing adjustment
* @param float $char_space Char spacing adjustment
* @param float $angle Angle to write the text at, measured clockwise starting from the x-axis
*/
function text($x, $y, $text, $font, $size, $color = [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 $label Label of the value (Creator, Producer, etc.)
* @param string $value The text to set
*/
public function add_info(string $label, string $value): void;
/**
* Calculates text size, in points
*
* @param string $text The text to be sized
* @param string $font The font file to use
* @param float $size The font size, in points
* @param float $word_spacing Word spacing, if any
* @param float $char_spacing Char spacing, if any
*
* @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 The font file to use
* @param float $size The font size, in points
*
* @return float
*/
function get_font_height($font, $size);
/**
* Returns the font x-height, in points
*
* @param string $font The font file to use
* @param float $size The font size, in points
*
* @return float
*/
//function get_font_x_height($font, $size);
/**
* Calculates font baseline, in points
*
* @param string $font The font file to use
* @param float $size The font size, in points
*
* @return float
*/
function get_font_baseline($font, $size);
/**
* Returns the PDF's width in points
*
* @return float
*/
function get_width();
/**
* Returns the PDF's height in points
*
* @return float
*/
function get_height();
/**
* Sets the opacity
*
* @param float $opacity
* @param string $mode
*/
public function set_opacity(float $opacity, string $mode = "Normal"): void;
/**
* 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
*/
function set_default_view($view, $options = []);
/**
* @param string $code
*/
function javascript($code);
/**
* Starts a new page
*
* Subsequent drawing operations will appear on the new page.
*/
function new_page();
/**
* Streams the PDF to the client.
*
* @param string $filename The filename to present to the client.
* @param array $options Associative array: 'compress' => 1 or 0 (default 1); 'Attachment' => 1 or 0 (default 1).
*/
function stream($filename, $options = []);
/**
* Returns the PDF as a string.
*
* @param array $options Associative array: 'compress' => 1 or 0 (default 1).
*
* @return string
*/
function output($options = []);
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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|float[] $paper
* @param string $orientation
* @param string|null $class
*
* @return Canvas
*/
static function get_instance(Dompdf $dompdf, $paper, string $orientation, ?string $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" && extension_loaded('gd')) {
$class = "Dompdf\\Adapter\\GD";
} else {
$class = "Dompdf\\Adapter\\CPDF";
}
}
}
return new $class($paper, $orientation, $dompdf);
}
}

View File

@@ -0,0 +1,999 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
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.
*/
protected const BORDER_STYLE_SCORE = [
"double" => 8,
"solid" => 7,
"dashed" => 6,
"dotted" => 5,
"ridge" => 4,
"outset" => 3,
"groove" => 2,
"inset" => 1,
"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 array[]
*/
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 whether the columns' width can be modified
*
* @var bool
*/
private $_columns_locked = false;
/**
* Tells whether 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(): void
{
$this->_num_rows = 0;
$this->_num_cols = 0;
$this->_cells = [];
$this->_frames = [];
if (!$this->_columns_locked) {
$this->_columns = [];
}
$this->_rows = [];
$this->_borders = [];
$this->__col = $this->__row = 0;
}
public function lock_columns(): void
{
$this->_columns_locked = true;
}
/**
* @return bool
*/
public function is_columns_locked()
{
return $this->_columns_locked;
}
/**
* @param bool $fixed
*/
public function set_layout_fixed(bool $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] = [
"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] = [
"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] = [
"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 [
"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 array|null
*/
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");
}
// Positions are stored relative to the table position
[$table_x, $table_y] = $this->_table->get_position();
$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 = $table_x;
} else {
$x = $table_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 = $table_y;
} else {
$y = $table_y + $this->_rows[$row]["y"];
}
return [$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"] = $col["x"] + $width;
}
/**
* @param int $i
* @param long $height
*/
public function set_row_height($i, $height)
{
$row =& $this->get_row($i);
if ($height > $row["height"]) {
$row["height"] = $height;
}
$next_row =& $this->get_row($i + 1);
$next_row["y"] = $row["y"] + $row["height"];
}
/**
* https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution
*
* @param int $i
* @param int $j
* @param string $h_v `horizontal` or `vertical`
* @param array $border_spec
*/
protected function resolve_border(int $i, int $j, string $h_v, array $border_spec): void
{
if (!isset($this->_borders[$i][$j][$h_v])) {
$this->_borders[$i][$j][$h_v] = $border_spec;
return;
}
$border = $this->_borders[$i][$j][$h_v];
$n_width = $border_spec["width"];
$n_style = $border_spec["style"];
$o_width = $border["width"];
$o_style = $border["style"];
if ($o_style === "hidden") {
return;
}
// A style of `none` has lowest priority independent of its specified
// width here, as its resolved width is always 0
if ($n_style === "hidden" || $n_width > $o_width
|| ($o_width == $n_width
&& isset(self::BORDER_STYLE_SCORE[$n_style])
&& isset(self::BORDER_STYLE_SCORE[$o_style])
&& self::BORDER_STYLE_SCORE[$n_style] > self::BORDER_STYLE_SCORE[$o_style])
) {
$this->_borders[$i][$j][$h_v] = $border_spec;
}
}
/**
* Get the resolved border properties for the given frame.
*
* @param AbstractFrameDecorator $frame
*
* @return array[]
*/
protected function get_resolved_border(AbstractFrameDecorator $frame): array
{
$key = $frame->get_id();
$columns = $this->_frames[$key]["columns"];
$rows = $this->_frames[$key]["rows"];
$first_col = $columns[0];
$last_col = $columns[count($columns) - 1];
$first_row = $rows[0];
$last_row = $rows[count($rows) - 1];
$max_top = null;
$max_bottom = null;
$max_left = null;
$max_right = null;
foreach ($columns as $col) {
$top = $this->_borders[$first_row][$col]["horizontal"];
$bottom = $this->_borders[$last_row + 1][$col]["horizontal"];
if ($max_top === null || $top["width"] > $max_top["width"]) {
$max_top = $top;
}
if ($max_bottom === null || $bottom["width"] > $max_bottom["width"]) {
$max_bottom = $bottom;
}
}
foreach ($rows as $row) {
$left = $this->_borders[$row][$first_col]["vertical"];
$right = $this->_borders[$row][$last_col + 1]["vertical"];
if ($max_left === null || $left["width"] > $max_left["width"]) {
$max_left = $left;
}
if ($max_right === null || $right["width"] > $max_right["width"]) {
$max_right = $right;
}
}
return [$max_top, $max_right, $max_bottom, $max_left];
}
/**
* @param AbstractFrameDecorator $frame
*/
public function add_frame(Frame $frame): void
{
$style = $frame->get_style();
$display = $style->display;
$collapse = $this->_table->get_style()->border_collapse === "collapse";
// Recursively add the frames within the table, its row groups and rows
if ($frame === $this->_table
|| $display === "table-row"
|| in_array($display, TableFrameDecorator::ROW_GROUPS, true)
) {
$start_row = $this->__row;
foreach ($frame->get_children() as $child) {
$this->add_frame($child);
}
if ($display === "table-row") {
$this->add_row();
}
$num_rows = $this->__row - $start_row - 1;
$key = $frame->get_id();
// Row groups always span across the entire table
$this->_frames[$key]["columns"] = range(0, max(0, $this->_num_cols - 1));
$this->_frames[$key]["rows"] = range($start_row, max(0, $this->__row - 1));
$this->_frames[$key]["frame"] = $frame;
if ($collapse) {
$bp = $style->get_border_properties();
// Resolve vertical 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"]);
}
// Resolve horizontal borders
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"]);
}
if ($frame === $this->_table) {
// Clear borders because the cells are now using them. The
// border width still needs to be set to half the resolved
// width so that the table is positioned properly
[$top, $right, $bottom, $left] = $this->get_resolved_border($frame);
$style->set_used("border_top_width", $top["width"] / 2);
$style->set_used("border_right_width", $right["width"] / 2);
$style->set_used("border_bottom_width", $bottom["width"] / 2);
$style->set_used("border_left_width", $left["width"] / 2);
$style->set_used("border_style", "none");
} else {
// Clear borders for rows and row groups
$style->set_used("border_width", 0);
$style->set_used("border_style", "none");
}
}
if ($frame === $this->_table) {
// Apply resolved borders to table cells and calculate column
// widths after all frames have been added
$this->calculate_column_widths();
}
return;
}
// Add the frame to the cellmap
$key = $frame->get_id();
$node = $frame->get_node();
$bp = $style->get_border_properties();
// Determine where this cell is going
$colspan = max((int) $node->getAttribute("colspan"), 1);
$rowspan = max((int) $node->getAttribute("rowspan"), 1);
// 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
$this->resolve_border($row, $this->__col, "vertical", $bp["left"]);
$this->resolve_border($row, $this->__col + $colspan, "vertical", $bp["right"]);
}
}
// Columns:
for ($j = 0; $j < $colspan; $j++) {
$col = $this->__col + $j;
$this->_frames[$key]["columns"][] = $col;
if ($collapse) {
// Resolve horizontal borders
$this->resolve_border($this->__row, $col, "horizontal", $bp["top"]);
$this->resolve_border($this->__row + $rowspan, $col, "horizontal", $bp["bottom"]);
}
}
$this->_frames[$key]["frame"] = $frame;
$this->__col += $colspan;
if ($this->__col > $this->_num_cols) {
$this->_num_cols = $this->__col;
}
}
/**
* Apply resolved borders to table cells and calculate column widths.
*/
protected function calculate_column_widths(): void
{
$table = $this->_table;
$table_style = $table->get_style();
$collapse = $table_style->border_collapse === "collapse";
if ($collapse) {
$v_spacing = 0;
$h_spacing = 0;
} else {
// The additional 1/2 width gets added to the table proper
[$h, $v] = $table_style->border_spacing;
$v_spacing = $v / 2;
$h_spacing = $h / 2;
}
foreach ($this->_frames as $frame_info) {
/** @var TableCellFrameDecorator */
$frame = $frame_info["frame"];
$style = $frame->get_style();
$display = $style->display;
if ($display !== "table-cell") {
continue;
}
if ($collapse) {
// Set the resolved border at half width
[$top, $right, $bottom, $left] = $this->get_resolved_border($frame);
$style->set_used("border_top_width", $top["width"] / 2);
$style->set_used("border_top_style", $top["style"]);
$style->set_used("border_top_color", $top["color"]);
$style->set_used("border_right_width", $right["width"] / 2);
$style->set_used("border_right_style", $right["style"]);
$style->set_used("border_right_color", $right["color"]);
$style->set_used("border_bottom_width", $bottom["width"] / 2);
$style->set_used("border_bottom_style", $bottom["style"]);
$style->set_used("border_bottom_color", $bottom["color"]);
$style->set_used("border_left_width", $left["width"] / 2);
$style->set_used("border_left_style", $left["style"]);
$style->set_used("border_left_color", $left["color"]);
$style->set_used("margin", 0);
} else {
// Border spacing is effectively a margin between cells
$style->set_used("margin_top", $v_spacing);
$style->set_used("margin_bottom", $v_spacing);
$style->set_used("margin_left", $h_spacing);
$style->set_used("margin_right", $h_spacing);
}
if ($this->_columns_locked) {
continue;
}
$node = $frame->get_node();
$colspan = max((int) $node->getAttribute("colspan"), 1);
$first_col = $frame_info["columns"][0];
// Resolve the frame's width
if ($this->_fixed_layout) {
list($frame_min, $frame_max) = [0, 10e-10];
} else {
list($frame_min, $frame_max) = $frame->get_min_max_width();
}
$width = $style->width;
$val = null;
if (Helpers::is_percent($width) && $colspan === 1) {
$var = "percent";
$val = (float)rtrim($width, "% ");
} elseif ($width !== "auto" && $colspan === 1) {
$var = "absolute";
$val = $frame_min;
}
$min = 0;
$max = 0;
for ($cs = 0; $cs < $colspan; $cs++) {
// Resolve the frame's width(s) with other cells
$col =& $this->get_column($first_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 && $colspan === 1) {
// 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));
for ($c = 0; $c < $colspan; $c++) {
$col =& $this->get_column($first_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($first_col + $c);
$col["max-width"] += $inc;
}
}
}
// Adjust absolute columns so that the absolute (and max) width is the
// largest minimum width of all cells. This accounts for cells without
// absolute width within an absolute column
foreach ($this->_columns as &$col) {
if ($col["absolute"] > 0) {
$col["absolute"] = $col["min-width"];
$col["max-width"] = $col["min-width"];
}
}
}
protected function add_row(): void
{
$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 already 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 already 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();
$first_index = $this->_frames[$g_key]["rows"][0];
$last_index = $first_index;
$row = $last_row;
while ($row = $row->get_prev_sibling()) {
$last_index++;
}
$this->_frames[$g_key]["rows"] = range($first_index, $last_index);
}
public function assign_x_positions(): void
{
// 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(): void
{
// Pre-condition: widths and heights of each column & row must be
// calcluated
foreach ($this->_frames as $arr) {
$frame = $arr["frame"];
$h = 0.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()->set_used("height", $h);
}
}
}
/**
* Re-adjust frame height if the table height is larger than its content
*/
public function set_frame_heights(float $table_height, float $content_height): void
{
// Distribute the increased height proportionally amongst each row
foreach ($this->_frames as $arr) {
$frame = $arr["frame"];
$h = 0.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.0;
}
if ($frame instanceof TableCellFrameDecorator) {
$frame->set_cell_height($new_height);
} else {
$frame->get_style()->set_used("height", $new_height);
}
}
}
/**
* Used for debugging:
*
* @return string
*/
public function __toString(): string
{
$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 = [];
foreach ($this->_frames as $key => $val) {
$arr[$key] = ["columns" => $val["columns"], "rows" => $val["rows"]];
}
$str .= Helpers::pre_r($arr, true);
if (php_sapi_name() == "cli") {
$str = strip_tags(str_replace(["<br/>", "<b>", "</b>"],
["\n", chr(27) . "[01;33m", chr(27) . "[0m"],
$str));
}
return $str;
}
}

View File

@@ -0,0 +1,652 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Css;
use Dompdf\Frame;
use Dompdf\Helpers;
/**
* 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
private static $__ATTRIBUTE_LOOKUP = [
//'caption' => array ( 'align' => '', ),
'img' => [
'align' => [
'bottom' => 'vertical-align: baseline;',
'middle' => 'vertical-align: middle;',
'top' => 'vertical-align: top;',
'left' => 'float: left;',
'right' => 'float: right;'
],
'border' => 'border: %0.2Fpx solid;',
'height' => '_set_px_height',
'hspace' => 'padding-left: %1$0.2Fpx; padding-right: %1$0.2Fpx;',
'vspace' => 'padding-top: %1$0.2Fpx; padding-bottom: %1$0.2Fpx;',
'width' => '_set_px_width',
],
'table' => [
'align' => [
'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' => [
'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' => [
'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' => [
'align' => 'text-align: %s;',
],
'h1' => [
'align' => 'text-align: %s;',
],
'h2' => [
'align' => 'text-align: %s;',
],
'h3' => [
'align' => 'text-align: %s;',
],
'h4' => [
'align' => 'text-align: %s;',
],
'h5' => [
'align' => 'text-align: %s;',
],
'h6' => [
'align' => 'text-align: %s;',
],
//TODO: translate more form element attributes
'input' => [
'size' => '_set_input_width'
],
'p' => [
'align' => 'text-align: %s;',
],
// 'col' => array(
// 'align' => '',
// 'valign' => '',
// ),
// 'colgroup' => array(
// 'align' => '',
// 'valign' => '',
// ),
'tbody' => [
'align' => '_set_table_row_align',
'valign' => '_set_table_row_valign',
],
'td' => [
'align' => 'text-align: %s;',
'bgcolor' => '_set_background_color',
'height' => 'height: %s;',
'nowrap' => 'white-space: nowrap;',
'valign' => 'vertical-align: %s;',
'width' => 'width: %s;',
],
'tfoot' => [
'align' => '_set_table_row_align',
'valign' => '_set_table_row_valign',
],
'th' => [
'align' => 'text-align: %s;',
'bgcolor' => '_set_background_color',
'height' => 'height: %s;',
'nowrap' => 'white-space: nowrap;',
'valign' => 'vertical-align: %s;',
'width' => 'width: %s;',
],
'thead' => [
'align' => '_set_table_row_align',
'valign' => '_set_table_row_valign',
],
'tr' => [
'align' => '_set_table_row_align',
'bgcolor' => '_set_table_row_bgcolor',
'valign' => '_set_table_row_valign',
],
'body' => [
'background' => 'background-image: url(%s);',
'bgcolor' => '_set_background_color',
'link' => '_set_body_link',
'text' => '_set_color',
],
'br' => [
'clear' => 'clear: %s;',
],
'basefont' => [
'color' => '_set_color',
'face' => 'font-family: %s;',
'size' => '_set_basefont_size',
],
'font' => [
'color' => '_set_color',
'face' => 'font-family: %s;',
'size' => '_set_font_size',
],
'dir' => [
'compact' => 'margin: 0.5em 0;',
],
'dl' => [
'compact' => 'margin: 0.5em 0;',
],
'menu' => [
'compact' => 'margin: 0.5em 0;',
],
'ol' => [
'compact' => 'margin: 0.5em 0;',
'start' => 'counter-reset: -dompdf-default-counter %d;',
'type' => 'list-style-type: %s;',
],
'ul' => [
'compact' => 'margin: 0.5em 0;',
'type' => 'list-style-type: %s;',
],
'li' => [
'type' => 'list-style-type: %s;',
'value' => 'counter-reset: -dompdf-default-counter %d;',
],
'pre' => [
'width' => 'width: %s;',
],
];
protected static $_last_basefont_size = 3;
protected static $_font_size_lookup = [
// 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
*/
protected static function _resolve_target(\DOMNode $node, $target, $value)
{
if ($target[0] === "_") {
return self::$target($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[]
*/
protected static 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
*/
protected static 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
*/
protected static function _set_color(\DOMElement $node, $value)
{
$value = self::_get_valid_color($value);
return "color: $value;";
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return string
*/
protected static function _set_background_color(\DOMElement $node, $value)
{
$value = self::_get_valid_color($value);
return "background-color: $value;";
}
protected static function _set_px_width(\DOMElement $node, string $value): string
{
$v = trim($value);
if (Helpers::is_percent($v)) {
return sprintf("width: %s;", $v);
}
if (is_numeric(mb_substr($v, 0, 1))) {
return sprintf("width: %spx;", (float) $v);
}
return "";
}
protected static function _set_px_height(\DOMElement $node, string $value): string
{
$v = trim($value);
if (Helpers::is_percent($v)) {
return sprintf("height: %s;", $v);
}
if (is_numeric(mb_substr($v, 0, 1))) {
return sprintf("height: %spx;", (float) $v);
}
return "";
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return null
*/
protected static 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
*/
protected static function _set_table_border(\DOMElement $node, $value)
{
return "border-width: $value" . "px;";
}
/**
* @param \DOMElement $node
* @param string $value
*
* @return string
*/
protected static 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
*/
protected static 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
*/
protected static 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
*/
protected static 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
*/
protected static function _set_input_width(\DOMElement $node, $value)
{
if (empty($value)) { return null; }
if ($node->hasAttribute("type") && in_array(strtolower($node->getAttribute("type")), ["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
*/
protected static 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
*/
protected static 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
*/
protected static 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
*/
protected static 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
*/
protected static 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
*/
protected static 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, "; ");
}
}

View File

@@ -0,0 +1,339 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Css;
use Dompdf\Helpers;
class Color
{
static $cssColorNames = [
"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 array|string|null $color
* @return array|string|null
*/
static function parse($color)
{
if ($color === null) {
return null;
}
if (is_array($color)) {
// Assume the array has the right format...
// FIXME: should/could verify this.
return $color;
}
static $cache = [];
$color = strtolower($color);
if (isset($cache[$color])) {
return $cache[$color];
}
if ($color === "transparent") {
return $cache[$color] = $color;
}
if (isset(self::$cssColorNames[$color])) {
return $cache[$color] = self::getArray(self::$cssColorNames[$color]);
}
// https://www.w3.org/TR/css-color-4/#hex-notation
if (mb_substr($color, 0, 1) === "#") {
$length = mb_strlen($color);
$alpha = 1.0;
// #rgb format
if ($length === 4) {
return $cache[$color] = self::getArray($color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3]);
}
// #rgba format
if ($length === 5) {
if (ctype_xdigit($color[4])) {
$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
if ($length === 7) {
return $cache[$color] = self::getArray(mb_substr($color, 1, 6));
}
// #rrggbbaa format
if ($length === 9) {
if (ctype_xdigit(mb_substr($color, 7, 2))) {
$alpha = round(hexdec(mb_substr($color, 7, 2))/255, 2);
}
return $cache[$color] = self::getArray(mb_substr($color, 1, 6), $alpha);
}
return null;
}
// rgb( r g b [/α] ) / rgb( r,g,b[,α] ) format and alias rgba()
// https://www.w3.org/TR/css-color-4/#rgb-functions
if (mb_substr($color, 0, 4) === "rgb(" || mb_substr($color, 0, 5) === "rgba(") {
$i = mb_strpos($color, "(");
$j = mb_strpos($color, ")");
// Bad color value
if ($i === false || $j === false) {
return null;
}
$value_decl = trim(mb_substr($color, $i + 1, $j - $i - 1));
if (mb_strpos($value_decl, ",") === false) {
// Space-separated values syntax `r g b` or `r g b / α`
$parts = preg_split("/\s*\/\s*/", $value_decl);
$triplet = preg_split("/\s+/", $parts[0]);
$alpha = $parts[1] ?? 1.0;
} else {
// Comma-separated values syntax `r, g, b` or `r, g, b, α`
$parts = preg_split("/\s*,\s*/", $value_decl);
$triplet = array_slice($parts, 0, 3);
$alpha = $parts[3] ?? 1.0;
}
if (count($triplet) !== 3) {
return null;
}
// Parse alpha value
if (Helpers::is_percent($alpha)) {
$alpha = (float) $alpha / 100;
} else {
$alpha = (float) $alpha;
}
$alpha = max(0.0, min($alpha, 1.0));
foreach ($triplet as &$c) {
if (Helpers::is_percent($c)) {
$c = round((float) $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
if (mb_substr($color, 0, 5) === "cmyk(") {
$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);
}
// Invalid or unsupported color format
return null;
}
/**
* @param array|string $color
* @param float $alpha
* @return array
*/
static function getArray($color, $alpha = 1.0)
{
$c = [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 {
if (ctype_xdigit($color) === false || mb_strlen($color) !== 6) {
// invalid color value ... expected 6-character hex
return $c;
}
$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", $color, 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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);
}
}

View File

@@ -0,0 +1,635 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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 user font families 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 USER_FONTS_FILE = "installed-fonts.json";
/**
* Underlying {@link Canvas} object to perform text size calculations
*
* @var Canvas
*/
protected $canvas;
/**
* Array of bundled font family names to variants
*
* @var array
*/
protected $bundledFonts = [];
/**
* Array of user defined font family names to variants
*
* @var array
*/
protected $userFonts = [];
/**
* combined list of all font families with absolute paths
*
* @var array
*/
protected $fontFamilies;
/**
* @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::USER_FONTS_FILE}. This file should be writable by the
* webserver process.
*
* @see FontMetrics::loadFontFamilies()
*/
public function saveFontFamilies()
{
file_put_contents($this->getUserFontsFilePath(), json_encode($this->userFonts, JSON_PRETTY_PRINT));
}
/**
* @deprecated
*/
public function load_font_families()
{
$this->loadFontFamilies();
}
/**
* Loads the stored font family cache
*
* @see FontMetrics::saveFontFamilies()
*/
public function loadFontFamilies()
{
$file = $this->options->getRootDir() . "/lib/fonts/installed-fonts.dist.json";
$this->bundledFonts = json_decode(file_get_contents($file), true);
if (is_readable($this->getUserFontsFilePath())) {
$this->userFonts = json_decode(file_get_contents($this->getUserFontsFilePath()), true);
} else {
$this->loadFontFamiliesLegacy();
}
}
private function loadFontFamiliesLegacy()
{
$legacyCacheFile = $this->options->getFontDir() . '/dompdf_font_family_cache.php';
if (is_readable($legacyCacheFile)) {
$fontDir = $this->options->getFontDir();
$rootDir = $this->options->getRootDir();
if (!defined("DOMPDF_DIR")) { define("DOMPDF_DIR", $rootDir); }
if (!defined("DOMPDF_FONT_DIR")) { define("DOMPDF_FONT_DIR", $fontDir); }
$cacheDataClosure = require $legacyCacheFile;
$cacheData = is_array($cacheDataClosure) ? $cacheDataClosure : $cacheDataClosure($fontDir, $rootDir);
if (is_array($cacheData)) {
foreach ($cacheData as $family => $variants) {
if (!isset($this->bundledFonts[$family]) && is_array($variants)) {
foreach ($variants as $variant => $variantPath) {
$variantName = basename($variantPath);
$variantDir = dirname($variantPath);
if ($variantDir == $fontDir) {
$this->userFonts[$family][$variant] = $variantName;
} else {
$this->userFonts[$family][$variant] = $variantPath;
}
}
}
}
$this->saveFontFamilies();
}
}
}
/**
* @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)
{
$fontname = mb_strtolower($style["family"]);
$families = $this->getFontFamilies();
$entry = [];
if (isset($families[$fontname])) {
$entry = $families[$fontname];
}
$styleString = $this->getType("{$style['weight']} {$style['style']}");
$remoteHash = md5($remoteFile);
$prefix = $fontname . "_" . $styleString;
$prefix = trim($prefix, "-");
if (function_exists('iconv')) {
$prefix = @iconv('utf-8', 'us-ascii//TRANSLIT', $prefix);
}
$prefix_encoding = mb_detect_encoding($prefix, mb_detect_order(), true);
$substchar = mb_substitute_character();
mb_substitute_character(0x005F);
$prefix = mb_convert_encoding($prefix, "ISO-8859-1", $prefix_encoding);
mb_substitute_character($substchar);
$prefix = preg_replace("[\W]", "_", $prefix);
$prefix = preg_replace("/[^-_\w]+/", "", $prefix);
$localFile = $prefix . "_" . $remoteHash;
$localFilePath = $this->getOptions()->getFontDir() . "/" . $localFile;
if (isset($entry[$styleString]) && $localFilePath == $entry[$styleString]) {
return true;
}
$entry[$styleString] = $localFile;
// Download the remote file
[$protocol] = Helpers::explode_url($remoteFile);
$allowed_protocols = $this->options->getAllowedProtocols();
if (!array_key_exists($protocol, $allowed_protocols)) {
Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The communication protocol is not supported.", __FILE__, __LINE__);
return false;
}
foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
[$result, $message] = $rule($remoteFile);
if ($result !== true) {
Helpers::record_warnings(E_USER_WARNING, "Error loading $remoteFile: $message", __FILE__, __LINE__);
return false;
}
}
list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
if ($remoteFileContent === null) {
return false;
}
$localTempFile = @tempnam($this->options->get("tempDir"), "dompdf-font-");
file_put_contents($localTempFile, $remoteFileContent);
$font = Font::load($localTempFile);
if (!$font) {
unlink($localTempFile);
return false;
}
$font->parse();
$font->saveAdobeFontMetrics("$localFilePath.ufm");
$font->close();
unlink($localTempFile);
if ( !file_exists("$localFilePath.ufm") ) {
return false;
}
$fontExtension = ".ttf";
switch ($font->getFontType()) {
case "TrueType":
default:
$fontExtension = ".ttf";
break;
}
// Save the changes
file_put_contents($localFilePath.$fontExtension, $remoteFileContent);
if ( !file_exists($localFilePath.$fontExtension) ) {
unlink("$localFilePath.ufm");
return false;
}
$this->setFontFamily($fontname, $entry);
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 font file to use
* @param float $size The font size, in points
* @param float $wordSpacing Word spacing, if any
* @param float $charSpacing Char spacing, if any
*
* @return float
*/
public function getTextWidth(string $text, $font, float $size, float $wordSpacing = 0.0, float $charSpacing = 0.0): float
{
// @todo Make sure this cache is efficient before enabling it
static $cache = [];
if ($text === "") {
return 0;
}
// Don't cache long strings
$useCache = !isset($text[50]); // Faster than strlen
// Text-size calculations depend on the canvas used. Make sure to not
// return wrong values when switching canvas backends
$canvasClass = get_class($this->canvas);
$key = "$canvasClass/$font/$size/$wordSpacing/$charSpacing";
if ($useCache && isset($cache[$key][$text])) {
return $cache[$key][$text];
}
$width = $this->canvas->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, in points
*
* @param string $font The font file to use
* @param float $size The font size, in points
*
* @return float
*/
public function getFontHeight($font, float $size): float
{
return $this->canvas->get_font_height($font, $size);
}
/**
* Calculates font baseline, in points
*
* @param string $font The font file to use
* @param float $size The font size, in points
*
* @return float
*/
public function getFontBaseline($font, float $size): float
{
return $this->canvas->get_font_baseline($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|null $familyRaw
* @param string $subtypeRaw
*
* @return string|null
*/
public function getFont($familyRaw, $subtypeRaw = "normal")
{
static $cache = [];
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);
$families = $this->getFontFamilies();
if ($familyRaw) {
$family = str_replace(["'", '"'], "", strtolower($familyRaw));
if (isset($families[$family][$subtype])) {
return $cache[$familyRaw][$subtypeRaw] = $families[$family][$subtype];
}
return null;
}
$fallback_families = [strtolower($this->options->getDefaultFont()), "serif"];
foreach ($fallback_families as $family) {
if (isset($families[$family][$subtype])) {
return $cache[$familyRaw][$subtypeRaw] = $families[$family][$subtype];
}
if (!isset($families[$family])) {
continue;
}
$family = $families[$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(["'", '"'], "", mb_strtolower($family));
$families = $this->getFontFamilies();
if (isset($families[$family])) {
return $families[$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)) {
$weight = 700;
} elseif (preg_match('/([1-9]00)/', $type, $match)) {
$weight = (int)$match[0];
} else {
$weight = 400;
}
$weight = $weight === 400 ? 'normal' : $weight;
$weight = $weight === 700 ? 'bold' : $weight;
$style = preg_match('/italic|oblique/i', $type) ? 'italic' : null;
if ($weight === 'normal' && $style !== null) {
return $style;
}
return $style === null
? $weight
: $weight.'_'.$style;
}
/**
* @return array
* @deprecated
*/
public function get_font_families()
{
return $this->getFontFamilies();
}
/**
* Returns the current font lookup table
*
* @return array
*/
public function getFontFamilies()
{
if (!isset($this->fontFamilies)) {
$this->setFontFamilies();
}
return $this->fontFamilies;
}
/**
* Convert loaded fonts to font lookup table
*
* @return array
*/
public function setFontFamilies()
{
$fontFamilies = [];
if (isset($this->bundledFonts) && is_array($this->bundledFonts)) {
foreach ($this->bundledFonts as $family => $variants) {
if (!isset($fontFamilies[$family])) {
$fontFamilies[$family] = array_map(function ($variant) {
return $this->getOptions()->getRootDir() . '/lib/fonts/' . $variant;
}, $variants);
}
}
}
if (isset($this->userFonts) && is_array($this->userFonts)) {
foreach ($this->userFonts as $family => $variants) {
$fontFamilies[$family] = array_map(function ($variant) {
$variantName = basename($variant);
if ($variantName === $variant) {
return $this->getOptions()->getFontDir() . '/' . $variant;
}
return $variant;
}, $variants);
}
}
$this->fontFamilies = $fontFamilies;
}
/**
* @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->userFonts[mb_strtolower($fontname)] = $entry;
$this->saveFontFamilies();
unset($this->fontFamilies);
}
/**
* @return string
*/
public function getUserFontsFilePath()
{
return $this->options->getFontDir() . '/' . self::USER_FONTS_FILE;
}
/**
* @param Options $options
* @return $this
*/
public function setOptions(Options $options)
{
$this->options = $options;
unset($this->fontFamilies);
return $this;
}
/**
* @return Options
*/
public function getOptions()
{
return $this->options;
}
/**
* @param Canvas $canvas
* @return $this
*/
public function setCanvas(Canvas $canvas)
{
$this->canvas = $canvas;
return $this;
}
/**
* @return Canvas
*/
public function getCanvas()
{
return $this->canvas;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Frame;
use Dompdf\Dompdf;
use Dompdf\Exception;
use Dompdf\Frame;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameDecorator\Page as PageFrameDecorator;
use Dompdf\FrameReflower\Page as PageFrameReflower;
use Dompdf\Positioner\AbstractPositioner;
use DOMXPath;
/**
* 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)
*
* @package dompdf
*/
class Factory
{
/**
* Array of positioners for specific frame types
*
* @var AbstractPositioner[]
*/
protected static $_positioners;
/**
* Decorate the root Frame
*
* @param Frame $root The frame to decorate
* @param Dompdf $dompdf The dompdf instance
*
* @return PageFrameDecorator
*/
public static function decorate_root(Frame $root, Dompdf $dompdf): PageFrameDecorator
{
$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|null $root The root of the frame
*
* @throws Exception
* @return AbstractFrameDecorator|null
* FIXME: this is admittedly a little smelly...
*/
public static function decorate_frame(Frame $frame, Dompdf $dompdf, ?Frame $root = null): ?AbstractFrameDecorator
{
$style = $frame->get_style();
$display = $style->display;
switch ($display) {
case "block":
$positioner = "Block";
$decorator = "Block";
$reflower = "Block";
break;
case "inline-block":
$positioner = "Inline";
$decorator = "Block";
$reflower = "Block";
break;
case "inline":
$positioner = "Inline";
if ($frame->is_text_node()) {
$decorator = "Text";
$reflower = "Text";
} 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:
case "none":
if ($style->_dompdf_keep !== "yes") {
// Remove the node and the frame
$frame->get_parent()->remove_child($frame);
return null;
}
$positioner = "NullPositioner";
$decorator = "NullFrameDecorator";
$reflower = "NullFrameReflower";
break;
}
// Handle CSS position
$position = $style->position;
if ($position === "absolute") {
$positioner = "Absolute";
} elseif ($position === "fixed") {
$positioner = "Fixed";
}
$node = $frame->get_node();
// Handle nodeName
if ($node->nodeName === "img") {
$style->set_prop("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 && $parent_node instanceof \DOMElement) {
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->set_prop("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(string $type): AbstractPositioner
{
if (!isset(self::$_positioners[$type])) {
$class = '\\Dompdf\\Positioner\\'.$type;
self::$_positioners[$type] = new $class();
}
return self::$_positioners[$type];
}
}

View File

@@ -0,0 +1,100 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Frame;
use Iterator;
use Dompdf\Frame;
/**
* Linked-list Iterator
*
* Returns children in order and allows for the list to change during iteration,
* provided the changes occur to or after the current element.
*
* @package dompdf
*/
class FrameListIterator implements Iterator
{
/**
* @var Frame
*/
protected $parent;
/**
* @var Frame|null
*/
protected $cur;
/**
* @var Frame|null
*/
protected $prev;
/**
* @var int
*/
protected $num;
/**
* @param Frame $frame
*/
public function __construct(Frame $frame)
{
$this->parent = $frame;
$this->rewind();
}
public function rewind(): void
{
$this->cur = $this->parent->get_first_child();
$this->prev = null;
$this->num = 0;
}
/**
* @return bool
*/
public function valid(): bool
{
return $this->cur !== null;
}
/**
* @return int
*/
public function key(): int
{
return $this->num;
}
/**
* @return Frame|null
*/
public function current(): ?Frame
{
return $this->cur;
}
public function next(): void
{
if ($this->cur === null) {
return;
}
if ($this->cur->get_parent() === $this->parent) {
$this->prev = $this->cur;
$this->cur = $this->cur->get_next_sibling();
$this->num++;
} else {
// Continue from the previous child if the current frame has been
// moved to another parent
$this->cur = $this->prev !== null
? $this->prev->get_next_sibling()
: $this->parent->get_first_child();
}
}
}

View File

@@ -0,0 +1,324 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Frame;
use DOMDocument;
use DOMNode;
use DOMElement;
use DOMXPath;
use Dompdf\Exception;
use Dompdf\Frame;
use IteratorAggregate;
/**
* 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 capabilities for
* styling and layout.
*
* @package dompdf
*/
class FrameTree implements IteratorAggregate
{
/**
* Tags to ignore while parsing the tree
*
* @var array
*/
protected static $HIDDEN_TAGS = [
"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 = [];
}
/**
* Returns the DOMDocument object representing the current 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
*
* @deprecated Iterate the tree directly instead
* @return FrameTreeIterator
*/
public function get_frames(): FrameTreeIterator
{
return new FrameTreeIterator($this->_root);
}
/**
* Returns a post-order iterator for all frames in the tree
*
* @return FrameTreeIterator
*/
public function getIterator(): FrameTreeIterator
{
return new FrameTreeIterator($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 = [];
$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;
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Frame;
use Iterator;
use Dompdf\Frame;
/**
* Pre-order Iterator
*
* Returns frames in preorder traversal order (parent then children)
*
* @package dompdf
*/
class FrameTreeIterator implements Iterator
{
/**
* @var Frame
*/
protected $_root;
/**
* @var Frame[]
*/
protected $_stack = [];
/**
* @var int
*/
protected $_num;
/**
* @param Frame $root
*/
public function __construct(Frame $root)
{
$this->_stack[] = $this->_root = $root;
$this->_num = 0;
}
public function rewind(): void
{
$this->_stack = [$this->_root];
$this->_num = 0;
}
/**
* @return bool
*/
public function valid(): bool
{
return count($this->_stack) > 0;
}
/**
* @return int
*/
public function key(): int
{
return $this->_num;
}
/**
* @return Frame
*/
public function current(): Frame
{
return end($this->_stack);
}
public function next(): void
{
$b = array_pop($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;
}
}
}
}

View File

@@ -0,0 +1,923 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use DOMElement;
use DOMNode;
use Dompdf\Helpers;
use Dompdf\Dompdf;
use Dompdf\Exception;
use Dompdf\Frame;
use Dompdf\Frame\Factory;
use Dompdf\Frame\FrameListIterator;
use Dompdf\Frame\FrameTreeIterator;
use Dompdf\FrameReflower\AbstractFrameReflower;
use Dompdf\Css\Style;
use Dompdf\Positioner\AbstractPositioner;
/**
* Base AbstractFrameDecorator class
*
* @package dompdf
*/
abstract class AbstractFrameDecorator extends Frame
{
const DEFAULT_COUNTER = "-dompdf-default-counter";
/**
* array([id] => counter_value) (for generated content)
*
* @var array
*/
public $_counters = [];
/**
* 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 AbstractFrameReflower
*/
protected $_reflower;
/**
* Reference to the current dompdf instance
*
* @var Dompdf
*/
protected $_dompdf;
/**
* First block parent
*
* @var Block
*/
private $_block_parent;
/**
* First positioned parent (position: relative | absolute | fixed)
*
* @var AbstractFrameDecorator
*/
private $_positioned_parent;
/**
* Cache for the get_parent while loop results
*
* @var Frame
*/
private $_cached_parent;
/**
* Whether generated content and counters have been set.
*
* @var bool
*/
public $content_set = false;
/**
* Whether the frame has been split
*
* @var bool
*/
public $is_split = false;
/**
* Whether the frame is a split-off frame
*
* @var bool
*/
public $is_split_off = false;
/**
* 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": forcibly 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 AbstractFrameDecorator
*/
function copy(DOMNode $node)
{
$frame = new Frame($node);
$style = clone $this->_frame->get_style();
$style->reset();
$frame->set_style($style);
if ($node instanceof DOMElement && $node->hasAttribute("id")) {
$node->setAttribute("data-dompdf-original-id", $node->getAttribute("id"));
$node->removeAttribute("id");
}
return Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
}
/**
* Create a deep copy: copy this node and all children
*
* @return AbstractFrameDecorator
*/
function deep_copy()
{
$node = $this->_frame->get_node()->cloneNode();
$frame = new Frame($node);
$style = clone $this->_frame->get_style();
$style->reset();
$frame->set_style($style);
if ($node instanceof DOMElement && $node->hasAttribute("id")) {
$node->setAttribute("data-dompdf-original-id", $node->getAttribute("id"));
$node->removeAttribute("id");
}
$deco = Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
foreach ($this->get_children() as $child) {
$deco->append_child($child->deep_copy());
}
return $deco;
}
/**
* Create an anonymous child frame, inheriting styles from this frame.
*
* @param string $node_name
* @param string $display
*
* @return AbstractFrameDecorator
*/
public function create_anonymous_child(string $node_name, string $display): AbstractFrameDecorator
{
$style = $this->get_style();
$child_style = $style->get_stylesheet()->create_style();
$child_style->set_prop("display", $display);
$child_style->inherit($style);
$node = $this->get_node()->ownerDocument->createElement($node_name);
$frame = new Frame($node);
$frame->set_style($child_style);
return Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
}
function reset()
{
$this->_frame->reset();
$this->_reflower->reset();
$this->reset_generated_content();
$this->revert_counter_increment();
$this->content_set = false;
$this->_counters = [];
// clear parent lookup caches
$this->_cached_parent = null;
$this->_block_parent = null;
$this->_positioned_parent = null;
// Reset all children
foreach ($this->get_children() as $child) {
$child->reset();
}
}
/**
* If this represents a generated node then child nodes represent generated
* content. Remove the children since the content will be generated next
* time this frame is reflowed.
*/
protected function reset_generated_content(): void
{
if ($this->content_set
&& $this->get_node()->nodeName === "dompdf_generated"
) {
$content = $this->get_style()->content;
if ($content !== "normal" && $content !== "none") {
foreach ($this->get_children() as $child) {
$this->remove_child($child);
}
}
}
}
/**
* Decrement any counters that were incremented on the current node, unless
* that node is the body.
*/
protected function revert_counter_increment(): void
{
if ($this->content_set
&& $this->get_node()->nodeName !== "body"
&& ($decrement = $this->get_style()->counter_increment) !== "none"
) {
$this->decrement_counters($decrement);
}
}
// Getters -----------
function get_id()
{
return $this->_frame->get_id();
}
/**
* @return Frame
*/
function get_frame()
{
return $this->_frame;
}
function get_node()
{
return $this->_frame->get_node();
}
function get_style()
{
return $this->_frame->get_style();
}
/**
* @deprecated
*/
function get_original_style()
{
return $this->_frame->get_style();
}
function get_containing_block($i = null)
{
return $this->_frame->get_containing_block($i);
}
function get_position($i = null)
{
return $this->_frame->get_position($i);
}
/**
* @return Dompdf
*/
function get_dompdf()
{
return $this->_dompdf;
}
public function get_margin_width(): float
{
return $this->_frame->get_margin_width();
}
public function get_margin_height(): float
{
return $this->_frame->get_margin_height();
}
public function get_content_box(): array
{
return $this->_frame->get_content_box();
}
public function get_padding_box(): array
{
return $this->_frame->get_padding_box();
}
public function get_border_box(): array
{
return $this->_frame->get_border_box();
}
function set_id($id)
{
$this->_frame->set_id($id);
}
public function set_style(Style $style): void
{
$this->_frame->set_style($style);
}
function set_containing_block($x = null, $y = null, $w = null, $h = null)
{
$this->_frame->set_containing_block($x, $y, $w, $h);
}
function set_position($x = null, $y = null)
{
$this->_frame->set_position($x, $y);
}
function is_auto_height()
{
return $this->_frame->is_auto_height();
}
function is_auto_width()
{
return $this->_frame->is_auto_width();
}
function __toString()
{
return $this->_frame->__toString();
}
function prepend_child(Frame $child, $update_node = true)
{
while ($child instanceof AbstractFrameDecorator) {
$child = $child->_frame;
}
$this->_frame->prepend_child($child, $update_node);
}
function append_child(Frame $child, $update_node = true)
{
while ($child instanceof AbstractFrameDecorator) {
$child = $child->_frame;
}
$this->_frame->append_child($child, $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);
}
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);
}
function remove_child(Frame $child, $update_node = true)
{
while ($child instanceof AbstractFrameDecorator) {
$child = $child->_frame;
}
return $this->_frame->remove_child($child, $update_node);
}
/**
* @param bool $use_cache
* @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 FrameListIterator<AbstractFrameDecorator>
*/
public function get_children(): FrameListIterator
{
return new FrameListIterator($this);
}
/**
* @return FrameTreeIterator<AbstractFrameDecorator>
*/
function get_subtree(): FrameTreeIterator
{
return new FrameTreeIterator($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 AbstractPositioner
*/
function get_positioner()
{
return $this->_positioner;
}
/**
* @return 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
if (isset($this->_block_parent)) {
return $this->_block_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_positioned_parent()
{
// Find our nearest relative positioned parent
if (isset($this->_positioned_parent)) {
return $this->_positioned_parent;
}
$p = $this->get_parent();
while ($p) {
if ($p->is_positioned()) {
break;
}
$p = $p->get_parent();
}
if (!$p) {
$p = $this->_root;
}
return $this->_positioned_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|null $child
* @param bool $page_break
* @param bool $forced Whether the page break is forced.
*
* @throws Exception
*/
public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
{
if (is_null($child)) {
$this->get_parent()->split($this, $page_break, $forced);
return;
}
if ($child->get_parent() !== $this) {
throw new Exception("Unable to split: frame is not a child of this one.");
}
$this->revert_counter_increment();
$node = $this->_frame->get_node();
$split = $this->copy($node->cloneNode());
$style = $this->_frame->get_style();
$split_style = $split->get_style();
// Truncate the box decoration at the split, except for the body
if ($node->nodeName !== "body") {
// Clear bottom decoration of original frame
$style->margin_bottom = 0.0;
$style->padding_bottom = 0.0;
$style->border_bottom_width = 0.0;
$style->border_bottom_left_radius = 0.0;
$style->border_bottom_right_radius = 0.0;
// Clear top decoration of split frame
$split_style->margin_top = 0.0;
$split_style->padding_top = 0.0;
$split_style->border_top_width = 0.0;
$split_style->border_top_left_radius = 0.0;
$split_style->border_top_right_radius = 0.0;
$split_style->page_break_before = "auto";
}
$split_style->text_indent = 0.0;
$split_style->counter_reset = "none";
$this->is_split = true;
$split->is_split_off = true;
$split->_already_pushed = true;
$this->get_parent()->insert_child_after($split, $this);
if ($this instanceof Block) {
// Remove the frames that will be moved to the new split node from
// the line boxes
$this->remove_frames_from_line($child);
// recalculate the float offsets after paging
foreach ($this->get_line_boxes() as $line_box) {
$line_box->get_float_offsets();
}
}
if (!$forced) {
// Reset top margin in case of an unforced page break
// https://www.w3.org/TR/CSS21/page.html#allowed-page-breaks
$child->get_style()->margin_top = 0.0;
}
// Add $child and all following siblings to the new split node
$iter = $child;
while ($iter) {
$frame = $iter;
$iter = $iter->get_next_sibling();
$frame->reset();
$split->append_child($frame);
}
$this->get_parent()->split($split, $page_break, $forced);
// Preserve the current counter values. This must be done after the
// parent split, as counters get reset on frame reset
$split->_counters = $this->_counters;
}
/**
* @param array $counters
*/
public function reset_counters(array $counters): void
{
foreach ($counters as $id => $value) {
$this->reset_counter($id, $value);
}
}
/**
* @param string $id
* @param int $value
*/
public function reset_counter(string $id = self::DEFAULT_COUNTER, int $value = 0): void
{
$this->get_parent()->_counters[$id] = $value;
}
/**
* @param array $counters
*/
public function decrement_counters(array $counters): void
{
foreach ($counters as $id => $increment) {
$this->increment_counter($id, $increment * -1);
}
}
/**
* @param array $counters
*/
public function increment_counters(array $counters): void
{
foreach ($counters as $id => $increment) {
$this->increment_counter($id, $increment);
}
}
/**
* @param string $id
* @param int $increment
*/
public function increment_counter(string $id = self::DEFAULT_COUNTER, int $increment = 1): void
{
$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(string $id = self::DEFAULT_COUNTER, string $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 - 1) % 26) + ord('a'));
case "upper-latin":
case "upper-alpha":
return chr((($value - 1) % 26) + ord('A'));
case "lower-greek":
return Helpers::unichr($value + 944);
case "upper-greek":
return Helpers::unichr($value + 912);
}
}
final function position()
{
$this->_positioner->position($this);
}
/**
* @param float $offset_x
* @param float $offset_y
* @param bool $ignore_self
*/
final function move(float $offset_x, float $offset_y, bool $ignore_self = false): void
{
$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 public function get_min_max_width(): array
{
return $this->_reflower->get_min_max_width();
}
}

View File

@@ -0,0 +1,256 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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
*
* @package dompdf
*/
class Block extends AbstractFrameDecorator
{
/**
* Current line index
*
* @var int
*/
protected $_cl;
/**
* The block's line boxes
*
* @var LineBox[]
*/
protected $_line_boxes;
/**
* List of markers that have not found their line box to vertically align
* with yet. Markers are collected by nested block containers until an
* inline line box is found at the start of the block.
*
* @var ListBullet[]
*/
protected $dangling_markers;
/**
* Block constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
$this->_line_boxes = [new LineBox($this)];
$this->_cl = 0;
$this->dangling_markers = [];
}
function reset()
{
parent::reset();
$this->_line_boxes = [new LineBox($this)];
$this->_cl = 0;
$this->dangling_markers = [];
}
/**
* @return LineBox
*/
function get_current_line_box()
{
return $this->_line_boxes[$this->_cl];
}
/**
* @return int
*/
function get_current_line_number()
{
return $this->_cl;
}
/**
* @return LineBox[]
*/
function get_line_boxes()
{
return $this->_line_boxes;
}
/**
* @param int $line_number
* @return int
*/
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 = $cl);
}
/**
* @param int $i
*/
function clear_line($i)
{
if (isset($this->_line_boxes[$i])) {
unset($this->_line_boxes[$i]);
}
}
/**
* @param Frame $frame
* @return LineBox|null
*/
public function add_frame_to_line(Frame $frame): ?LineBox
{
$current_line = $this->_line_boxes[$this->_cl];
$frame->set_containing_line($current_line);
// Inline frames are currently treated as wrappers, and are not actually
// added to the line
if ($frame instanceof Inline) {
return null;
}
$current_line->add_frame($frame);
$this->increase_line_width($frame->get_margin_width());
$this->maximize_line_height($frame->get_margin_height(), $frame);
// Add any dangling list markers to the first line box if it is inline
if ($this->_cl === 0 && $current_line->inline
&& $this->dangling_markers !== []
) {
foreach ($this->dangling_markers as $marker) {
$current_line->add_list_marker($marker);
$this->maximize_line_height($marker->get_margin_height(), $marker);
}
$this->dangling_markers = [];
}
return $current_line;
}
/**
* Remove the given frame and all following frames and lines from the block.
*
* @param Frame $frame
*/
public function remove_frames_from_line(Frame $frame): void
{
// Inline frames are not added to line boxes themselves, only their
// text frame children
$actualFrame = $frame;
while ($actualFrame !== null && $actualFrame instanceof Inline) {
$actualFrame = $actualFrame->get_first_child();
}
if ($actualFrame === null) {
return;
}
// Search backwards through the lines for $frame
$frame = $actualFrame;
$i = $this->_cl;
$j = null;
while ($i > 0) {
$line = $this->_line_boxes[$i];
foreach ($line->get_frames() as $index => $f) {
if ($frame === $f) {
$j = $index;
break 2;
}
}
$i--;
}
if ($j === null) {
return;
}
// Remove all lines that follow
for ($k = $this->_cl; $k > $i; $k--) {
unset($this->_line_boxes[$k]);
}
// Remove the line, if it is empty
if ($j > 0) {
$line->remove_frames($j);
} else {
unset($this->_line_boxes[$i]);
}
// Reset array indices
$this->_line_boxes = array_values($this->_line_boxes);
$this->_cl = count($this->_line_boxes) - 1;
}
/**
* @param float $w
*/
public function increase_line_width(float $w): void
{
$this->_line_boxes[$this->_cl]->w += $w;
}
/**
* @param float $val
* @param Frame $frame
*/
public function maximize_line_height(float $val, Frame $frame): void
{
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
*/
public function add_line(bool $br = false): void
{
$line = $this->_line_boxes[$this->_cl];
$line->br = $br;
$y = $line->y + $line->h;
$new_line = new LineBox($this, $y);
$this->_line_boxes[++$this->_cl] = $new_line;
}
/**
* @param ListBullet $marker
*/
public function add_dangling_marker(ListBullet $marker): void
{
$this->dangling_markers[] = $marker;
}
/**
* Inherit any dangling markers from the parent block.
*
* @param Block $block
*/
public function inherit_dangling_markers(self $block): void
{
if ($block->dangling_markers !== []) {
$this->dangling_markers = $block->dangling_markers;
$block->dangling_markers = [];
}
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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;
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->getOptions()
);
if (Cache::is_broken($this->_image_url) &&
$alt = $frame->get_node()->getAttribute("alt")
) {
$fontMetrics = $dompdf->getFontMetrics();
$style = $frame->get_style();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$style->width = (4 / 3) * $fontMetrics->getTextWidth($alt, $font, $size, $word_spacing, $letter_spacing);
$style->height = $fontMetrics->getFontHeight($font, $size);
}
}
/**
* Get the intrinsic pixel dimensions of the image.
*
* @return array Width and height as `float|int`.
*/
public function get_intrinsic_dimensions(): array
{
[$width, $height] = Helpers::dompdf_getimagesize($this->_image_url, $this->_dompdf->getHttpContext());
return [$width, $height];
}
/**
* Resample the given pixel length according to dpi.
*
* @param float|int $length
* @return float
*/
public function resample($length): float
{
$dpi = $this->_dompdf->getOptions()->getDpi();
return ($length * 72) / $dpi;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,121 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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;
/**
* Decorates frames for inline layout
*
* @package dompdf
*/
class Inline extends AbstractFrameDecorator
{
/**
* Inline constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
}
/**
* Vertical padding, border, and margin do not apply when determining the
* height for inline 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
*/
public function get_margin_height(): float
{
$style = $this->get_style();
$font = $style->font_family;
$size = $style->font_size;
$fontHeight = $this->_dompdf->getFontMetrics()->getFontHeight($font, $size);
return ($style->line_height / ($size > 0 ? $size : 1)) * $fontHeight;
}
public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
{
if (is_null($child)) {
$this->get_parent()->split($this, $page_break, $forced);
return;
}
if ($child->get_parent() !== $this) {
throw new Exception("Unable to split: frame is not a child of this one.");
}
$this->revert_counter_increment();
$node = $this->_frame->get_node();
$split = $this->copy($node->cloneNode());
$style = $this->_frame->get_style();
$split_style = $split->get_style();
// Unset the current node's right style properties
$style->margin_right = 0.0;
$style->padding_right = 0.0;
$style->border_right_width = 0.0;
$style->border_top_right_radius = 0.0;
$style->border_bottom_right_radius = 0.0;
// Unset the split node's left style properties since we don't want them
// to propagate
$split_style->margin_left = 0.0;
$split_style->padding_left = 0.0;
$split_style->border_left_width = 0.0;
$split_style->border_top_left_radius = 0.0;
$split_style->border_bottom_left_radius = 0.0;
// If this is a generated node don't propagate the content style
if ($split->get_node()->nodeName == "dompdf_generated") {
$split_style->content = "normal";
}
//On continuation of inline element on next line,
//don't repeat non-horizontally repeatable background images
//See e.g. in testcase image_variants, long descriptions
if (($url = $style->background_image) && $url !== "none"
&& ($repeat = $style->background_repeat) && $repeat !== "repeat" && $repeat !== "repeat-x"
) {
$split_style->background_image = "none";
}
$this->get_parent()->insert_child_after($split, $this);
// Add $child and all following siblings to the new split node
$iter = $child;
while ($iter) {
$frame = $iter;
$iter = $iter->get_next_sibling();
$frame->reset();
$split->append_child($frame);
}
$parent = $this->get_parent();
if ($page_break) {
$parent->split($split, $page_break, $forced);
} elseif ($parent instanceof Inline) {
$parent->split($split);
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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
{
/**
* Bullet diameter as fraction of font size.
*/
public const BULLET_SIZE = 0.35;
/**
* Bullet offset from font baseline as fraction of font size.
*/
public const BULLET_OFFSET = 0.1;
/**
* Thickness of bullet outline as fraction of font size.
* See also `DECO_THICKNESS`. Screen: 0.08, print: better less, e.g. 0.04.
*/
public const BULLET_THICKNESS = 0.04;
/**
* Indentation from the start of the line as fraction of font size.
*/
public const MARKER_INDENT = 0.52;
/**
* ListBullet constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
parent::__construct($frame, $dompdf);
}
/**
* Get the width of the bullet symbol.
*
* @return float
*/
public function get_width(): float
{
$style = $this->_frame->get_style();
if ($style->list_style_type === "none") {
return 0.0;
}
return $style->font_size * self::BULLET_SIZE;
}
/**
* Get the height of the bullet symbol.
*
* @return float
*/
public function get_height(): float
{
$style = $this->_frame->get_style();
if ($style->list_style_type === "none") {
return 0.0;
}
return $style->font_size * self::BULLET_SIZE;
}
/**
* Get the width of the bullet, including indentation.
*/
public function get_margin_width(): float
{
$style = $this->get_style();
if ($style->list_style_type === "none") {
return 0.0;
}
return $style->font_size * (self::BULLET_SIZE + self::MARKER_INDENT);
}
/**
* Get the line height for the bullet.
*
* This increases the height of the corresponding line box when necessary.
*/
public function get_margin_height(): float
{
$style = $this->get_style();
if ($style->list_style_type === "none") {
return 0.0;
}
// TODO: This is a copy of `FrameDecorator\Text::get_margin_height()`
// Would be nice to properly refactor that at some point
$font = $style->font_family;
$size = $style->font_size;
$fontHeight = $this->_dompdf->getFontMetrics()->getFontHeight($font, $size);
return ($style->line_height / ($size > 0 ? $size : 1)) * $fontHeight;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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;
use Dompdf\Image\Cache;
/**
* Decorates frames for list bullets with custom images
*
* @package dompdf
*/
class ListBulletImage extends ListBullet
{
/**
* The underlying image frame
*
* @var Image
*/
protected $_img;
/**
* The image's width in pixels
*
* @var float
*/
protected $_width;
/**
* The image's height in pixels
*
* @var float
*/
protected $_height;
/**
* ListBulletImage constructor.
* @param Frame $frame
* @param Dompdf $dompdf
*/
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);
$url = $this->_img->get_image_url();
if (Cache::is_broken($url)) {
$this->_width = parent::get_width();
$this->_height = parent::get_height();
} else {
// Resample the bullet image to be consistent with 'auto' sized images
[$width, $height] = $this->_img->get_intrinsic_dimensions();
$this->_width = $this->_img->resample($width);
$this->_height = $this->_img->resample($height);
}
}
public function get_width(): float
{
return $this->_width;
}
public function get_height(): float
{
return $this->_height;
}
public function get_margin_width(): float
{
$style = $this->get_style();
return $this->_width + $style->font_size * self::MARKER_INDENT;
}
public function get_margin_height(): float
{
$fontMetrics = $this->_dompdf->getFontMetrics();
$style = $this->get_style();
$font = $style->font_family;
$size = $style->font_size;
$fontHeight = $fontMetrics->getFontHeight($font, $size);
$baseline = $fontMetrics->getFontBaseline($font, $size);
// This is the same factor as used in
// `FrameDecorator\Text::get_margin_height()`
$f = $style->line_height / ($size > 0 ? $size : 1);
// FIXME: Tries to approximate replacing the space above the font
// baseline with the image
return $f * ($fontHeight - $baseline) + $this->_height;
}
/**
* Return image url
*
* @return string
*/
function get_image_url()
{
return $this->_img->get_image_url();
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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;
}
}

View File

@@ -0,0 +1,753 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Dompdf;
use Dompdf\Helpers;
use Dompdf\Frame;
use Dompdf\Renderer;
/**
* Decorates frames for page layout
*
* @package dompdf
*/
class Page extends AbstractFrameDecorator
{
/**
* The y value of the bottom edge of the page area.
*
* https://www.w3.org/TR/CSS21/page.html#page-margins
*
* @var float
*/
protected $bottom_page_edge;
/**
* 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 = [];
//........................................................................
/**
* 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_edge = 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;
}
/**
* Calculate the bottom edge of the page area after margins have been
* applied for the current page.
*/
public function calculate_bottom_page_edge(): void
{
[, , , $cbh] = $this->get_containing_block();
$style = $this->get_style();
$margin_bottom = (float) $style->length_in_pt($style->margin_bottom, $cbh);
$this->bottom_page_edge = $cbh - $margin_bottom;
}
/**
* 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 = [];
$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 preceding frame's
* page_break_after property.
*
* @link http://www.w3.org/TR/CSS21/page.html#forced
*
* @param AbstractFrameDecorator $frame the frame to check
*
* @return bool true if a page break occurred
*/
function check_forced_page_break(Frame $frame)
{
// Skip check if page is already split and for the body
if ($this->_page_full || $frame->get_node()->nodeName === "body") {
return false;
}
$page_breaks = ["always", "left", "right"];
$style = $frame->get_style();
if (($frame->is_block_level() || $style->display === "table-row")
&& in_array($style->page_break_before, $page_breaks, true)
) {
// Prevent cascading splits
$frame->split(null, true, true);
$style->page_break_before = "auto";
$this->_page_full = true;
$frame->_already_pushed = true;
return true;
}
// Find the preceding block-level sibling (or table row). Inline
// elements are treated as if wrapped in an anonymous block container
// here. See https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
$prev = $frame->get_prev_sibling();
while ($prev && (($prev->is_text_node() && $prev->get_node()->nodeValue === "")
|| $prev->get_node()->nodeName === "bullet")
) {
$prev = $prev->get_prev_sibling();
}
if ($prev && ($prev->is_block_level() || $prev->get_style()->display === "table-row")) {
if (in_array($prev->get_style()->page_break_after, $page_breaks, true)) {
// Prevent cascading splits
$frame->split(null, true, true);
$prev->get_style()->page_break_after = "auto";
$this->_page_full = true;
$frame->_already_pushed = true;
return true;
}
$prev_last_child = $prev->get_last_child();
while ($prev_last_child && (($prev_last_child->is_text_node() && $prev_last_child->get_node()->nodeValue === "")
|| $prev_last_child->get_node()->nodeName === "bullet")
) {
$prev_last_child = $prev_last_child->get_prev_sibling();
}
if ($prev_last_child
&& $prev_last_child->is_block_level()
&& in_array($prev_last_child->get_style()->page_break_after, $page_breaks, true)
) {
$frame->split(null, true, true);
$prev_last_child->get_style()->page_break_after = "auto";
$this->_page_full = true;
$frame->_already_pushed = true;
return true;
}
}
return false;
}
/**
* Check for a gap between the top content edge of a frame and its child
* content.
*
* Additionally, the top margin, border, and padding of the frame must fit
* on the current page.
*
* @param float $childPos The top margin or line-box edge of the child content.
* @param Frame $frame The parent frame to check.
* @return bool
*/
protected function hasGap(float $childPos, Frame $frame): bool
{
$style = $frame->get_style();
$cbw = $frame->get_containing_block("w");
$contentEdge = $frame->get_position("y") + (float) $style->length_in_pt([
$style->margin_top,
$style->border_top_width,
$style->padding_top
], $cbw);
return Helpers::lengthGreater($childPos, $contentEdge)
&& Helpers::lengthLessOrEqual($contentEdge, $this->bottom_page_edge);
}
/**
* 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 an
* unforced page break occurs here, the used values of the
* relevant 'margin-top' and 'margin-bottom' properties are set
* to '0'. When a forced page break occurs here, the used value
* of the relevant 'margin-bottom' property is set to '0'; the
* relevant 'margin-top' used value may either be set to '0' or
* retained.
* 2. Between line boxes inside a block container box.
* 3. Between the content edge of a block container box and the
* outer edges of its child content (margin edges of block-level
* children or line box edges for inline-level children) if there
* is a (non-zero) gap between them.
*
* 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 a 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) or (3) is allowed only
* if the 'page-break-inside' property of the element and all
* its ancestors is 'auto'.
*
* If the above does not provide enough break points to keep content
* from overflowing the page boxes, then rules A, B and D are
* dropped in order to find additional breakpoints.
*
* If that still does not lead to sufficient break points, rule C is
* dropped as well, to find still more break points.
*
* We also allow breaks between table rows.
*
* @param AbstractFrameDecorator $frame the frame to check
*
* @return bool true if a break is allowed, false otherwise
*/
protected function _page_break_allowed(Frame $frame)
{
Helpers::dompdf_debug("page-break", "_page_break_allowed(" . $frame->get_node()->nodeName . ")");
$display = $frame->get_style()->display;
// Block Frames (1):
if ($frame->is_block_level() || $display === "-dompdf-image") {
// Avoid breaks within table-cells
if ($this->_in_table > ($display === "table" ? 1 : 0)) {
Helpers::dompdf_debug("page-break", "In table: " . $this->_in_table);
return false;
}
// Rule A
if ($frame->get_style()->page_break_before === "avoid") {
Helpers::dompdf_debug("page-break", "before: avoid");
return false;
}
// Find the preceding block-level sibling. Inline elements are
// treated as if wrapped in an anonymous block container here. See
// https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
$prev = $frame->get_prev_sibling();
while ($prev && (($prev->is_text_node() && $prev->get_node()->nodeValue === "")
|| $prev->get_node()->nodeName === "bullet")
) {
$prev = $prev->get_prev_sibling();
}
// Does the previous element allow a page break after?
if ($prev && ($prev->is_block_level() || $prev->get_style()->display === "-dompdf-image")
&& $prev->get_style()->page_break_after === "avoid"
) {
Helpers::dompdf_debug("page-break", "after: avoid");
return false;
}
// Rules B & D
$parent = $frame->get_parent();
$p = $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 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;
}
// Check for a possible type (3) break
if (!$prev && $parent && !$this->hasGap($frame->get_position("y"), $parent)) {
Helpers::dompdf_debug("page-break", "First block-level frame, no gap");
return false;
}
Helpers::dompdf_debug("page-break", "block: break allowed");
return true;
} // Inline frames (2):
else {
if ($frame->is_inline_level()) {
// 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();
$parent_style = $block_parent->get_style();
$line = $block_parent->get_current_line_box();
$line_count = count($block_parent->get_line_boxes());
$line_number = $frame->get_containing_line() && empty($line->get_frames())
? $line_count - 1
: $line_count;
// The line number of the frame can be less than the current
// number of line boxes, in case we are backtracking. As long as
// we are not checking for widows yet, just checking against the
// number of line boxes is sufficient in most cases, though.
if ($line_number <= $parent_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;
}
Helpers::dompdf_debug("page-break", "inline: break allowed");
return true;
// Table-rows
} else {
if ($display === "table-row") {
// 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;
}
// Rule A (table row)
if ($frame->get_style()->page_break_before === "avoid") {
Helpers::dompdf_debug("page-break", "before: avoid");
return false;
}
// Find the preceding row
$prev = $frame->get_prev_sibling();
if (!$prev) {
$prev_group = $frame->get_parent()->get_prev_sibling();
if ($prev_group
&& in_array($prev_group->get_style()->display, Table::ROW_GROUPS, true)
) {
$prev = $prev_group->get_last_child();
}
}
// Check if a page break is allowed after the preceding row
if ($prev && $prev->get_style()->page_break_after === "avoid") {
Helpers::dompdf_debug("page-break", "after: avoid");
return false;
}
// Avoid breaking before the first row of a table
if (!$prev) {
Helpers::dompdf_debug("page-break", "table: first-row");
return false;
}
// Rule B (table row)
// Check if the page_break_inside property is not 'avoid'
// for the parent table or any of its ancestors
$table = Table::find_parent_table($frame);
$p = $table;
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();
}
Helpers::dompdf_debug("page-break", "table-row: break allowed");
return true;
} else {
if (in_array($display, Table::ROW_GROUPS, true)) {
// Disallow breaks at row-groups: only split at row boundaries
return false;
} else {
Helpers::dompdf_debug("page-break", "? " . $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 AbstractFrameDecorator $frame the frame to check
*
* @return bool
*/
function check_page_break(Frame $frame)
{
if ($this->_page_full || $frame->_already_pushed
// Never check for breaks on empty text nodes
|| ($frame->is_text_node() && $frame->get_node()->nodeValue === "")
) {
return false;
}
$p = $frame;
do {
$display = $p->get_style()->display;
if ($display == "table-row") {
if ($p->_already_pushed) { return false; }
}
} while ($p = $p->get_parent());
// If the frame is absolute or 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();
// 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 && $p !== $this) {
$cbw = $p->get_containing_block("w");
$max_y += (float) $p->get_style()->computed_bottom_spacing($cbw);
$p = $p->get_parent();
}
// Check if $frame flows off the page
if (Helpers::lengthLessOrEqual($max_y, $this->bottom_page_edge)) {
// 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;
$pushed_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 ($iter->_already_pushed) {
$pushed_flg = true;
} elseif ($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;
$iter->_already_pushed = true;
$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;
$pushed_flg = false;
continue;
}
if ($pushed_flg) {
// The frame was already pushed, avoid breaking on a previous page
break;
}
$next = $iter->get_prev_sibling();
// Skip empty text nodes
while ($next && $next->is_text_node() && $next->get_node()->nodeValue === "") {
$next = $next->get_prev_sibling();
}
if ($next) {
Helpers::dompdf_debug("page-break", "following prev sibling.");
if ($next->is_table() && !$iter->is_table()) {
$this->_in_table++;
} elseif (!$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->_already_pushed === false) {
$iter = $iter->get_parent();
}
if ($iter) {
$iter->split(null, true);
$iter->_already_pushed = true;
} else {
return false;
}
} else {
$frame->split(null, true);
}
$this->_page_full = true;
$frame->_already_pushed = true;
return true;
}
//........................................................................
public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
{
// 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;
}
}

View File

@@ -0,0 +1,343 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameDecorator;
use Dompdf\Cellmap;
use DOMNode;
use Dompdf\Css\Style;
use Dompdf\Dompdf;
use Dompdf\Frame;
/**
* Decorates Frames for table layout
*
* @package dompdf
*/
class Table extends AbstractFrameDecorator
{
public const VALID_CHILDREN = Style::TABLE_INTERNAL_TYPES;
/**
* List of all row-group display types.
*/
public const ROW_GROUPS = [
"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 Cellmap
*/
protected $_cellmap;
/**
* Table header rows. Each table header is duplicated when a table
* spans pages.
*
* @var TableRowGroup[]
*/
protected $_headers;
/**
* Table footer rows. Each table footer is duplicated when a table
* spans pages.
*
* @var TableRowGroup[]
*/
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->_headers = [];
$this->_footers = [];
}
public function reset()
{
parent::reset();
$this->_cellmap->reset();
$this->_headers = [];
$this->_footers = [];
$this->_reflower->reset();
}
//........................................................................
/**
* Split the table at $row. $row and all subsequent rows will be
* added to the clone. This method is overridden in order to remove
* frames from the cellmap properly.
*/
public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
{
if (is_null($child)) {
parent::split($child, $page_break, $forced);
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, $page_break, $forced);
} elseif (in_array($child->get_style()->display, self::ROW_GROUPS, true)) {
// Individual rows should have already been handled
parent::split($child, $page_break, $forced);
} else {
$iter = $child;
while ($iter) {
$this->_cellmap->remove_row($iter);
$iter = $iter->get_next_sibling();
}
parent::split($child, $page_break, $forced);
}
}
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 Cellmap
*/
public function get_cellmap()
{
return $this->_cellmap;
}
//........................................................................
/**
* Check for text nodes between valid table children that only contain white
* space, except if white space is to be preserved.
*
* @param AbstractFrameDecorator $frame
*
* @return bool
*/
private function isEmptyTextNode(AbstractFrameDecorator $frame): bool
{
// This is based on the white-space pattern in `FrameReflower\Text`,
// i.e. only match on collapsible white space
$wsPattern = '/^[^\S\xA0\x{202F}\x{2007}]*$/u';
$validChildOrNull = function ($frame) {
return $frame === null
|| in_array($frame->get_style()->display, self::VALID_CHILDREN, true);
};
return $frame instanceof Text
&& !$frame->is_pre()
&& preg_match($wsPattern, $frame->get_text())
&& $validChildOrNull($frame->get_prev_sibling())
&& $validChildOrNull($frame->get_next_sibling());
}
/**
* Restructure tree so that the table has the correct structure. Misplaced
* children are appropriately wrapped in anonymous row groups, rows, and
* cells.
*
* https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
*/
public function normalize(): void
{
$column_caption = ["table-column-group", "table-column", "table-caption"];
$children = iterator_to_array($this->get_children());
$tbody = null;
foreach ($children as $child) {
$display = $child->get_style()->display;
if (in_array($display, self::ROW_GROUPS, true)) {
// Reset anonymous tbody
$tbody = null;
// Add headers and footers
if ($display === "table-header-group") {
$this->_headers[] = $child;
} elseif ($display === "table-footer-group") {
$this->_footers[] = $child;
}
continue;
}
if (in_array($display, $column_caption, true)) {
continue;
}
// Remove empty text nodes between valid children
if ($this->isEmptyTextNode($child)) {
$this->remove_child($child);
continue;
}
// Catch consecutive misplaced frames within a single anonymous group
if ($tbody === null) {
$tbody = $this->create_anonymous_child("tbody", "table-row-group");
$this->insert_child_before($tbody, $child);
}
$tbody->append_child($child);
}
// Handle empty table: Make sure there is at least one row group
if (!$this->get_first_child()) {
$tbody = $this->create_anonymous_child("tbody", "table-row-group");
$this->append_child($tbody);
}
foreach ($this->get_children() as $child) {
$display = $child->get_style()->display;
if (in_array($display, self::ROW_GROUPS, true)) {
$this->normalizeRowGroup($child);
}
}
}
private function normalizeRowGroup(AbstractFrameDecorator $frame): void
{
$children = iterator_to_array($frame->get_children());
$tr = null;
foreach ($children as $child) {
$display = $child->get_style()->display;
if ($display === "table-row") {
// Reset anonymous tr
$tr = null;
continue;
}
// Remove empty text nodes between valid children
if ($this->isEmptyTextNode($child)) {
$frame->remove_child($child);
continue;
}
// Catch consecutive misplaced frames within a single anonymous row
if ($tr === null) {
$tr = $frame->create_anonymous_child("tr", "table-row");
$frame->insert_child_before($tr, $child);
}
$tr->append_child($child);
}
// Handle empty row group: Make sure there is at least one row
if (!$frame->get_first_child()) {
$tr = $frame->create_anonymous_child("tr", "table-row");
$frame->append_child($tr);
}
foreach ($frame->get_children() as $child) {
$this->normalizeRow($child);
}
}
private function normalizeRow(AbstractFrameDecorator $frame): void
{
$children = iterator_to_array($frame->get_children());
$td = null;
foreach ($children as $child) {
$display = $child->get_style()->display;
if ($display === "table-cell") {
// Reset anonymous td
$td = null;
continue;
}
// Remove empty text nodes between valid children
if ($this->isEmptyTextNode($child)) {
$frame->remove_child($child);
continue;
}
// Catch consecutive misplaced frames within a single anonymous cell
if ($td === null) {
$td = $frame->create_anonymous_child("td", "table-cell");
$frame->insert_child_before($td, $child);
}
$td->append_child($child);
}
// Handle empty row: Make sure there is at least one cell
if (!$frame->get_first_child()) {
$td = $frame->create_anonymous_child("td", "table-cell");
$frame->append_child($td);
}
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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 = [];
$this->_content_height = 0;
}
//........................................................................
function reset()
{
parent::reset();
$this->_resolved_borders = [];
$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(
[
$style->margin_top,
$style->padding_top,
$style->border_top_width,
$style->border_bottom_width,
$style->padding_bottom,
$style->margin_bottom
],
(float)$style->length_in_pt($style->height)
);
$new_height = $height - $v_space;
$style->set_used("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;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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 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);
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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);
}
/**
* Split the row group at the given child and remove all subsequent child
* rows and all subsequent row groups from the cellmap.
*/
public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
{
if (is_null($child)) {
parent::split($child, $page_break, $forced);
return;
}
// Remove child & all subsequent rows from the cellmap
/** @var Table $parent */
$parent = $this->get_parent();
$cellmap = $parent->get_cellmap();
$iter = $child;
while ($iter) {
$cellmap->remove_row($iter);
$iter = $iter->get_next_sibling();
}
// Remove all subsequent row groups from the cellmap
$iter = $this->get_next_sibling();
while ($iter) {
$cellmap->remove_row_group($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(null, $page_break, $forced);
return;
}
$cellmap->update_row_group($this, $child->get_prev_sibling());
parent::split($child, $page_break, $forced);
}
}

View File

@@ -0,0 +1,191 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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;
/**
* Decorates Frame objects for text layout
*
* @package dompdf
*/
class Text extends AbstractFrameDecorator
{
/**
* @var float
*/
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 = 0.0;
}
function reset()
{
parent::reset();
$this->text_spacing = 0.0;
}
// Accessor methods
/**
* @return float
*/
public function get_text_spacing(): float
{
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 padding, border, and margin do not apply when determining the
* height for inline 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
*/
public function get_margin_height(): float
{
// This function is also called in add_frame_to_line() and is used to
// determine the line height
$style = $this->get_style();
$font = $style->font_family;
$size = $style->font_size;
$fontHeight = $this->_dompdf->getFontMetrics()->getFontHeight($font, $size);
return ($style->line_height / ($size > 0 ? $size : 1)) * $fontHeight;
}
public function get_padding_box(): array
{
$style = $this->_frame->get_style();
$pb = $this->_frame->get_padding_box();
$pb[3] = $pb["h"] = (float) $style->length_in_pt($style->height);
return $pb;
}
/**
* @param float $spacing
*/
public function set_text_spacing(float $spacing): void
{
$this->text_spacing = $spacing;
$this->recalculate_width();
}
/**
* Recalculate the text width
*
* @return float
*/
public function recalculate_width(): float
{
$fontMetrics = $this->_dompdf->getFontMetrics();
$style = $this->get_style();
$text = $this->get_text();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $this->text_spacing + $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$text_width = $fontMetrics->getTextWidth($text, $font, $size, $word_spacing, $letter_spacing);
$style->set_used("width", $text_width);
return $text_width;
}
// Text manipulation methods
/**
* Split the text in this frame at the offset specified. The remaining
* text is added as a sibling frame following this one and is returned.
*
* @param int $offset
* @return Frame|null
*/
function split_text($offset)
{
if ($offset == 0) {
return null;
}
$split = $this->_frame->get_node()->splitText($offset);
if ($split === false) {
return null;
}
$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 int $offset
* @param int $count
*/
function delete_text($offset, $count)
{
$this->_frame->get_node()->deleteData($offset, $count);
}
/**
* @param string $text
*/
function set_text($text)
{
$this->_frame->get_node()->data = $text;
}
}

View File

@@ -0,0 +1,705 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\Dompdf;
use Dompdf\Helpers;
use Dompdf\Frame;
use Dompdf\Frame\Factory;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameDecorator\Block;
/**
* 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 AbstractFrameDecorator
*/
protected $_frame;
/**
* Cached min/max child size
*
* @var array
*/
protected $_min_max_child_cache;
/**
* Cached min/max size
*
* @var array
*/
protected $_min_max_cache;
/**
* AbstractFrameReflower constructor.
* @param AbstractFrameDecorator $frame
*/
function __construct(AbstractFrameDecorator $frame)
{
$this->_frame = $frame;
$this->_min_max_child_cache = null;
$this->_min_max_cache = null;
}
/**
* @return Dompdf
*/
function get_dompdf()
{
return $this->_frame->get_dompdf();
}
public function reset(): void
{
$this->_min_max_child_cache = null;
$this->_min_max_cache = null;
}
/**
* Determine the actual containing block for absolute and fixed position.
*
* https://www.w3.org/TR/CSS21/visudet.html#containing-block-details
*/
protected function determine_absolute_containing_block(): void
{
$frame = $this->_frame;
$style = $frame->get_style();
switch ($style->position) {
case "absolute":
$parent = $frame->find_positioned_parent();
if ($parent !== $frame->get_root()) {
$parent_style = $parent->get_style();
$parent_padding_box = $parent->get_padding_box();
//FIXME: an accurate measure of the positioned parent height
// is not possible until reflow has completed;
// we'll fall back to the parent's containing block,
// which is wrong for auto-height parents
if ($parent_style->height === "auto") {
$parent_containing_block = $parent->get_containing_block();
$containing_block_height = $parent_containing_block["h"] -
(float)$parent_style->length_in_pt([
$parent_style->margin_top,
$parent_style->margin_bottom,
$parent_style->border_top_width,
$parent_style->border_bottom_width
], $parent_containing_block["w"]);
} else {
$containing_block_height = $parent_padding_box["h"];
}
$frame->set_containing_block($parent_padding_box["x"], $parent_padding_box["y"], $parent_padding_box["w"], $containing_block_height);
break;
}
case "fixed":
$initial_cb = $frame->get_root()->get_first_child()->get_containing_block();
$frame->set_containing_block($initial_cb["x"], $initial_cb["y"], $initial_cb["w"], $initial_cb["h"]);
break;
default:
// Nothing to do, containing block already set via parent
break;
}
}
/**
* Collapse frames margins
* http://www.w3.org/TR/CSS21/box.html#collapsing-margins
*/
protected function _collapse_margins(): void
{
$frame = $this->_frame;
// Margins of float/absolutely positioned/inline-level elements do not collapse
if (!$frame->is_in_flow() || $frame->is_inline_level()
|| $frame->get_root() === $frame || $frame->get_parent() === $frame->get_root()
) {
return;
}
$cb = $frame->get_containing_block();
$style = $frame->get_style();
$t = $style->length_in_pt($style->margin_top, $cb["w"]);
$b = $style->length_in_pt($style->margin_bottom, $cb["w"]);
// Handle 'auto' values
if ($t === "auto") {
$style->set_used("margin_top", 0.0);
$t = 0.0;
}
if ($b === "auto") {
$style->set_used("margin_bottom", 0.0);
$b = 0.0;
}
// Collapse vertical margins:
$n = $frame->get_next_sibling();
if ( $n && !($n->is_block_level() && $n->is_in_flow()) ) {
while ($n = $n->get_next_sibling()) {
if ($n->is_block_level() && $n->is_in_flow()) {
break;
}
if (!$n->get_first_child()) {
$n = null;
break;
}
}
}
if ($n) {
$n_style = $n->get_style();
$n_t = (float)$n_style->length_in_pt($n_style->margin_top, $cb["w"]);
$b = $this->get_collapsed_margin_length($b, $n_t);
$style->set_used("margin_bottom", $b);
$n_style->set_used("margin_top", 0.0);
}
// Collapse our first child's margin, if there is no border or padding
if ($style->border_top_width == 0 && $style->length_in_pt($style->padding_top) == 0) {
$f = $this->_frame->get_first_child();
if ( $f && !($f->is_block_level() && $f->is_in_flow()) ) {
while ($f = $f->get_next_sibling()) {
if ($f->is_block_level() && $f->is_in_flow()) {
break;
}
if (!$f->get_first_child()) {
$f = null;
break;
}
}
}
// Margins are collapsed only between block-level boxes
if ($f) {
$f_style = $f->get_style();
$f_t = (float)$f_style->length_in_pt($f_style->margin_top, $cb["w"]);
$t = $this->get_collapsed_margin_length($t, $f_t);
$style->set_used("margin_top", $t);
$f_style->set_used("margin_top", 0.0);
}
}
// Collapse our last child's margin, if there is no border or padding
if ($style->border_bottom_width == 0 && $style->length_in_pt($style->padding_bottom) == 0) {
$l = $this->_frame->get_last_child();
if ( $l && !($l->is_block_level() && $l->is_in_flow()) ) {
while ($l = $l->get_prev_sibling()) {
if ($l->is_block_level() && $l->is_in_flow()) {
break;
}
if (!$l->get_last_child()) {
$l = null;
break;
}
}
}
// Margins are collapsed only between block-level boxes
if ($l) {
$l_style = $l->get_style();
$l_b = (float)$l_style->length_in_pt($l_style->margin_bottom, $cb["w"]);
$b = $this->get_collapsed_margin_length($b, $l_b);
$style->set_used("margin_bottom", $b);
$l_style->set_used("margin_bottom", 0.0);
}
}
}
/**
* Get the combined (collapsed) length of two adjoining margins.
*
* See http://www.w3.org/TR/CSS21/box.html#collapsing-margins.
*
* @param float $l1
* @param float $l2
*
* @return float
*/
private function get_collapsed_margin_length(float $l1, float $l2): float
{
if ($l1 < 0 && $l2 < 0) {
return min($l1, $l2); // min(x, y) = - max(abs(x), abs(y)), if x < 0 && y < 0
}
if ($l1 < 0 || $l2 < 0) {
return $l1 + $l2; // x + y = x - abs(y), if y < 0
}
return max($l1, $l2);
}
/**
* Handle relative positioning according to
* https://www.w3.org/TR/CSS21/visuren.html#relative-positioning.
*
* @param AbstractFrameDecorator $frame The frame to handle.
*/
protected function position_relative(AbstractFrameDecorator $frame): void
{
$style = $frame->get_style();
if ($style->position === "relative") {
$cb = $frame->get_containing_block();
$top = $style->length_in_pt($style->top, $cb["h"]);
$right = $style->length_in_pt($style->right, $cb["w"]);
$bottom = $style->length_in_pt($style->bottom, $cb["h"]);
$left = $style->length_in_pt($style->left, $cb["w"]);
// FIXME RTL case:
// if ($left !== "auto" && $right !== "auto") $left = -$right;
if ($left === "auto" && $right === "auto") {
$left = 0;
} elseif ($left === "auto") {
$left = -$right;
}
if ($top === "auto" && $bottom === "auto") {
$top = 0;
} elseif ($top === "auto") {
$top = -$bottom;
}
$frame->move($left, $top);
}
}
/**
* @param Block|null $block
*/
abstract function reflow(Block $block = null);
/**
* Resolve the `min-width` property.
*
* Resolves to 0 if not set or if a percentage and the containing-block
* width is not defined.
*
* @param float|null $cbw Width of the containing block.
*
* @return float
*/
protected function resolve_min_width(?float $cbw): float
{
$style = $this->_frame->get_style();
$min_width = $style->min_width;
return $min_width !== "auto"
? $style->length_in_pt($min_width, $cbw ?? 0)
: 0.0;
}
/**
* Resolve the `max-width` property.
*
* Resolves to `INF` if not set or if a percentage and the containing-block
* width is not defined.
*
* @param float|null $cbw Width of the containing block.
*
* @return float
*/
protected function resolve_max_width(?float $cbw): float
{
$style = $this->_frame->get_style();
$max_width = $style->max_width;
return $max_width !== "none"
? $style->length_in_pt($max_width, $cbw ?? INF)
: INF;
}
/**
* Resolve the `min-height` property.
*
* Resolves to 0 if not set or if a percentage and the containing-block
* height is not defined.
*
* @param float|null $cbh Height of the containing block.
*
* @return float
*/
protected function resolve_min_height(?float $cbh): float
{
$style = $this->_frame->get_style();
$min_height = $style->min_height;
return $min_height !== "auto"
? $style->length_in_pt($min_height, $cbh ?? 0)
: 0.0;
}
/**
* Resolve the `max-height` property.
*
* Resolves to `INF` if not set or if a percentage and the containing-block
* height is not defined.
*
* @param float|null $cbh Height of the containing block.
*
* @return float
*/
protected function resolve_max_height(?float $cbh): float
{
$style = $this->_frame->get_style();
$max_height = $style->max_height;
return $max_height !== "none"
? $style->length_in_pt($style->max_height, $cbh ?? INF)
: INF;
}
/**
* Get the minimum and maximum preferred width of the contents of the frame,
* as requested by its children.
*
* @return array A two-element array of min and max width.
*/
public function get_min_max_child_width(): array
{
if (!is_null($this->_min_max_child_cache)) {
return $this->_min_max_child_cache;
}
$low = [];
$high = [];
for ($iter = $this->_frame->get_children(); $iter->valid(); $iter->next()) {
$inline_min = 0;
$inline_max = 0;
// Add all adjacent inline widths together to calculate max width
while ($iter->valid() && ($iter->current()->is_inline_level() || $iter->current()->get_style()->display === "-dompdf-image")) {
/** @var AbstractFrameDecorator */
$child = $iter->current();
$child->get_reflower()->_set_content();
$minmax = $child->get_min_max_width();
if (in_array($child->get_style()->white_space, ["pre", "nowrap"], true)) {
$inline_min += $minmax["min"];
} else {
$low[] = $minmax["min"];
}
$inline_max += $minmax["max"];
$iter->next();
}
if ($inline_min > 0) {
$low[] = $inline_min;
}
if ($inline_max > 0) {
$high[] = $inline_max;
}
// Skip children with absolute position
if ($iter->valid() && !$iter->current()->is_absolute()) {
/** @var AbstractFrameDecorator */
$child = $iter->current();
$child->get_reflower()->_set_content();
list($low[], $high[]) = $child->get_min_max_width();
}
}
$min = count($low) ? max($low) : 0;
$max = count($high) ? max($high) : 0;
return $this->_min_max_child_cache = [$min, $max];
}
/**
* Get the minimum and maximum preferred content-box width of the frame.
*
* @return array A two-element array of min and max width.
*/
public function get_min_max_content_width(): array
{
return $this->get_min_max_child_width();
}
/**
* Get the minimum and maximum preferred border-box width of the frame.
*
* Required for shrink-to-fit width calculation, as used in automatic table
* layout, absolute positioning, float and inline-block. This provides a
* basic implementation. Child classes should override this or
* `get_min_max_content_width` as necessary.
*
* @return array An array `[0 => min, 1 => max, "min" => min, "max" => max]`
* of min and max width.
*/
public function get_min_max_width(): array
{
if (!is_null($this->_min_max_cache)) {
return $this->_min_max_cache;
}
$style = $this->_frame->get_style();
[$min, $max] = $this->get_min_max_content_width();
// Account for margins, borders, and padding
$dims = [
$style->padding_left,
$style->padding_right,
$style->border_left_width,
$style->border_right_width,
$style->margin_left,
$style->margin_right
];
// The containing block is not defined yet, treat percentages as 0
$delta = (float) $style->length_in_pt($dims, 0);
$min += $delta;
$max += $delta;
return $this->_min_max_cache = [$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(["\\\n", '\\"', "\\'"],
["", '"', "'"], $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
*
* https://www.w3.org/TR/css-content-3/#quotes
*
* @return array An array of pairs of quotes
*/
protected function _parse_quotes(): array
{
$quotes = $this->_frame->get_style()->quotes;
if ($quotes === "none") {
return [];
}
if ($quotes === "auto") {
// TODO: Use typographically appropriate quotes for the current
// language here
return [['"', '"'], ["'", "'"]];
}
// Matches quote types
$re = '/(\'[^\']*\')|(\"[^\"]*\")/';
// Split on spaces, except within quotes
if (!preg_match_all($re, $quotes, $matches, PREG_SET_ORDER)) {
return [];
}
$quotes_array = [];
foreach ($matches as $_quote) {
$quotes_array[] = $this->_parse_string($_quote[0], true);
}
return array_chunk($quotes_array, 2);
}
/**
* Parses the CSS "content" property
*
* https://www.w3.org/TR/CSS21/generate.html#content
*
* @return string The resulting string
*/
protected function _parse_content(): string
{
$style = $this->_frame->get_style();
$content = $style->content;
if ($content === "normal" || $content === "none") {
return "";
}
$quotes = $this->_parse_quotes();
$text = "";
foreach ($content as $val) {
// String
if (in_array(mb_substr($val, 0, 1), ['"', "'"], true)) {
$text .= $this->_parse_string($val);
continue;
}
$val = mb_strtolower($val);
// Keywords
if ($val === "open-quote") {
// FIXME: Take quotation depth into account
if (isset($quotes[0][0])) {
$text .= $quotes[0][0];
}
continue;
} elseif ($val === "close-quote") {
// FIXME: Take quotation depth into account
if (isset($quotes[0][1])) {
$text .= $quotes[0][1];
}
continue;
} elseif ($val === "no-open-quote") {
// FIXME: Increment quotation depth
continue;
} elseif ($val === "no-close-quote") {
// FIXME: Decrement quotation depth
continue;
}
// attr()
if (mb_substr($val, 0, 5) === "attr(") {
$i = mb_strpos($val, ")");
if ($i === false) {
continue;
}
$attr = trim(mb_substr($val, 5, $i - 5));
if ($attr === "") {
continue;
}
$text .= $this->_frame->get_parent()->get_node()->getAttribute($attr);
continue;
}
// counter()/counters()
if (mb_substr($val, 0, 7) === "counter") {
// Handle counter() references:
// http://www.w3.org/TR/CSS21/generate.html#content
$i = mb_strpos($val, ")");
if ($i === false) {
continue;
}
preg_match('/(counters?)(^\()*?\(\s*([^\s,]+)\s*(,\s*["\']?([^"\'\)]*)["\']?\s*(,\s*([^\s)]+)\s*)?)?\)/i', $val, $args);
$counter_id = $args[3];
if (strtolower($args[1]) === "counter") {
// counter(name [,style])
if (isset($args[5])) {
$type = trim($args[5]);
} else {
$type = "decimal";
}
$p = $this->_frame->lookup_counter_frame($counter_id);
$text .= $p->counter_value($counter_id, $type);
} elseif (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 = "decimal";
}
$p = $this->_frame->lookup_counter_frame($counter_id);
$tmp = [];
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;
}
}
return $text;
}
/**
* Handle counters and set generated content if the frame is a
* generated-content frame.
*/
protected function _set_content(): void
{
$frame = $this->_frame;
if ($frame->content_set) {
return;
}
$style = $frame->get_style();
if (($reset = $style->counter_reset) !== "none") {
$frame->reset_counters($reset);
}
if (($increment = $style->counter_increment) !== "none") {
$frame->increment_counters($increment);
}
if ($frame->get_node()->nodeName === "dompdf_generated") {
$content = $this->_parse_content();
if ($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);
}
}
$frame->content_set = true;
}
}

View File

@@ -0,0 +1,948 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\FrameReflower;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
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;
use Dompdf\Helpers;
/**
* 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;
/**
* Frame for this reflower
*
* @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();
$absolute = $frame->is_absolute();
$cb = $frame->get_containing_block();
$w = $cb["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 = [$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 ($absolute) {
$dims[] = $left !== "auto" ? $left : 0;
$dims[] = $right !== "auto" ? $right : 0;
}
$sum = (float)$style->length_in_pt($dims, $w);
// Compare to the containing block
$diff = $w - $sum;
if ($absolute) {
// Absolutely positioned
// http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
if ($width === "auto" || $left === "auto" || $right === "auto") {
// "all of the three are 'auto'" logic + otherwise case
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
$block_parent = $frame->find_block_parent();
$parent_content = $block_parent->get_content_box();
$line = $block_parent->get_current_line_box();
// TODO: This is the in-flow inline position. Use the in-flow
// block position if the original display type is block-level
$inflow_x = $parent_content["x"] - $cb["x"] + $line->left + $line->w;
if ($width === "auto" && $left === "auto" && $right === "auto") {
// rule 3, per instruction preceding rule set
// shrink-to-fit width
$left = $inflow_x;
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff - $left), $max);
$right = $diff - $left - $width;
} elseif ($width === "auto" && $left === "auto") {
// rule 1
// shrink-to-fit width
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff), $max);
$left = $diff - $width;
} elseif ($width === "auto" && $right === "auto") {
// rule 3
// shrink-to-fit width
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff), $max);
$right = $diff - $width;
} elseif ($left === "auto" && $right === "auto") {
// rule 2
$left = $inflow_x;
$right = $diff - $left;
} elseif ($left === "auto") {
// rule 4
$left = $diff;
} elseif ($width === "auto") {
// rule 5
$width = max($diff, 0);
} else {
// $right === "auto"
// rule 6
$right = $diff;
}
} else {
// "none of the three are 'auto'" logic described in paragraph preceding the rules
if ($diff >= 0) {
if ($lm === "auto" && $rm === "auto") {
$lm = $rm = $diff / 2;
} elseif ($lm === "auto") {
$lm = $diff;
} elseif ($rm === "auto") {
$rm = $diff;
}
} else {
// over-constrained, solve for right
$right = $right + $diff;
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
}
}
} elseif ($style->float !== "none" || $style->display === "inline-block") {
// Shrink-to-fit width for float and inline block
// https://www.w3.org/TR/CSS21/visudet.html#float-width
// https://www.w3.org/TR/CSS21/visudet.html#inlineblock-width
if ($width === "auto") {
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff), $max);
}
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
} else {
// Block-level, normal flow
// https://www.w3.org/TR/CSS21/visudet.html#blockwidth
if ($diff >= 0) {
// Find auto properties and get them to take up the slack
if ($width === "auto") {
$width = $diff;
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
} elseif ($lm === "auto" && $rm === "auto") {
$lm = $rm = $diff / 2;
} elseif ($lm === "auto") {
$lm = $diff;
} elseif ($rm === "auto") {
$rm = $diff;
}
} else {
// We are over constrained--set margin-right to the difference
$rm = (float) $rm + $diff;
if ($width === "auto") {
$width = 0;
}
if ($lm === "auto") {
$lm = 0;
}
}
}
return [
"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 (!isset($cb["w"])) {
throw new Exception("Box property calculation requires containing block width");
}
$width = $style->length_in_pt($style->width, $cb["w"]);
$values = $this->_calculate_width($width);
$margin_left = $values["margin_left"];
$margin_right = $values["margin_right"];
$width = $values["width"];
$left = $values["left"];
$right = $values["right"];
// Handle min/max width
// https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
$min_width = $this->resolve_min_width($cb["w"]);
$max_width = $this->resolve_max_width($cb["w"]);
if ($width > $max_width) {
$values = $this->_calculate_width($max_width);
$margin_left = $values["margin_left"];
$margin_right = $values["margin_right"];
$width = $values["width"];
$left = $values["left"];
$right = $values["right"];
}
if ($width < $min_width) {
$values = $this->_calculate_width($min_width);
$margin_left = $values["margin_left"];
$margin_right = $values["margin_right"];
$width = $values["width"];
$left = $values["left"];
$right = $values["right"];
}
return [$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"]);
$margin_top = $style->length_in_pt($style->margin_top, $cb["w"]);
$margin_bottom = $style->length_in_pt($style->margin_bottom, $cb["w"]);
$top = $style->length_in_pt($style->top, $cb["h"]);
$bottom = $style->length_in_pt($style->bottom, $cb["h"]);
if ($frame->is_absolute()) {
// Absolutely positioned
// http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-height
$h_dims = [
$top !== "auto" ? $top : 0,
$height !== "auto" ? $height : 0,
$bottom !== "auto" ? $bottom : 0
];
$w_dims = [
$style->margin_top !== "auto" ? $style->margin_top : 0,
$style->padding_top,
$style->border_top_width,
$style->border_bottom_width,
$style->padding_bottom,
$style->margin_bottom !== "auto" ? $style->margin_bottom : 0
];
$sum = (float)$style->length_in_pt($h_dims, $cb["h"])
+ (float)$style->length_in_pt($w_dims, $cb["w"]);
$diff = $cb["h"] - $sum;
if ($height === "auto" || $top === "auto" || $bottom === "auto") {
// "all of the three are 'auto'" logic + otherwise case
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$block_parent = $frame->find_block_parent();
$current_line = $block_parent->get_current_line_box();
// TODO: This is the in-flow inline position. Use the in-flow
// block position if the original display type is block-level
$inflow_y = $current_line->y - $cb["y"];
if ($height === "auto" && $top === "auto" && $bottom === "auto") {
// rule 3, per instruction preceding rule set
$top = $inflow_y;
$height = $content_height;
$bottom = $diff - $top - $height;
} elseif ($height === "auto" && $top === "auto") {
// rule 1
$height = $content_height;
$top = $diff - $height;
} elseif ($height === "auto" && $bottom === "auto") {
// rule 3
$height = $content_height;
$bottom = $diff - $height;
} elseif ($top === "auto" && $bottom === "auto") {
// rule 2
$top = $inflow_y;
$bottom = $diff - $top;
} elseif ($top === "auto") {
// rule 4
$top = $diff;
} elseif ($height === "auto") {
// rule 5
$height = max($diff, 0);
} else {
// $bottom === "auto"
// rule 6
$bottom = $diff;
}
} else {
// "none of the three are 'auto'" logic described in paragraph preceding the rules
if ($diff >= 0) {
if ($margin_top === "auto" && $margin_bottom === "auto") {
$margin_top = $margin_bottom = $diff / 2;
} elseif ($margin_top === "auto") {
$margin_top = $diff;
} elseif ($margin_bottom === "auto") {
$margin_bottom = $diff;
}
} else {
// over-constrained, solve for bottom
$bottom = $bottom + $diff;
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
}
}
} else {
// https://www.w3.org/TR/CSS21/visudet.html#normal-block
// https://www.w3.org/TR/CSS21/visudet.html#block-root-margin
if ($height === "auto") {
$height = $content_height;
}
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
// Handle min/max height
// https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
$min_height = $this->resolve_min_height($cb["h"]);
$max_height = $this->resolve_max_height($cb["h"]);
$height = Helpers::clamp($height, $min_height, $max_height);
}
// TODO: Need to also take min/max height into account for absolute
// positioning, using similar logic to the `_calculate_width`/
// `calculate_restricted_width` split above. The non-absolute case
// can simply clamp height within min/max, as margins and offsets are
// not affected
return [$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);
$text_indent = (float)$style->length_in_pt($style->text_indent, $w);
switch ($style->text_align) {
default:
case "left":
foreach ($this->_frame->get_line_boxes() as $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
if ($line->left) {
foreach ($line->frames_to_align() as $frame) {
$frame->move($line->left, 0);
}
}
}
break;
case "right":
foreach ($this->_frame->get_line_boxes() as $i => $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
$indent = $i === 0 ? $text_indent : 0;
$dx = $width - $line->w - $line->right - $indent;
foreach ($line->frames_to_align() as $frame) {
$frame->move($dx, 0);
}
}
break;
case "justify":
// We justify all lines except the last one, unless the frame
// has been split, in which case the actual last line is part of
// the split-off frame
$lines = $this->_frame->get_line_boxes();
$last_line_index = $this->_frame->is_split ? null : count($lines) - 1;
foreach ($lines as $i => $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
if ($line->left) {
foreach ($line->frames_to_align() as $frame) {
$frame->move($line->left, 0);
}
}
if ($line->br || $i === $last_line_index) {
continue;
}
$frames = $line->get_frames();
$other_frame_count = 0;
foreach ($frames as $frame) {
if (!($frame instanceof TextFrameDecorator)) {
$other_frame_count++;
}
}
$word_count = $line->wc + $other_frame_count;
// Set the spacing for each child
if ($word_count > 1) {
$indent = $i === 0 ? $text_indent : 0;
$spacing = ($width - $line->get_width() - $indent) / ($word_count - 1);
} else {
$spacing = 0;
}
$dx = 0;
foreach ($frames as $frame) {
if ($frame instanceof TextFrameDecorator) {
$text = $frame->get_text();
$spaces = mb_substr_count($text, " ");
$frame->move($dx, 0);
$frame->set_text_spacing($spacing);
$dx += $spaces * $spacing;
} else {
$frame->move($dx, 0);
}
}
// The line (should) now occupy the entire width
$line->w = $width;
}
break;
case "center":
case "centre":
foreach ($this->_frame->get_line_boxes() as $i => $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
$indent = $i === 0 ? $text_indent : 0;
$dx = ($width + $line->left - $line->w - $line->right - $indent) / 2;
foreach ($line->frames_to_align() as $frame) {
$frame->move($dx, 0);
}
}
break;
}
}
/**
* Align inline children vertically.
* Aligns each child vertically after each line is reflowed
*/
function vertical_align()
{
$fontMetrics = $this->get_dompdf()->getFontMetrics();
foreach ($this->_frame->get_line_boxes() as $line) {
$height = $line->h;
// Move all markers to the top of the line box
foreach ($line->get_list_markers() as $marker) {
$x = $marker->get_position("x");
$marker->set_position($x, $line->y);
}
foreach ($line->frames_to_align() as $frame) {
$style = $frame->get_style();
$isInlineBlock = $style->display !== "inline"
&& $style->display !== "-dompdf-list-bullet";
$baseline = $fontMetrics->getFontBaseline($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) {
// Workaround: Skip vertical alignment if the frame is the
// only one one the line, excluding empty text frames, which
// may be the result of trailing white space
// FIXME: This special case should be removed once vertical
// alignment is properly fixed
$skip = true;
foreach ($line->get_frames() as $other) {
if ($other !== $frame
&& !($other->is_text_node() && $other->get_node()->nodeValue === "")
) {
$skip = false;
break;
}
}
if ($skip) {
continue;
}
$marginHeight = $frame->get_margin_height();
$imageHeightDiff = $height * 0.8 - $marginHeight;
$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 - $style->line_height;
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) - $marginHeight;
}
} 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 AbstractFrameDecorator $child
*/
function process_clear(AbstractFrameDecorator $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 AbstractFrameDecorator $child
* @param float $cb_x
* @param float $cb_w
*/
function process_float(AbstractFrameDecorator $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
*/
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;
}
$this->determine_absolute_containing_block();
// Counters and generated content
$this->_set_content();
// Inherit any dangling list markers
if ($block && $this->_frame->is_in_flow()) {
$this->_frame->inherit_dangling_markers($block);
}
// Collapse margins if required
$this->_collapse_margins();
$style = $this->_frame->get_style();
$cb = $this->_frame->get_containing_block();
// Determine the constraints imposed by this frame: calculate the width
// of the content area:
[$width, $margin_left, $margin_right, $left, $right] = $this->_calculate_restricted_width();
// Store the calculated properties
$style->set_used("width", $width);
$style->set_used("margin_left", $margin_left);
$style->set_used("margin_right", $margin_right);
$style->set_used("left", $left);
$style->set_used("right", $right);
$margin_top = $style->length_in_pt($style->margin_top, $cb["w"]);
$margin_bottom = $style->length_in_pt($style->margin_bottom, $cb["w"]);
$auto_top = $style->top === "auto";
$auto_margin_top = $margin_top === "auto";
// Update the position
$this->_frame->position();
[$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([
$margin_top !== "auto" ? $margin_top : 0,
$style->border_top_width,
$style->padding_top
], $cb["w"]);
$bottom = (float)$style->length_in_pt([
$margin_bottom !== "auto" ? $margin_bottom : 0,
$style->border_bottom_width,
$style->padding_bottom
], $cb["w"]);
$cb_x = $x + (float)$margin_left + (float)$style->length_in_pt([$style->border_left_width,
$style->padding_left], $cb["w"]);
$cb_y = $y + $top;
$height = $style->length_in_pt($style->height, $cb["h"]);
if ($height === "auto") {
$height = ($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) {
$child->set_containing_block($cb_x, $cb_y, $width, $height);
$this->process_clear($child);
$child->reflow($this->_frame);
// Check for a page break before the child
$page->check_page_break($child);
// Don't add the child to the line if a page break has occurred
// before it (possibly via a descendant), in which case it has been
// reset, including its position
if ($page->is_full() && $child->get_position("x") === null) {
break;
}
$this->process_float($child, $cb_x, $width);
}
// Stop reflow if a page break has occurred before the frame, in which
// case it has been reset, including its position
if ($page->is_full() && $this->_frame->get_position("x") === null) {
return;
}
// Determine our height
[$height, $margin_top, $margin_bottom, $top, $bottom] = $this->_calculate_restricted_height();
$style->set_used("height", $height);
$style->set_used("margin_top", $margin_top);
$style->set_used("margin_bottom", $margin_bottom);
$style->set_used("top", $top);
$style->set_used("bottom", $bottom);
if ($this->_frame->is_absolute()) {
if ($auto_top) {
$this->_frame->move(0, $top);
}
if ($auto_margin_top) {
$this->_frame->move(0, $margin_top, true);
}
}
$this->_text_align();
$this->vertical_align();
// Handle relative positioning
foreach ($this->_frame->get_children() as $child) {
$this->position_relative($child);
}
if ($block && $this->_frame->is_in_flow()) {
$block->add_frame_to_line($this->_frame);
if ($this->_frame->is_block_level()) {
$block->add_line();
}
}
}
public function get_min_max_content_width(): array
{
// TODO: While the containing block is not set yet on the frame, it can
// already be determined in some cases due to fixed dimensions on the
// ancestor forming the containing block. In such cases, percentage
// values could be resolved here
$style = $this->_frame->get_style();
$width = $style->width;
$fixed_width = $width !== "auto" && !Helpers::is_percent($width);
// If the frame has a specified width, then we don't need to check
// its children
if ($fixed_width) {
$min = (float) $style->length_in_pt($width, 0);
$max = $min;
} else {
[$min, $max] = $this->get_min_max_child_width();
}
// Handle min/max width style properties
$min_width = $this->resolve_min_width(null);
$max_width = $this->resolve_max_width(null);
$min = Helpers::clamp($min, $min_width, $max_width);
$max = Helpers::clamp($max, $min_width, $max_width);
return [$min, $max];
}
}

View File

@@ -0,0 +1,213 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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->determine_absolute_containing_block();
// Counters and generated content
$this->_set_content();
//FLOAT
//$frame = $this->_frame;
//$page = $frame->get_root();
//if ($frame->get_style()->float !== "none" ) {
// $page->add_floating_frame($this);
//}
$this->resolve_dimensions();
$this->resolve_margins();
$frame = $this->_frame;
$frame->position();
if ($block && $frame->is_in_flow()) {
$block->add_frame_to_line($frame);
}
}
public function get_min_max_content_width(): array
{
// TODO: While the containing block is not set yet on the frame, it can
// already be determined in some cases due to fixed dimensions on the
// ancestor forming the containing block. In such cases, percentage
// values could be resolved here
$style = $this->_frame->get_style();
[$width] = $this->calculate_size(null, null);
$min_width = $this->resolve_min_width(null);
$percent_width = Helpers::is_percent($style->width)
|| Helpers::is_percent($style->max_width)
|| ($style->width === "auto"
&& (Helpers::is_percent($style->height) || Helpers::is_percent($style->max_height)));
// Use the specified min width as minimum when width or max width depend
// on the containing block and cannot be resolved yet. This mimics
// browser behavior
$min = $percent_width ? $min_width : $width;
$max = $width;
return [$min, $max];
}
/**
* Calculate width and height, accounting for min/max constraints.
*
* * https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
* * https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
* * https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
* * https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
*
* @param float|null $cbw Width of the containing block.
* @param float|null $cbh Height of the containing block.
*
* @return float[]
*/
protected function calculate_size(?float $cbw, ?float $cbh): array
{
/** @var ImageFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
$computed_width = $style->width;
$computed_height = $style->height;
$width = $cbw === null && Helpers::is_percent($computed_width)
? "auto"
: $style->length_in_pt($computed_width, $cbw ?? 0);
$height = $cbh === null && Helpers::is_percent($computed_height)
? "auto"
: $style->length_in_pt($computed_height, $cbh ?? 0);
$min_width = $this->resolve_min_width($cbw);
$max_width = $this->resolve_max_width($cbw);
$min_height = $this->resolve_min_height($cbh);
$max_height = $this->resolve_max_height($cbh);
if ($width === "auto" && $height === "auto") {
// Use intrinsic dimensions, resampled to pt
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
$w = $frame->resample($img_width);
$h = $frame->resample($img_height);
// Resolve min/max constraints according to the constraint-violation
// table in https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
$max_width = max($min_width, $max_width);
$max_height = max($min_height, $max_height);
if (($w > $max_width && $h <= $max_height)
|| ($w > $max_width && $h > $max_height && $max_width / $w <= $max_height / $h)
|| ($w < $min_width && $h > $min_height)
|| ($w < $min_width && $h < $min_height && $min_width / $w > $min_height / $h)
) {
$width = Helpers::clamp($w, $min_width, $max_width);
$height = $width * ($img_height / $img_width);
$height = Helpers::clamp($height, $min_height, $max_height);
} else {
$height = Helpers::clamp($h, $min_height, $max_height);
$width = $height * ($img_width / $img_height);
$width = Helpers::clamp($width, $min_width, $max_width);
}
} elseif ($height === "auto") {
// Width is fixed, scale height according to aspect ratio
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
$width = Helpers::clamp((float) $width, $min_width, $max_width);
$height = $width * ($img_height / $img_width);
$height = Helpers::clamp($height, $min_height, $max_height);
} elseif ($width === "auto") {
// Height is fixed, scale width according to aspect ratio
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
$height = Helpers::clamp((float) $height, $min_height, $max_height);
$width = $height * ($img_width / $img_height);
$width = Helpers::clamp($width, $min_width, $max_width);
} else {
// Width and height are fixed
$width = Helpers::clamp((float) $width, $min_width, $max_width);
$height = Helpers::clamp((float) $height, $min_height, $max_height);
}
return [$width, $height];
}
protected function resolve_dimensions(): void
{
/** @var ImageFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
$debug_png = $this->get_dompdf()->getOptions()->getDebugPng();
if ($debug_png) {
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
print "resolve_dimensions() " .
$frame->get_style()->width . " " .
$frame->get_style()->height . ";" .
$frame->get_parent()->get_style()->width . " " .
$frame->get_parent()->get_style()->height . ";" .
$frame->get_parent()->get_parent()->get_style()->width . " " .
$frame->get_parent()->get_parent()->get_style()->height . ";" .
$img_width . " " .
$img_height . "|";
}
[, , $cbw, $cbh] = $frame->get_containing_block();
[$width, $height] = $this->calculate_size($cbw, $cbh);
if ($debug_png) {
print $width . " " . $height . ";";
}
$style->set_used("width", $width);
$style->set_used("height", $height);
}
protected function resolve_margins(): void
{
// Only handle the inline case for now
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
$style = $this->_frame->get_style();
if ($style->margin_left === "auto") {
$style->set_used("margin_left", 0.0);
}
if ($style->margin_right === "auto") {
$style->set_used("margin_right", 0.0);
}
if ($style->margin_top === "auto") {
$style->set_used("margin_top", 0.0);
}
if ($style->margin_bottom === "auto") {
$style->set_used("margin_bottom", 0.0);
}
}
}

View File

@@ -0,0 +1,188 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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\Inline as InlineFrameDecorator;
use Dompdf\FrameDecorator\Text as TextFrameDecorator;
/**
* Reflows inline frames
*
* @package dompdf
*/
class Inline extends AbstractFrameReflower
{
/**
* Inline constructor.
* @param InlineFrameDecorator $frame
*/
function __construct(InlineFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* Handle reflow of empty inline frames.
*
* Regular inline frames are positioned together with their text (or inline)
* children after child reflow. Empty inline frames have no children that
* could determine the positioning, so they need to be handled separately.
*
* @param BlockFrameDecorator $block
*/
protected function reflow_empty(BlockFrameDecorator $block): void
{
/** @var InlineFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
// Resolve width, so the margin width can be checked
$style->set_used("width", 0.0);
$cb = $frame->get_containing_block();
$line = $block->get_current_line_box();
$width = $frame->get_margin_width();
if ($width > ($cb["w"] - $line->left - $line->w - $line->right)) {
$block->add_line();
// Find the appropriate inline ancestor to split
$child = $frame;
$p = $child->get_parent();
while ($p instanceof InlineFrameDecorator && !$child->get_prev_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if ($p instanceof InlineFrameDecorator) {
// Split parent and stop current reflow. Reflow continues
// via child-reflow loop of split parent
$p->split($child);
return;
}
}
$frame->position();
$block->add_frame_to_line($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
/** @var InlineFrameDecorator */
$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;
}
// Counters and generated content
$this->_set_content();
$style = $frame->get_style();
// Resolve auto margins
// https://www.w3.org/TR/CSS21/visudet.html#inline-width
// https://www.w3.org/TR/CSS21/visudet.html#inline-non-replaced
if ($style->margin_left === "auto") {
$style->set_used("margin_left", 0.0);
}
if ($style->margin_right === "auto") {
$style->set_used("margin_right", 0.0);
}
if ($style->margin_top === "auto") {
$style->set_used("margin_top", 0.0);
}
if ($style->margin_bottom === "auto") {
$style->set_used("margin_bottom", 0.0);
}
// Handle line breaks
if ($frame->get_node()->nodeName === "br") {
if ($block) {
$line = $block->get_current_line_box();
$frame->set_containing_line($line);
$block->maximize_line_height($frame->get_margin_height(), $frame);
$block->add_line(true);
$next = $frame->get_next_sibling();
$p = $frame->get_parent();
if ($next && $p instanceof InlineFrameDecorator) {
$p->split($next);
}
}
return;
}
// Handle empty inline frames
if (!$frame->get_first_child()) {
if ($block) {
$this->reflow_empty($block);
}
return;
}
// 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_width = $style->border_left_width;
$f_style->border_left_style = $style->border_left_style;
$f_style->border_left_color = $style->border_left_color;
}
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_width = $style->border_right_width;
$l_style->border_right_style = $style->border_right_style;
$l_style->border_right_color = $style->border_right_color;
}
$cb = $frame->get_containing_block();
// 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);
// Stop reflow if the frame has been reset by a line or page break
// due to child reflow
if (!$frame->content_set) {
return;
}
}
if (!$frame->get_first_child()) {
return;
}
// Assume the position of the first child
[$x, $y] = $frame->get_first_child()->get_position();
$frame->set_position($x, $y);
// Handle relative positioning
foreach ($frame->get_children() as $child) {
$this->position_relative($child);
}
if ($block) {
$block->add_frame_to_line($frame);
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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\ListBullet as ListBulletFrameDecorator;
/**
* Reflows list bullets
*
* @package dompdf
*/
class ListBullet extends AbstractFrameReflower
{
/**
* ListBullet constructor.
* @param ListBulletFrameDecorator $frame
*/
function __construct(ListBulletFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
if ($block === null) {
return;
}
/** @var ListBulletFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
$style->set_used("width", $frame->get_width());
$frame->position();
if ($style->list_style_position === "inside") {
$block->add_frame_to_line($frame);
} else {
$block->add_dangling_marker($frame);
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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;
}
}

View File

@@ -0,0 +1,199 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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 PageFrameDecorator $frame
* @param int $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);
}
$frame->calculate_bottom_page_edge();
}
/**
* Paged layout:
* http://www.w3.org/TR/CSS21/page.html
*
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
/** @var PageFrameDecorator $frame */
$frame = $this->_frame;
$child = $frame->get_first_child();
$fixed_children = [];
$prev_child = null;
$current_page = 0;
while ($child) {
$this->apply_page_style($frame, $current_page + 1);
$style = $frame->get_style();
// Pages are only concerned with margins
$cb = $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) {
foreach ($child->get_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
$frame->get_renderer()->render($child);
// Check for end render callback
$this->_check_callbacks("end_page_render", $child);
if ($next_child) {
$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(string $event, Frame $frame): void
{
if (!isset($this->_callbacks)) {
$dompdf = $this->get_dompdf();
$this->_callbacks = $dompdf->getCallbacks();
$this->_canvas = $dompdf->getCanvas();
}
if (isset($this->_callbacks[$event])) {
$fs = $this->_callbacks[$event];
$canvas = $this->_canvas;
$fontMetrics = $this->get_dompdf()->getFontMetrics();
foreach ($fs as $f) {
$f($frame, $canvas, $fontMetrics);
}
}
}
}

View File

@@ -0,0 +1,523 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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\Helpers;
/**
* Reflows tables
*
* @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
*/
public function reset(): void
{
parent::reset();
$this->_state = 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
$delta = $this->_state["width_delta"];
$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 (excluding borders and
// padding)
$cb = $this->_frame->get_containing_block();
$columns =& $this->_frame->get_cellmap()->get_columns();
$width = $style->width;
$min_table_width = $this->resolve_min_width($cb["w"]) - $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;
} elseif ($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->set_used("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 ($columns as $i => $col) {
$cellmap->set_column_width($i, $col["max-width"]);
}
return;
}
// Determine leftover and assign it evenly to all columns
if ($width > $min_width) {
// We have three cases to deal with:
//
// 1. All columns are auto or absolute width. In this case we
// distribute extra space across all auto columns weighted by the
// difference between their max and min width, or by max width only
// if the width of the table is larger than the max width for all
// columns.
//
// 2. Only absolute widths have been specified, no auto columns. In
// this case we distribute extra space across all columns weighted
// by their absolute width.
//
// 3. Percentage widths have been specified. In this case we normalize
// the percentage values and try to assign widths as fractions of
// the table width. Absolute column widths are fully satisfied and
// any remaining space is evenly distributed among all auto columns.
// Case 1:
if ($percent_used == 0 && count($auto)) {
foreach ($absolute as $i) {
$w = $columns[$i]["min-width"];
$cellmap->set_column_width($i, $w);
}
if ($width < $max_width) {
$increment = $width - $min_width;
$table_delta = $max_width - $min_width;
foreach ($auto as $i) {
$min = $columns[$i]["min-width"];
$max = $columns[$i]["max-width"];
$col_delta = $max - $min;
$w = $min + $increment * ($col_delta / $table_delta);
$cellmap->set_column_width($i, $w);
}
} else {
$increment = $width - $max_width;
$auto_max = $max_width - $absolute_used;
foreach ($auto as $i) {
$max = $columns[$i]["max-width"];
$f = $auto_max > 0 ? $max / $auto_max : 1 / count($auto);
$w = $max + $increment * $f;
$cellmap->set_column_width($i, $w);
}
}
return;
}
// Case 2:
if ($percent_used == 0 && !count($auto)) {
$increment = $width - $absolute_used;
foreach ($absolute as $i) {
$abs = $columns[$i]["min-width"];
$f = $absolute_used > 0 ? $abs / $absolute_used : 1 / count($absolute);
$w = $abs + $increment * $f;
$cellmap->set_column_width($i, $w);
}
return;
}
// Case 3:
if ($percent_used > 0) {
// 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;
}
// Account for the minimum space used by the unassigned auto
// columns, by the columns with absolute widths, and the
// percentage columns following the current one
$used_width = $auto_min + $absolute_used;
foreach ($absolute as $i) {
$w = $columns[$i]["min-width"];
$cellmap->set_column_width($i, $w);
}
$percent_min = 0;
foreach ($percent as $i) {
$percent_min += $columns[$i]["min-width"];
}
// First-come, first served
foreach ($percent as $i) {
$min = $columns[$i]["min-width"];
$percent_min -= $min;
$slack = $width - $used_width - $percent_min;
$columns[$i]["percent"] *= $scale;
$w = min($columns[$i]["percent"] * $width / 100, $slack);
if ($w < $min) {
$w = $min;
}
$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) {
$w = $columns[$i]["min-width"] + $increment;
$cellmap->set_column_width($i, $w);
}
}
return;
}
} else {
// We are over-constrained:
// Each column gets its minimum width
foreach ($columns as $i => $col) {
$cellmap->set_column_width($i, $col["min-width"]);
}
}
}
/**
* Determine the frame's height based on min/max height
*
* @return float
*/
protected function _calculate_height()
{
$frame = $this->_frame;
$style = $frame->get_style();
$cb = $frame->get_containing_block();
$height = $style->length_in_pt($style->height, $cb["h"]);
$cellmap = $frame->get_cellmap();
$cellmap->assign_frame_heights();
$rows = $cellmap->get_rows();
// Determine our content height
$content_height = 0.0;
foreach ($rows as $r) {
$content_height += $r["height"];
}
if ($height === "auto") {
$height = $content_height;
}
// Handle min/max height
// https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
$min_height = $this->resolve_min_height($cb["h"]);
$max_height = $this->resolve_max_height($cb["h"]);
$height = Helpers::clamp($height, $min_height, $max_height);
// Use the content height or the height value, whichever is greater
if ($height <= $content_height) {
$height = $content_height;
} else {
// FIXME: Borders and row positions are not properly updated by this
// $cellmap->set_frame_heights($height, $content_height);
}
return $height;
}
/**
* @param BlockFrameDecorator|null $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();
$this->determine_absolute_containing_block();
// Counters and generated content
$this->_set_content();
// Collapse vertical margins, if required
$this->_collapse_margins();
// 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") {
[$h, $v] = $style->border_spacing;
$v = $v / 2;
$h = $h / 2;
$style->set_used("padding_left", (float)$style->length_in_pt($style->padding_left, $cb["w"]) + $h);
$style->set_used("padding_right", (float)$style->length_in_pt($style->padding_right, $cb["w"]) + $h);
$style->set_used("padding_top", (float)$style->length_in_pt($style->padding_top, $cb["w"]) + $v);
$style->set_used("padding_bottom", (float)$style->length_in_pt($style->padding_bottom, $cb["w"]) + $v);
}
$this->_assign_widths();
// Adjust left & right margins, if they are auto
$delta = $this->_state["width_delta"];
$width = $style->width;
$left = $style->length_in_pt($style->margin_left, $cb["w"]);
$right = $style->length_in_pt($style->margin_right, $cb["w"]);
$diff = (float) $cb["w"] - (float) $width - $delta;
if ($left === "auto" && $right === "auto") {
if ($diff < 0) {
$left = 0;
$right = $diff;
} else {
$left = $right = $diff / 2;
}
} else {
if ($left === "auto") {
$left = max($diff - $right, 0);
}
if ($right === "auto") {
$right = max($diff - $left, 0);
}
}
$style->set_used("margin_left", $left);
$style->set_used("margin_right", $right);
$frame->position();
[$x, $y] = $frame->get_position();
// Determine the content edge
$offset_x = (float)$left + (float)$style->length_in_pt([
$style->padding_left,
$style->border_left_width
], $cb["w"]);
$offset_y = (float)$style->length_in_pt([
$style->margin_top,
$style->border_top_width,
$style->padding_top
], $cb["w"]);
$content_x = $x + $offset_x;
$content_y = $y + $offset_y;
if (isset($cb["h"])) {
$h = $cb["h"];
} else {
$h = null;
}
$cellmap = $frame->get_cellmap();
$col =& $cellmap->get_column(0);
$col["x"] = $offset_x;
$row =& $cellmap->get_row(0);
$row["y"] = $offset_y;
$cellmap->assign_x_positions();
// Set the containing block of each child & reflow
foreach ($frame->get_children() as $child) {
$child->set_containing_block($content_x, $content_y, $width, $h);
$child->reflow();
if (!$page->in_nested_table()) {
// Check if a split has occurred
$page->check_page_break($child);
if ($page->is_full()) {
break;
}
}
}
// Stop reflow if a page break has occurred before the frame, in which
// case it has been reset, including its position
if ($page->is_full() && $frame->get_position("x") === null) {
$page->table_reflow_end();
return;
}
// Assign heights to our cells:
$style->set_used("height", $this->_calculate_height());
$page->table_reflow_end();
if ($block && $frame->is_in_flow()) {
$block->add_frame_to_line($frame);
if ($frame->is_block_level()) {
$block->add_line();
}
}
}
public function get_min_max_width(): array
{
if (!is_null($this->_min_max_cache)) {
return $this->_min_max_cache;
}
$style = $this->_frame->get_style();
$cellmap = $this->_frame->get_cellmap();
$this->_frame->normalize();
// Add the cells to the cellmap (this will calculate column widths as
// frames are added)
$cellmap->add_frame($this->_frame);
// Find the min/max width of the table and sort the columns into
// absolute/percent/auto arrays
$this->_state = [];
$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"] = [];
$this->_state["percent"] = [];
$this->_state["auto"] = [];
$columns =& $cellmap->get_columns();
foreach ($columns as $i => $col) {
$this->_state["min_width"] += $col["min-width"];
$this->_state["max_width"] += $col["max-width"];
if ($col["absolute"] > 0) {
$this->_state["absolute"][] = $i;
$this->_state["absolute_used"] += $col["min-width"];
} elseif ($col["percent"] > 0) {
$this->_state["percent"][] = $i;
$this->_state["percent_used"] += $col["percent"];
} else {
$this->_state["auto"][] = $i;
$this->_state["auto_min"] += $col["min-width"];
}
}
// Account for margins, borders, padding, and border spacing
$cb_w = $this->_frame->get_containing_block("w");
$lm = (float) $style->length_in_pt($style->margin_left, $cb_w);
$rm = (float) $style->length_in_pt($style->margin_right, $cb_w);
$dims = [
$style->border_left_width,
$style->border_right_width,
$style->padding_left,
$style->padding_right
];
if ($style->border_collapse !== "collapse") {
list($dims[]) = $style->border_spacing;
}
$delta = (float) $style->length_in_pt($dims, $cb_w);
$this->_state["width_delta"] = $delta;
$min_width = $this->_state["min_width"] + $delta + $lm + $rm;
$max_width = $this->_state["max_width"] + $delta + $lm + $rm;
return $this->_min_max_cache = [
$min_width,
$max_width,
"min" => $min_width,
"max" => $max_width
];
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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\Helpers;
/**
* 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)
{
// Counters and generated content
$this->_set_content();
$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([$style->margin_left,
$style->padding_left,
$style->border_left_width],
$w);
$right_space = (float)$style->length_in_pt([$style->padding_right,
$style->margin_right,
$style->border_right_width],
$w);
$top_space = (float)$style->length_in_pt([$style->margin_top,
$style->padding_top,
$style->border_top_width],
$h);
$bottom_space = (float)$style->length_in_pt([$style->margin_bottom,
$style->padding_bottom,
$style->border_bottom_width],
$h);
$cb_w = $w - $left_space - $right_space;
$style->set_used("width", $cb_w);
$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) {
$child->set_containing_block($content_x, $content_y, $cb_w, $h);
$this->process_clear($child);
$child->reflow($this->_frame);
$this->process_float($child, $content_x, $cb_w);
if ($page->is_full()) {
break;
}
}
// Determine our height
$style_height = (float)$style->length_in_pt($style->height, $h);
/** @var FrameDecorator\TableCell */
$frame = $this->_frame;
$frame->set_content_height($this->_calculate_content_height());
$height = max($style_height, (float)$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->set_used("height", $height);
$this->_text_align();
$this->vertical_align();
// Handle relative positioning
foreach ($this->_frame->get_children() as $child) {
$this->position_relative($child);
}
}
public function get_min_max_content_width(): array
{
// Ignore percentage values for a specified width here, as they are
// relative to the table width, which is not determined yet
$style = $this->_frame->get_style();
$width = $style->width;
$fixed_width = $width !== "auto" && !Helpers::is_percent($width);
[$min, $max] = $this->get_min_max_child_width();
// For table cells: Use specified width if it is greater than the
// minimum defined by the content
if ($fixed_width) {
$width = (float) $style->length_in_pt($width, 0);
$min = max($width, $min);
$max = $min;
}
// Handle min/max width style properties
$min_width = $this->resolve_min_width(null);
$max_width = $this->resolve_max_width(null);
$min = Helpers::clamp($min, $min_width, $max_width);
$max = Helpers::clamp($max, $min_width, $max_width);
return [$min, $max];
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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)
{
/** @var TableRowFrameDecorator */
$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;
}
// Counters and generated content
$this->_set_content();
$this->_frame->position();
$style = $this->_frame->get_style();
$cb = $this->_frame->get_containing_block();
foreach ($this->_frame->get_children() as $child) {
$child->set_containing_block($cb);
$child->reflow();
if ($page->is_full()) {
break;
}
}
if ($page->is_full()) {
return;
}
$table = TableFrameDecorator::find_parent_table($this->_frame);
$cellmap = $table->get_cellmap();
$style->set_used("width", $cellmap->get_frame_width($this->_frame));
$style->set_used("height", $cellmap->get_frame_height($this->_frame));
$this->_frame->set_position($cellmap->get_frame_position($this->_frame));
}
/**
* @throws Exception
*/
public function get_min_max_width(): array
{
throw new Exception("Min/max width is undefined for table rows");
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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\TableRowGroup as TableRowGroupFrameDecorator;
/**
* Reflows table row groups (e.g. tbody tags)
*
* @package dompdf
*/
class TableRowGroup extends AbstractFrameReflower
{
/**
* TableRowGroup constructor.
* @param TableRowGroupFrameDecorator $frame
*/
function __construct(TableRowGroupFrameDecorator $frame)
{
parent::__construct($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
/** @var TableRowGroupFrameDecorator */
$frame = $this->_frame;
$page = $frame->get_root();
// Counters and generated content
$this->_set_content();
$style = $frame->get_style();
$cb = $frame->get_containing_block();
foreach ($frame->get_children() as $child) {
$child->set_containing_block($cb["x"], $cb["y"], $cb["w"], $cb["h"]);
$child->reflow();
// Check if a split has occurred
$page->check_page_break($child);
if ($page->is_full()) {
break;
}
}
$table = TableFrameDecorator::find_parent_table($frame);
$cellmap = $table->get_cellmap();
// Stop reflow if a page break has occurred before the frame, in which
// case it is not part of its parent table's cell map yet
if ($page->is_full() && !$cellmap->frame_exists_in_cellmap($frame)) {
return;
}
$style->set_used("width", $cellmap->get_frame_width($frame));
$style->set_used("height", $cellmap->get_frame_height($frame));
$frame->set_position($cellmap->get_frame_position($frame));
}
}

View File

@@ -0,0 +1,611 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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\Inline as InlineFrameDecorator;
use Dompdf\FrameDecorator\Text as TextFrameDecorator;
use Dompdf\FontMetrics;
use Dompdf\Helpers;
/**
* Reflows text frames.
*
* @package dompdf
*/
class Text extends AbstractFrameReflower
{
/**
* PHP string representation of HTML entity <shy>
*/
const SOFT_HYPHEN = "\xC2\xAD";
/**
* The regex splits on everything that's a separator (^\S double negative),
* excluding the following non-breaking space characters:
* * nbsp (\xA0)
* * narrow nbsp (\x{202F})
* * figure space (\x{2007})
*/
public static $_whitespace_pattern = '/([^\S\xA0\x{202F}\x{2007}]+)/u';
/**
* The regex splits on everything that's a separator (^\S double negative)
* plus dashes, excluding the following non-breaking space characters:
* * nbsp (\xA0)
* * narrow nbsp (\x{202F})
* * figure space (\x{2007})
*/
public static $_wordbreak_pattern = '/([^\S\xA0\x{202F}\x{2007}\n]+|\R|\-+|\xAD+)/u';
/**
* Frame for this reflower
*
* @var TextFrameDecorator
*/
protected $_frame;
/**
* Saves trailing whitespace trimmed after a line break, so it can be
* restored when needed.
*
* @var string|null
*/
protected $trailingWs = null;
/**
* @var FontMetrics
*/
private $fontMetrics;
/**
* @param TextFrameDecorator $frame
* @param FontMetrics $fontMetrics
*/
public function __construct(TextFrameDecorator $frame, FontMetrics $fontMetrics)
{
parent::__construct($frame);
$this->setFontMetrics($fontMetrics);
}
/**
* Apply text transform and white-space collapse according to style.
*
* * http://www.w3.org/TR/CSS21/text.html#propdef-text-transform
* * http://www.w3.org/TR/CSS21/text.html#propdef-white-space
*
* @param string $text
* @return string
*/
protected function pre_process_text(string $text): string
{
$style = $this->_frame->get_style();
// Handle text transform
switch ($style->text_transform) {
case "capitalize":
$text = Helpers::mb_ucwords($text);
break;
case "uppercase":
$text = mb_convert_case($text, MB_CASE_UPPER);
break;
case "lowercase":
$text = mb_convert_case($text, MB_CASE_LOWER);
break;
default:
break;
}
// Handle white-space collapse
switch ($style->white_space) {
default:
case "normal":
case "nowrap":
$text = preg_replace(self::$_whitespace_pattern, " ", $text) ?? "";
break;
case "pre-line":
// Collapse white space except for line breaks
$text = preg_replace('/([^\S\xA0\x{202F}\x{2007}\n]+)/u', " ", $text) ?? "";
break;
case "pre":
case "pre-wrap":
break;
}
return $text;
}
/**
* @param string $text
* @param BlockFrameDecorator $block
* @param bool $nowrap
*
* @return bool|int
*/
protected function line_break(string $text, BlockFrameDecorator $block, bool $nowrap = false)
{
$fontMetrics = $this->getFontMetrics();
$frame = $this->_frame;
$style = $frame->get_style();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
// Determine the available width
$current_line = $block->get_current_line_box();
$line_width = $frame->get_containing_block("w");
$current_line_width = $current_line->left + $current_line->w + $current_line->right;
$available_width = $line_width - $current_line_width;
// Determine the frame width including margin, padding & border
$visible_text = preg_replace('/\xAD/u', "", $text);
$text_width = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
$mbp_width = (float) $style->length_in_pt([
$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;
if (Helpers::lengthLessOrEqual($frame_width, $available_width)) {
return false;
}
if ($nowrap) {
return $current_line_width == 0 ? false : 0;
}
// Split the text into words
$words = preg_split(self::$_wordbreak_pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$wc = count($words);
// Determine the split point
$width = 0.0;
$str = "";
$space_width = $fontMetrics->getTextWidth(" ", $font, $size, $word_spacing, $letter_spacing);
$shy_width = $fontMetrics->getTextWidth(self::SOFT_HYPHEN, $font, $size);
// @todo support <wbr>
for ($i = 0; $i < $wc; $i += 2) {
// Allow trailing white space to overflow. White space is always
// collapsed to the standard space character currently, so only
// handle that for now
$sep = $words[$i + 1] ?? "";
$word = $sep === " " ? $words[$i] : $words[$i] . $sep;
$word_width = $fontMetrics->getTextWidth($word, $font, $size, $word_spacing, $letter_spacing);
$used_width = $width + $word_width + $mbp_width;
if (Helpers::lengthGreater($used_width, $available_width)) {
// If the previous split happened by soft hyphen, we have to
// append its width again because the last hyphen of a line
// won't be removed
if (isset($words[$i - 1]) && self::SOFT_HYPHEN === $words[$i - 1]) {
$width += $shy_width;
}
break;
}
// If the word is splitted by soft hyphen, but no line break is needed
// we have to reduce the width. But the str is not modified, otherwise
// the wrong offset is calculated at the end of this method.
if ($sep === self::SOFT_HYPHEN) {
$width += $word_width - $shy_width;
$str .= $word;
} elseif ($sep === " ") {
$width += $word_width + $space_width;
$str .= $word . $sep;
} else {
$width += $word_width;
$str .= $word;
}
}
// The first word has overflowed. Force it onto the line, or as many
// characters as fit if breaking words is allowed
if ($current_line_width == 0 && $width === 0.0) {
if ($sep === " ") {
$word .= $sep;
}
// https://www.w3.org/TR/css-text-3/#overflow-wrap-property
$wrap = $style->overflow_wrap;
$break_word = $wrap === "anywhere" || $wrap === "break-word";
if ($break_word) {
$s = "";
for ($j = 0; $j < mb_strlen($word); $j++) {
$c = mb_substr($word, $j, 1);
$w = $fontMetrics->getTextWidth($s . $c, $font, $size, $word_spacing, $letter_spacing);
if (Helpers::lengthGreater($w, $available_width)) {
break;
}
$s .= $c;
}
// Always force the first character onto the line
$str = $j === 0 ? $s . $c : $s;
} else {
$str = $word;
}
}
$offset = mb_strlen($str);
return $offset;
}
/**
* @param string $text
* @return bool|int
*/
protected function newline_break(string $text)
{
if (($i = mb_strpos($text, "\n")) === false) {
return false;
}
return $i + 1;
}
/**
* @param BlockFrameDecorator $block
* @return bool|null Whether to add a new line at the end. `null` if reflow
* should be stopped.
*/
protected function layout_line(BlockFrameDecorator $block): ?bool
{
$frame = $this->_frame;
$style = $frame->get_style();
$current_line = $block->get_current_line_box();
$text = $frame->get_text();
// Trim leading white space if this is the first text on the line
if ($current_line->w === 0.0 && !$frame->is_pre()) {
$text = ltrim($text, " ");
}
if ($text === "") {
$frame->set_text("");
$style->set_used("width", 0.0);
return false;
}
// Determine the next line break
// http://www.w3.org/TR/CSS21/text.html#propdef-white-space
$white_space = $style->white_space;
$nowrap = $white_space === "nowrap" || $white_space === "pre";
switch ($white_space) {
default:
case "normal":
case "nowrap":
$split = $this->line_break($text, $block, $nowrap);
$add_line = false;
break;
case "pre":
case "pre-line":
case "pre-wrap":
$hard_split = $this->newline_break($text);
$first_line = $hard_split !== false
? mb_substr($text, 0, $hard_split)
: $text;
$soft_split = $this->line_break($first_line, $block, $nowrap);
$split = $soft_split !== false ? $soft_split : $hard_split;
$add_line = $hard_split !== false;
break;
}
if ($split === 0) {
// Make sure to move text when floating frames leave no space to
// place anything onto the line
// TODO: Would probably be better to move just below the current
// floating frame instead of trying to place text in line-height
// increments
if ($current_line->h === 0.0) {
// Line height might be 0
$h = max($frame->get_margin_height(), 1.0);
$block->maximize_line_height($h, $frame);
}
// Break line and repeat layout
$block->add_line();
// Find the appropriate inline ancestor to split
$child = $frame;
$p = $child->get_parent();
while ($p instanceof InlineFrameDecorator && !$child->get_prev_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if ($p instanceof InlineFrameDecorator) {
// Split parent and stop current reflow. Reflow continues
// via child-reflow loop of split parent
$p->split($child);
return null;
}
return $this->layout_line($block);
}
// Final split point is determined
if ($split !== false && $split < mb_strlen($text)) {
// Split the line
$frame->set_text($text);
$frame->split_text($split);
$add_line = true;
// Remove inner soft hyphens
$t = $frame->get_text();
$shyPosition = mb_strpos($t, self::SOFT_HYPHEN);
if (false !== $shyPosition && $shyPosition < mb_strlen($t) - 1) {
$t = str_replace(self::SOFT_HYPHEN, "", mb_substr($t, 0, -1)) . mb_substr($t, -1);
$frame->set_text($t);
}
} else {
// No split required
// Remove soft hyphens
$text = str_replace(self::SOFT_HYPHEN, "", $text);
$frame->set_text($text);
}
// Set our new width
$frame->recalculate_width();
return $add_line;
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$frame = $this->_frame;
$page = $frame->get_root();
$page->check_forced_page_break($frame);
if ($page->is_full()) {
return;
}
// Determine the text height
$style = $frame->get_style();
$size = $style->font_size;
$font = $style->font_family;
$font_height = $this->getFontMetrics()->getFontHeight($font, $size);
$style->set_used("height", $font_height);
// Handle text transform and white space
$text = $this->pre_process_text($frame->get_text());
$frame->set_text($text);
if ($block === null) {
return;
}
$add_line = $this->layout_line($block);
if ($add_line === null) {
return;
}
$frame->position();
// Skip wrapped white space between block-level elements in case white
// space is collapsed
if ($frame->get_text() === "" && $frame->get_margin_width() === 0.0) {
return;
}
$line = $block->add_frame_to_line($frame);
$trimmed = trim($frame->get_text());
// Split the text into words (used to determine spacing between
// words on justified lines)
if ($trimmed !== "") {
$words = preg_split(self::$_whitespace_pattern, $trimmed);
$line->wc += count($words);
}
if ($add_line) {
$block->add_line();
}
}
/**
* Trim trailing white space from the frame text.
*/
public function trim_trailing_ws(): void
{
$frame = $this->_frame;
$text = $frame->get_text();
$trailing = mb_substr($text, -1);
// White space is always collapsed to the standard space character
// currently, so only handle that for now
if ($trailing === " ") {
$this->trailingWs = $trailing;
$frame->set_text(mb_substr($text, 0, -1));
$frame->recalculate_width();
}
}
public function reset(): void
{
parent::reset();
// Restore trimmed trailing white space, as the frame will go through
// another reflow and line breaks might be different after a split
if ($this->trailingWs !== null) {
$text = $this->_frame->get_text();
$this->_frame->set_text($text . $this->trailingWs);
$this->trailingWs = null;
}
}
//........................................................................
public function get_min_max_width(): array
{
$fontMetrics = $this->getFontMetrics();
$frame = $this->_frame;
$style = $frame->get_style();
$text = $frame->get_text();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
// Handle text transform and white space
$text = $this->pre_process_text($frame->get_text());
if (!$frame->is_pre()) {
// Determine whether the frame is at the start of its parent block.
// Trim leading white space in that case
$child = $frame;
$p = $frame->get_parent();
while (!$p->is_block() && !$child->get_prev_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if (!$child->get_prev_sibling()) {
$text = ltrim($text, " ");
}
// Determine whether the frame is at the end of its parent block.
// Trim trailing white space in that case
$child = $frame;
$p = $frame->get_parent();
while (!$p->is_block() && !$child->get_next_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if (!$child->get_next_sibling()) {
$text = rtrim($text, " ");
}
}
// Strip soft hyphens for max-line-width calculations
$visible_text = preg_replace('/\xAD/u', "", $text);
// Determine minimum text width
switch ($style->white_space) {
default:
case "normal":
case "pre-line":
case "pre-wrap":
// The min width is the longest word or, if breaking words is
// allowed with the `anywhere` keyword, the widest character.
// For performance reasons, we only check the first character in
// the latter case.
// https://www.w3.org/TR/css-text-3/#overflow-wrap-property
if ($style->overflow_wrap === "anywhere") {
$char = mb_substr($visible_text, 0, 1);
$min = $fontMetrics->getTextWidth($char, $font, $size, $word_spacing, $letter_spacing);
} else {
// Find the longest word
$words = preg_split(self::$_wordbreak_pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$lengths = array_map(function ($chunk) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
// Allow trailing white space to overflow. As in actual
// layout above, only handle a single space for now
$sep = $chunk[1] ?? "";
$word = $sep === " " ? $chunk[0] : $chunk[0] . $sep;
return $fontMetrics->getTextWidth($word, $font, $size, $word_spacing, $letter_spacing);
}, array_chunk($words, 2));
$min = max($lengths);
}
break;
case "pre":
// Find the longest line
$lines = array_flip(preg_split("/\R/u", $visible_text));
array_walk($lines, function (&$chunked_text_width, $chunked_text) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
$chunked_text_width = $fontMetrics->getTextWidth($chunked_text, $font, $size, $word_spacing, $letter_spacing);
});
arsort($lines);
$min = reset($lines);
break;
case "nowrap":
$min = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
break;
}
// Determine maximum text width
switch ($style->white_space) {
default:
case "normal":
$max = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
break;
case "pre-line":
case "pre-wrap":
// Find the longest line
$lines = array_flip(preg_split("/\R/u", $visible_text));
array_walk($lines, function (&$chunked_text_width, $chunked_text) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
$chunked_text_width = $fontMetrics->getTextWidth($chunked_text, $font, $size, $word_spacing, $letter_spacing);
});
arsort($lines);
$max = reset($lines);
break;
case "pre":
case "nowrap":
$max = $min;
break;
}
// Account for margins, borders, and padding
$dims = [
$style->padding_left,
$style->padding_right,
$style->border_left_width,
$style->border_right_width,
$style->margin_left,
$style->margin_right
];
// The containing block is not defined yet, treat percentages as 0
$delta = (float) $style->length_in_pt($dims, 0);
$min += $delta;
$max += $delta;
return [$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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,254 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Image;
use Dompdf\Options;
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 = [];
/**
* @var array
*/
protected static $tempImages = [];
/**
* The url to the "broken image" used when images can't be loaded
*
* @var string
*/
public static $broken_image = "data:image/svg+xml;charset=utf8,%3C?xml version='1.0'?%3E%3Csvg width='64' height='64' xmlns='http://www.w3.org/2000/svg'%3E%3Cg%3E%3Crect stroke='%23666666' id='svg_1' height='60.499994' width='60.166667' y='1.666669' x='1.999998' stroke-width='1.5' fill='none'/%3E%3Cline stroke-linecap='null' stroke-linejoin='null' id='svg_3' y2='59.333253' x2='59.749916' y1='4.333415' x1='4.250079' stroke-width='1.5' stroke='%23999999' fill='none'/%3E%3Cline stroke-linecap='null' stroke-linejoin='null' id='svg_4' y2='59.999665' x2='4.062838' y1='3.750342' x1='60.062164' stroke-width='1.5' stroke='%23999999' fill='none'/%3E%3C/g%3E%3C/svg%3E";
public static $error_message = "Image not found or type unknown";
/**
* 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 Options $options An instance of Dompdf\Options
*
* @return array An array with three elements: The local path to the image, the image
* extension, and an error message if the image could not be cached
*/
static function resolve_url($url, $protocol, $host, $base_path, Options $options)
{
$tempfile = null;
$resolved_url = null;
$type = null;
$message = null;
try {
$full_url = Helpers::build_url($protocol, $host, $base_path, $url);
if ($full_url === null) {
throw new ImageException("Unable to parse image URL $url.", E_WARNING);
}
$parsed_url = Helpers::explode_url($full_url);
$protocol = strtolower($parsed_url["protocol"]);
$is_data_uri = strpos($protocol, "data:") === 0;
if (!$is_data_uri) {
$allowed_protocols = $options->getAllowedProtocols();
if (!array_key_exists($protocol, $allowed_protocols)) {
throw new ImageException("Permission denied on $url. The communication protocol is not supported.", E_WARNING);
}
foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
[$result, $message] = $rule($full_url);
if (!$result) {
throw new ImageException("Error loading $url: $message", E_WARNING);
}
}
}
if ($protocol === "file://") {
$resolved_url = $full_url;
} elseif (isset(self::$_cache[$full_url])) {
$resolved_url = self::$_cache[$full_url];
} else {
$tmp_dir = $options->getTempDir();
if (($resolved_url = @tempnam($tmp_dir, "ca_dompdf_img_")) === false) {
throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING);
}
$tempfile = $resolved_url;
$image = null;
if ($is_data_uri) {
if (($parsed_data_uri = Helpers::parse_data_uri($url)) !== false) {
$image = $parsed_data_uri["data"];
}
} else {
list($image, $http_response_header) = Helpers::getFileContent($full_url, $options->getHttpContext());
}
// Image not found or invalid
if ($image === null) {
$msg = ($is_data_uri ? "Data-URI could not be parsed" : "Image not found");
throw new ImageException($msg, E_WARNING);
}
if (@file_put_contents($resolved_url, $image) === false) {
throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING);
}
self::$_cache[$full_url] = $resolved_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);
}
list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext());
if (($width && $height && in_array($type, ["gif", "png", "jpeg", "bmp", "svg","webp"], true)) === false) {
throw new ImageException("Image type unknown", E_WARNING);
}
if ($type === "svg") {
$parser = xml_parser_create("utf-8");
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
xml_set_element_handler(
$parser,
function ($parser, $name, $attributes) use ($options, $parsed_url, $full_url) {
if ($name === "image") {
$attributes = array_change_key_case($attributes, CASE_LOWER);
$url = $attributes["xlink:href"] ?? $attributes["href"];
if (!empty($url)) {
$inner_full_url = Helpers::build_url($parsed_url["protocol"], $parsed_url["host"], $parsed_url["path"], $url);
if ($inner_full_url === $full_url) {
throw new ImageException("SVG self-reference is not allowed", E_WARNING);
}
[$resolved_url, $type, $message] = self::resolve_url($url, $parsed_url["protocol"], $parsed_url["host"], $parsed_url["path"], $options);
if (!empty($message)) {
throw new ImageException("This SVG document references a restricted resource. $message", E_WARNING);
}
}
}
},
false
);
if (($fp = fopen($resolved_url, "r")) !== false) {
while ($line = fread($fp, 8192)) {
xml_parse($parser, $line, false);
}
fclose($fp);
}
xml_parser_free($parser);
}
} catch (ImageException $e) {
if ($tempfile) {
unlink($tempfile);
}
$resolved_url = self::$broken_image;
list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext());
$message = self::$error_message;
Helpers::record_warnings($e->getCode(), $e->getMessage() . " \n $url", $e->getFile(), $e->getLine());
self::$_cache[$full_url] = $resolved_url;
}
return [$resolved_url, $type, $message];
}
/**
* Register a temp file for the given original image file.
*
* @param string $filePath The path of the original image.
* @param string $tempPath The path of the temp file to register.
* @param string $key An optional key to register the temp file at.
*/
static function addTempImage(string $filePath, string $tempPath, string $key = "default"): void
{
if (!isset(self::$tempImages[$filePath])) {
self::$tempImages[$filePath] = [];
}
self::$tempImages[$filePath][$key] = $tempPath;
}
/**
* Get the path of a temp file registered for the given original image file.
*
* @param string $filePath The path of the original image.
* @param string $key The key the temp file is registered at.
*/
static function getTempImage(string $filePath, string $key = "default"): ?string
{
return self::$tempImages[$filePath][$key] ?? null;
}
/**
* Unlink all cached images (i.e. temporary images either downloaded
* or converted) except for the bundled "broken image"
*/
static function clear(bool $debugPng = false)
{
foreach (self::$_cache as $file) {
if ($file === self::$broken_image) {
continue;
}
if ($debugPng) {
print "[clear unlink $file]";
}
if (file_exists($file)) {
unlink($file);
}
}
foreach (self::$tempImages as $versions) {
foreach ($versions as $file) {
if ($file === self::$broken_image) {
continue;
}
if ($debugPng) {
print "[unlink temp image $file]";
}
if (file_exists($file)) {
unlink($file);
}
}
}
self::$_cache = [];
self::$tempImages = [];
}
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.svg"))) {
Cache::$broken_image = realpath(__DIR__ . "/../../lib/res/broken_image.svg");
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
/**
* 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 Frame $frame
*/
public function render(Frame $frame)
{
if (!$this->_dompdf->getOptions()->getIsJavascriptEnabled()) {
return;
}
$this->insert($frame->get_node()->nodeValue);
}
}

View File

@@ -0,0 +1,412 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameDecorator\Block;
use Dompdf\FrameDecorator\ListBullet;
use Dompdf\FrameDecorator\Page;
use Dompdf\FrameReflower\Text as TextFrameReflower;
use Dompdf\Positioner\Inline as InlinePositioner;
/**
* 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 AbstractFrameDecorator[]
*/
protected $_frames = [];
/**
* @var ListBullet[]
*/
protected $list_markers = [];
/**
* @var int
*/
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 AbstractFrameDecorator
*/
public $tallest_frame = null;
/**
* @var bool[]
*/
public $floating_blocks = [];
/**
* @var bool
*/
public $br = false;
/**
* Whether the line box contains any inline-positioned frames.
*
* @var bool
*/
public $inline = 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 = [];
$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 = [];
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 > 0 && $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 > 0 && $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 AbstractFrameDecorator[]
*/
function &get_frames()
{
return $this->_frames;
}
/**
* @param AbstractFrameDecorator $frame
*/
public function add_frame(Frame $frame): void
{
$this->_frames[] = $frame;
if ($frame->get_positioner() instanceof InlinePositioner) {
$this->inline = true;
}
}
/**
* Remove the frame at the given index and all following frames from the
* line.
*
* @param int $index
*/
public function remove_frames(int $index): void
{
$lastIndex = count($this->_frames) - 1;
if ($index < 0 || $index > $lastIndex) {
return;
}
for ($i = $lastIndex; $i >= $index; $i--) {
$f = $this->_frames[$i];
unset($this->_frames[$i]);
$this->w -= $f->get_margin_width();
}
// Reset array indices
$this->_frames = array_values($this->_frames);
// Recalculate the height of the line
$h = 0.0;
$this->inline = false;
foreach ($this->_frames as $f) {
$h = max($h, $f->get_margin_height());
if ($f->get_positioner() instanceof InlinePositioner) {
$this->inline = true;
}
}
$this->h = $h;
}
/**
* Get the `outside` positioned list markers to be vertically aligned with
* the line box.
*
* @return ListBullet[]
*/
public function get_list_markers(): array
{
return $this->list_markers;
}
/**
* Add a list marker to the line box.
*
* The list marker is only added for the purpose of vertical alignment, it
* is not actually added to the list of frames of the line box.
*/
public function add_list_marker(ListBullet $marker): void
{
$this->list_markers[] = $marker;
}
/**
* An iterator of all list markers and inline positioned frames of the line
* box.
*
* @return \Iterator<AbstractFrameDecorator>
*/
public function frames_to_align(): \Iterator
{
yield from $this->list_markers;
foreach ($this->_frames as $frame) {
if ($frame->get_positioner() instanceof InlinePositioner) {
yield $frame;
}
}
}
/**
* Trim trailing whitespace from the line.
*/
public function trim_trailing_ws(): void
{
$lastIndex = count($this->_frames) - 1;
if ($lastIndex < 0) {
return;
}
$lastFrame = $this->_frames[$lastIndex];
$reflower = $lastFrame->get_reflower();
if ($reflower instanceof TextFrameReflower && !$lastFrame->is_pre()) {
$reflower->trim_trailing_ws();
$this->recalculate_width();
}
}
/**
* Recalculate LineBox width based on the contained frames total width.
*
* @return float
*/
public function recalculate_width(): float
{
$width = 0.0;
foreach ($this->_frames as $frame) {
$width += $frame->get_margin_width();
}
return $this->w = $width;
}
/**
* @return string
*/
public function __toString(): string
{
$props = ["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;
}
}
/*
class LineBoxList implements Iterator {
private $_p = 0;
private $_lines = array();
}
*/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf;
/**
* 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 = [])
{
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 Frame $frame
*/
public function render(Frame $frame)
{
$this->evaluate($frame->get_node()->nodeValue);
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameReflower\Block;
/**
* Positions absolutely positioned frames
*/
class Absolute extends AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
*/
function position(AbstractFrameDecorator $frame): void
{
if ($frame->get_reflower() instanceof Block) {
$style = $frame->get_style();
[$cbx, $cby, $cbw, $cbh] = $frame->get_containing_block();
// If the `top` value is `auto`, the frame will be repositioned
// after its height has been resolved
$left = (float) $style->length_in_pt($style->left, $cbw);
$top = (float) $style->length_in_pt($style->top, $cbh);
$frame->set_position($cbx + $left, $cby + $top);
} else {
// Legacy positioning logic for image and table frames
// TODO: Resolve dimensions, margins, and offsets similar to the
// block case in the reflowers and use the simplified logic above
$style = $frame->get_style();
$block_parent = $frame->find_block_parent();
$current_line = $block_parent->get_current_line_box();
list($x, $y, $w, $h) = $frame->get_containing_block();
$inflow_x = $block_parent->get_content_box()["x"] + $current_line->left + $current_line->w;
$inflow_y = $current_line->y;
$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);
list($width, $height) = [$frame->get_margin_width(), $frame->get_margin_height()];
$orig_width = $style->get_specified("width");
$orig_height = $style->get_specified("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 = $inflow_x;
} 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 = $inflow_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);
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Base AbstractPositioner class
*
* Defines positioner interface
*
* @package dompdf
*/
abstract class AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
*/
abstract function position(AbstractFrameDecorator $frame): void;
/**
* @param AbstractFrameDecorator $frame
* @param float $offset_x
* @param float $offset_y
* @param bool $ignore_self
*/
function move(
AbstractFrameDecorator $frame,
float $offset_x,
float $offset_y,
bool $ignore_self = false
): void {
[$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);
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
/**
* Positions block frames
*
* @package dompdf
*/
class Block extends AbstractPositioner
{
function position(AbstractFrameDecorator $frame): void
{
$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"];
$frame->set_position($x, $y);
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameReflower\Block;
/**
* Positions fixely positioned frames
*/
class Fixed extends Absolute
{
/**
* @param AbstractFrameDecorator $frame
*/
function position(AbstractFrameDecorator $frame): void
{
if ($frame->get_reflower() instanceof Block) {
parent::position($frame);
} else {
// Legacy positioning logic for image and table frames
// TODO: Resolve dimensions, margins, and offsets similar to the
// block case in the reflowers and use the simplified logic above
$style = $frame->get_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->get_specified("height"), $initialcb["h"]);
$width = (float)$style->length_in_pt($style->get_specified("width"), $initialcb["w"]);
$top = $style->length_in_pt($style->get_specified("top"), $initialcb["h"]);
$right = $style->length_in_pt($style->get_specified("right"), $initialcb["w"]);
$bottom = $style->length_in_pt($style->get_specified("bottom"), $initialcb["h"]);
$left = $style->length_in_pt($style->get_specified("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);
foreach ($frame->get_children() as $child) {
$child->set_position($x, $y);
}
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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\Exception;
use Dompdf\Helpers;
/**
* Positions inline frames
*
* @package dompdf
*/
class Inline extends AbstractPositioner
{
/**
* @param AbstractFrameDecorator $frame
* @throws Exception
*/
function position(AbstractFrameDecorator $frame): void
{
// Find our nearest block level parent and access its lines property
$block = $frame->find_block_parent();
if (!$block) {
throw new Exception("No block-level parent found. Not good.");
}
$cb = $frame->get_containing_block();
$line = $block->get_current_line_box();
if (!$frame->is_text_node() && !($frame instanceof InlineFrameDecorator)) {
// Atomic inline boxes and replaced inline elements
// (inline-block, inline-table, img etc.)
$width = $frame->get_margin_width();
$available_width = $cb["w"] - $line->left - $line->w - $line->right;
if (Helpers::lengthGreater($width, $available_width)) {
$block->add_line();
$line = $block->get_current_line_box();
}
}
$frame->set_position($cb["x"] + $line->w, $line->y);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Positioner;
use Dompdf\FrameDecorator\AbstractFrameDecorator;
use Dompdf\FrameDecorator\ListBullet as ListBulletFrameDecorator;
/**
* Positions list bullets
*
* @package dompdf
*/
class ListBullet extends AbstractPositioner
{
/**
* @param ListBulletFrameDecorator $frame
*/
function position(AbstractFrameDecorator $frame): void
{
// List markers are positioned to the left of the border edge of their
// parent element (FIXME: right for RTL)
$parent = $frame->get_parent();
$style = $parent->get_style();
$cbw = $parent->get_containing_block("w");
$margin_left = (float) $style->length_in_pt($style->margin_left, $cbw);
$border_edge = $parent->get_position("x") + $margin_left;
// This includes the marker indentation
$x = $border_edge - $frame->get_margin_width();
// The marker is later vertically aligned with the corresponding line
// box and its vertical position is fine-tuned in the renderer
$p = $frame->find_block_parent();
$y = $p->get_current_line_box()->y;
$frame->set_position($x, $y);
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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): void
{
return;
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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): void
{
$table = Table::find_parent_table($frame);
$cellmap = $table->get_cellmap();
$frame->set_position($cellmap->get_frame_position($frame));
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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): void
{
$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);
}
}

View File

@@ -0,0 +1,291 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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;
/**
* 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;
$this->_check_callbacks("begin_frame", $frame);
if ($_dompdf_debug) {
echo $frame;
flush();
}
$style = $frame->get_style();
if (in_array($style->visibility, ["hidden", "collapse"], true)) {
return;
}
$display = $style->display;
$transformList = $style->transform;
$hasTransform = $transformList !== [];
// Starts the CSS transformation
if ($hasTransform) {
$this->_canvas->save();
list($x, $y) = $frame->get_padding_box();
$origin = $style->transform_origin;
foreach ($transformList 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], (float)$style->length_in_pt($style->width));
$values[] = $y + (float)$style->length_in_pt($origin[1], (float)$style->length_in_pt($style->height));
call_user_func_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") {
$padding_box = $frame->get_padding_box();
[$x, $y, $w, $h] = $padding_box;
$style = $frame->get_style();
if ($style->has_border_radius()) {
$border_box = $frame->get_border_box();
[$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $padding_box);
$this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl);
} else {
$this->_canvas->clipping_rectangle($x, $y, $w, $h);
}
}
$stack = [];
foreach ($frame->get_children() as $child) {
// < 0 : negative 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 = $child_z_index + 1;
} elseif ($child_style->float !== "none" || $child->is_positioned()) {
$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 ($hasTransform) {
$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(string $event, Frame $frame): void
{
if (!isset($this->_callbacks)) {
$this->_callbacks = $this->_dompdf->getCallbacks();
}
if (isset($this->_callbacks[$event])) {
$fs = $this->_callbacks[$event];
$canvas = $this->_canvas;
$fontMetrics = $this->_dompdf->getFontMetrics();
foreach ($fs as $f) {
$f($frame, $canvas, $fontMetrics);
}
}
}
/**
* 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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Block as BlockFrameDecorator;
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();
$dompdf = $this->_dompdf;
$this->_set_opacity($frame->get_opacity($style->opacity));
[$x, $y, $w, $h] = $frame->get_border_box();
if ($node->nodeName === "body") {
// Margins should be fully resolved at this point
$mt = $style->margin_top;
$mb = $style->margin_bottom;
$h = $frame->get_containing_block("h") - $mt - $mb;
}
$border_box = [$x, $y, $w, $h];
// Draw our background, border and content
$this->_render_background($frame, $border_box);
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
// Handle anchors & links
if ($node->nodeName === "a" && $href = $node->getAttribute("href")) {
$href = Helpers::build_url($dompdf->getProtocol(), $dompdf->getBaseHost(), $dompdf->getBasePath(), $href) ?? $href;
$this->_canvas->add_link($href, $x, $y, $w, $h);
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
$this->debugBlockLayout($frame, "red", false);
}
protected function debugBlockLayout(Frame $frame, ?string $color, bool $lines = false): void
{
$options = $this->_dompdf->getOptions();
$debugLayout = $options->getDebugLayout();
if (!$debugLayout) {
return;
}
if ($color && $options->getDebugLayoutBlocks()) {
$this->_debug_layout($frame->get_border_box(), $color);
if ($options->getDebugLayoutPaddingBox()) {
$this->_debug_layout($frame->get_padding_box(), $color, [0.5, 0.5]);
}
}
if ($lines && $options->getDebugLayoutLines() && $frame instanceof BlockFrameDecorator) {
[$cx, , $cw] = $frame->get_content_box();
foreach ($frame->get_line_boxes() as $line) {
$lw = $cw - $line->left - $line->right;
$this->_debug_layout([$cx + $line->left, $line->y, $lw, $line->h], "orange");
}
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Frame;
use Dompdf\FrameDecorator\Image as ImageFrameDecorator;
use Dompdf\Image\Cache;
/**
* Image renderer
*
* @package dompdf
*/
class Image extends Block
{
/**
* @param ImageFrameDecorator $frame
*/
function render(Frame $frame)
{
$style = $frame->get_style();
$border_box = $frame->get_border_box();
$this->_set_opacity($frame->get_opacity($style->opacity));
// Render background & borders
$this->_render_background($frame, $border_box);
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
$content_box = $frame->get_content_box();
[$x, $y, $w, $h] = $content_box;
$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;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$this->_canvas->text(
$x,
$y,
$alt,
$font,
$size,
$style->color,
$word_spacing,
$letter_spacing
);
} elseif ($w > 0 && $h > 0) {
if ($style->has_border_radius()) {
[$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $content_box);
$this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl);
}
$this->_canvas->image($src, $x, $y, $w, $h, $style->image_resolution);
if ($style->has_border_radius()) {
$this->_canvas->clipping_end();
}
}
if ($msg = $frame->get_image_msg()) {
$parts = preg_split("/\s*\n\s*/", $msg);
$font = $style->font_family;
$height = 10;
$_y = $alt ? $y + $h - count($parts) * $height : $y;
foreach ($parts as $i => $_part) {
$this->_canvas->text($x, $_y + $i * $height, $_part, $font, $height * 0.8, [0.5, 0.5, 0.5]);
}
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
$this->debugBlockLayout($frame, "blue");
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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
*
* @package dompdf
*/
class Inline extends AbstractRenderer
{
function render(Frame $frame)
{
if (!$frame->get_first_child()) {
return; // No children, no service
}
$style = $frame->get_style();
$dompdf = $this->_dompdf;
$this->_set_opacity($frame->get_opacity($style->opacity));
$do_debug_layout_line = $dompdf->getOptions()->getDebugLayout()
&& $dompdf->getOptions()->getDebugLayoutInline();
// Draw the background & border behind each child. To do this we need
// to figure out just how much space each child takes:
[$x, $y] = $frame->get_first_child()->get_position();
[$w, $h] = $this->get_child_size($frame, $do_debug_layout_line);
[, , $cbw] = $frame->get_containing_block();
$margin_left = $style->length_in_pt($style->margin_left, $cbw);
$pt = $style->length_in_pt($style->padding_top, $cbw);
$pb = $style->length_in_pt($style->padding_bottom, $cbw);
// Make sure that border and background start inside the left margin
// Extend the drawn box by border and padding in vertical direction, as
// these do not affect layout
// FIXME: Using a small vertical offset of a fraction of the height here
// to work around the vertical position being slightly off in general
$x += $margin_left;
$y -= $style->border_top_width + $pt - ($h * 0.1);
$w += $style->border_left_width + $style->border_right_width;
$h += $style->border_top_width + $pt + $style->border_bottom_width + $pb;
$border_box = [$x, $y, $w, $h];
$this->_render_background($frame, $border_box);
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
$node = $frame->get_node();
$id = $node->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
// Only two levels of links frames
$is_link_node = $node->nodeName === "a";
if ($is_link_node) {
if (($name = $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 ($is_link_node) {
if ($href = $node->getAttribute("href")) {
$href = Helpers::build_url($dompdf->getProtocol(), $dompdf->getBaseHost(), $dompdf->getBasePath(), $href) ?? $href;
$this->_canvas->add_link($href, $x, $y, $w, $h);
}
}
}
protected function get_child_size(Frame $frame, bool $do_debug_layout_line): array
{
$w = 0.0;
$h = 0.0;
foreach ($frame->get_children() as $child) {
if ($child->get_node()->nodeValue === " " && $child->get_prev_sibling() && !$child->get_next_sibling()) {
break;
}
$style = $child->get_style();
$auto_width = $style->width === "auto";
$auto_height = $style->height === "auto";
[, , $child_w, $child_h] = $child->get_padding_box();
if ($auto_width || $auto_height) {
[$child_w2, $child_h2] = $this->get_child_size($child, $do_debug_layout_line);
if ($auto_width) {
$child_w = $child_w2;
}
if ($auto_height) {
$child_h = $child_h2;
}
}
$w += $child_w;
$h = max($h, $child_h);
if ($do_debug_layout_line) {
$this->_debug_layout($child->get_border_box(), "blue");
if ($this->_dompdf->getOptions()->getDebugLayoutPaddingBox()) {
$this->_debug_layout($child->get_padding_box(), "blue", [0.5, 0.5]);
}
}
}
return [$w, $h];
}
}

View File

@@ -0,0 +1,235 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Helpers;
use Dompdf\Frame;
use Dompdf\FrameDecorator\ListBullet as ListBulletFrameDecorator;
use Dompdf\FrameDecorator\ListBulletImage;
use Dompdf\Image\Cache;
/**
* Renders list bullets
*
* @package dompdf
*/
class ListBullet extends AbstractRenderer
{
/**
* @param $type
* @return mixed|string
*/
static function get_counter_chars($type)
{
static $cache = [];
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 int $n
* @param string $type
* @param int|null $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 - 1) % 26) + ord('a'));
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 ListBulletFrameDecorator $frame
*/
function render(Frame $frame)
{
$li = $frame->get_parent();
$style = $frame->get_style();
$this->_set_opacity($frame->get_opacity($style->opacity));
// Don't render bullets twice if the list item was split
if ($li->is_split_off) {
return;
}
$font_family = $style->font_family;
$font_size = $style->font_size;
$baseline = $this->_canvas->get_font_baseline($font_family, $font_size);
// Handle list-style-image
// If list style image is requested but missing, fall back to predefined types
if ($frame instanceof ListBulletImage && !Cache::is_broken($img = $frame->get_image_url())) {
[$x, $y] = $frame->get_position();
$w = $frame->get_width();
$h = $frame->get_height();
$y += $baseline - $h;
$this->_canvas->image($img, $x, $y, $w, $h);
} else {
$bullet_style = $style->list_style_type;
switch ($bullet_style) {
default:
case "disc":
case "circle":
[$x, $y] = $frame->get_position();
$offset = $font_size * ListBulletFrameDecorator::BULLET_OFFSET;
$r = ($font_size * ListBulletFrameDecorator::BULLET_SIZE) / 2;
$x += $r;
$y += $baseline - $r - $offset;
$o = $font_size * ListBulletFrameDecorator::BULLET_THICKNESS;
$this->_canvas->circle($x, $y, $r, $style->color, $o, null, $bullet_style !== "circle");
break;
case "square":
[$x, $y] = $frame->get_position();
$offset = $font_size * ListBulletFrameDecorator::BULLET_OFFSET;
$w = $font_size * ListBulletFrameDecorator::BULLET_SIZE;
$y += $baseline - $w - $offset;
$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;
}
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$text_width = $this->_dompdf->getFontMetrics()->getTextWidth($text, $font_family, $font_size, $word_spacing, $letter_spacing);
[$x, $y] = $frame->get_position();
// Correct for static frame width applied by positioner
$x += $frame->get_width() - $text_width;
$this->_canvas->text($x, $y, $text,
$font_family, $font_size,
$style->color, $word_spacing, $letter_spacing);
case "none":
break;
}
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
}
}

View File

@@ -0,0 +1,188 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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));
$border_box = $frame->get_border_box();
$table = Table::find_parent_table($frame);
if ($table->get_style()->border_collapse !== "collapse") {
$this->_render_background($frame, $border_box);
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
} else {
// The collapsed case is slightly complicated...
$cells = $table->get_cellmap()->get_spanned_cells($frame);
if (is_null($cells)) {
return;
}
// Render the background to the padding box, as the cells are
// rendered individually one after another, and we don't want the
// background to overlap an adjacent border
$padding_box = $frame->get_padding_box();
$this->_render_background($frame, $padding_box);
$this->_render_collapsed_border($frame, $table);
// FIXME: Outline should be drawn over other cells
$this->_render_outline($frame, $border_box);
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
// $this->debugBlockLayout($frame, "red", false);
}
/**
* @param Frame $frame
* @param Table $table
*/
protected function _render_collapsed_border(Frame $frame, Table $table): void
{
$cellmap = $table->get_cellmap();
$cells = $cellmap->get_spanned_cells($frame);
$num_rows = $cellmap->get_num_rows();
$num_cols = $cellmap->get_num_cols();
[$table_x, $table_y] = $table->get_position();
// 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);
$col = $cellmap->get_column($j);
$x = $table_x + $col["x"] - $bp["left"]["width"] / 2;
$y = $table_y + $top_row["y"] - $bp["top"]["width"] / 2;
$w = $col["used-width"] + ($bp["left"]["width"] + $bp["right"]["width"]) / 2;
if ($bp["top"]["width"] > 0) {
$widths = [
(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"]["width"] <= 0) {
continue;
}
$widths = [
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
];
$y = $table_y + $bottom_row["y"] + $bottom_row["height"] + $bp["bottom"]["width"] / 2;
$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);
$row = $cellmap->get_row($i);
$x = $table_x + $left_col["x"] - $bp["left"]["width"] / 2;
$y = $table_y + $row["y"] - $bp["top"]["width"] / 2;
$h = $row["height"] + ($bp["top"]["width"] + $bp["bottom"]["width"]) / 2;
if ($bp["left"]["width"] > 0) {
$widths = [
(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"]["width"] <= 0) {
continue;
}
$widths = [
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
];
$x = $table_x + $right_col["x"] + $right_col["used-width"] + $bp["right"]["width"] / 2;
$method = "_border_" . $bp["right"]["style"];
$this->$method($x, $y, $h, $bp["right"]["color"], $widths, "right", "square");
}
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @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));
$border_box = $frame->get_border_box();
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
$this->debugBlockLayout($frame, "red");
}
}

View File

@@ -0,0 +1,158 @@
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Renderer;
use Dompdf\Adapter\CPDF;
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)
{
$style = $frame->get_style();
$text = $frame->get_text();
if ($text === "") {
return;
}
$this->_set_opacity($frame->get_opacity($style->opacity));
list($x, $y) = $frame->get_position();
$cb = $frame->get_containing_block();
$ml = $style->margin_left;
$pl = $style->padding_left;
$bl = $style->border_left_width;
$x += (float) $style->length_in_pt([$ml, $pl, $bl], $cb["w"]);
$font = $style->font_family;
$size = $style->font_size;
$frame_font_size = $frame->get_dompdf()->getFontMetrics()->getFontHeight($font, $size);
$word_spacing = $frame->get_text_spacing() + $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$width = (float) $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, $letter_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 = $frame_font_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 = [];
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 2;
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, $size, $word_spacing, $letter_spacing);
$this->_debug_layout([$x, $y, $text_width, $frame_font_size], "orange", [0.5, 0.5]);
}
}
}