diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Adapter/CPDF.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Adapter/CPDF.php new file mode 100644 index 0000000..103cfc9 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Adapter/CPDF.php @@ -0,0 +1,1005 @@ + [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"); + + if ($this->_dompdf->getOptions()->isPdfAEnabled()) { + $this->_pdf->enablePdfACompliance(); + } + + $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 (method_exists(Helpers::class, $func_name)) { + $func_name = [Helpers::class, $func_name]; + } elseif (!function_exists($func_name)) { + throw new Exception("Function $func_name() not found. Cannot convert $type image: $image_url. Please install the image PHP extension."); + } + + 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); + if (PHP_MAJOR_VERSION < 8) { + 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; + + $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; + + $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; + + $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, '', 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); + } + } + + public function font_supports_char(string $font, string $char): bool + { + if ($char === "") { + return true; + } + + $subsetting = $this->_dompdf->getOptions()->getIsFontSubsettingEnabled(); + $this->_pdf->selectFont($font, '', false, $subsetting); + if (!\array_key_exists($font, $this->_pdf->fonts)) { + return false; + } + $fontInfo = $this->_pdf->fonts[$font]; + $charCode = Helpers::uniord($char, "UTF-8"); + + if (!$fontInfo["isUnicode"]) { + // The core fonts use Windows ANSI encoding. The char map uses the + // position of the character in the encoding's mapping table in this + // case, not the Unicode code point, which is different for the + // characters outside ISO-8859-1 (positions 0x80-0x9F) + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT + $mapping = [ + 0x20AC => 0x80, + 0x201A => 0x82, + 0x0192 => 0x83, + 0x201E => 0x84, + 0x2026 => 0x85, + 0x2020 => 0x86, + 0x2021 => 0x87, + 0x02C6 => 0x88, + 0x2030 => 0x89, + 0x0160 => 0x8A, + 0x2039 => 0x8B, + 0x0152 => 0x8C, + 0x017D => 0x8E, + 0x2018 => 0x91, + 0x2019 => 0x92, + 0x201C => 0x93, + 0x201D => 0x94, + 0x2022 => 0x95, + 0x2013 => 0x96, + 0x2014 => 0x97, + 0x02DC => 0x98, + 0x2122 => 0x99, + 0x0161 => 0x9A, + 0x203A => 0x9B, + 0x0153 => 0x9C, + 0x017E => 0x9E, + 0x0178 => 0x9F + ]; + + $charCode = $mapping[$charCode] ?? $charCode; + + if ($charCode > 0xFF) { + return false; + } + } + + return \array_key_exists($charCode, $fontInfo["C"]); + } + + /** + * @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("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; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Adapter/GD.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Adapter/GD.php new file mode 100644 index 0000000..8bd9393 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Adapter/GD.php @@ -0,0 +1,1061 @@ +_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) { + $points = [ + $x1, $y1, + $x1 + $w, $y1, + $x1 + $w, $y1 + $h, + $x1, $y1 + $h + ]; + if (version_compare(PHP_VERSION, "8.1.0", "<")) { + imagepolygon($this->get_image(), $points, count($points)/2, $c); + } else { + imagepolygon($this->get_image(), $points, $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) { + if (version_compare(PHP_VERSION, "8.1.0", "<")) { + imagefilledpolygon($this->get_image(), $points, count($points)/2, $c); + } else { + imagefilledpolygon($this->get_image(), $points, $c); + } + } else { + if (version_compare(PHP_VERSION, "8.1.0", "<")) { + imagepolygon($this->get_image(), $points, count($points)/2, $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 (method_exists(Helpers::class, $func_name)) { + $func_name = [Helpers::class, $func_name]; + } elseif (!function_exists($func_name)) { + throw new \Exception("Function $func_name() not found. Cannot convert $img_type image: $img. Please install the image PHP extension."); + } + $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: &#160; will render   rather than its character. + $text = preg_replace('/&(#(?:x[a-fA-F0-9]+|[0-9]+);)/', '&\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 + } + + private function getCharMap(string $font) + { + static $unicodeCharMapTables = []; + + if (isset($unicodeCharMapTables[$font])) { + return $unicodeCharMapTables[$font]; + } + + $metrics_name = "$font.ufm"; + if (!file_exists($metrics_name)) { + $metrics_name = "$font.afm"; + } + if (!file_exists($metrics_name)) { + return $unicodeCharMapTables[$font] = []; + } + + $cache_name = "$metrics_name.json"; + if (file_exists($cache_name)) { + $cached_font_info = json_decode(file_get_contents($cache_name), true); + $char_map = $cached_font_info['C']; + return $unicodeCharMapTables[$font] = $char_map; + } + + $char_map = []; + $file = file("$metrics_name"); + foreach ($file as $rowA) { + $row = trim($rowA); + $pos = strpos($row, ' '); + + if ($pos) { + // then there must be some keyword + $key = substr($row, 0, $pos); + switch ($key) { + case 'C': // Found in AFM files + $bits = explode(';', trim($row)); + $dtmp = ['C' => null, 'N' => null, 'WX' => null, 'B' => []]; + + foreach ($bits as $bit) { + $bits2 = explode(' ', trim($bit)); + if (mb_strlen($bits2[0], '8bit') == 0) { + continue; + } + + if (count($bits2) > 2) { + $dtmp[$bits2[0]] = []; + for ($i = 1; $i < count($bits2); $i++) { + $dtmp[$bits2[0]][] = $bits2[$i]; + } + } else { + if (count($bits2) == 2) { + $dtmp[$bits2[0]] = $bits2[1]; + } + } + } + + $c = (int)$dtmp['C']; + $n = $dtmp['N']; + $width = floatval($dtmp['WX']); + + if ($c >= 0) { + $char_map[$c] = $width; + } elseif (isset($n)) { + $char_map[$n] = $width; + } + break; + + // U 827 ; WX 0 ; N squaresubnosp ; G 675 ; + case 'U': // Found in UFM files + $bits = explode(';', trim($row)); + $dtmp = ['G' => null, 'N' => null, 'U' => null, 'WX' => null]; + + foreach ($bits as $bit) { + $bits2 = explode(' ', trim($bit)); + if (mb_strlen($bits2[0], '8bit') === 0) { + continue; + } + + if (count($bits2) > 2) { + $dtmp[$bits2[0]] = []; + for ($i = 1; $i < count($bits2); $i++) { + $dtmp[$bits2[0]][] = $bits2[$i]; + } + } else { + if (count($bits2) == 2) { + $dtmp[$bits2[0]] = $bits2[1]; + } + } + } + + $c = (int)$dtmp['U']; + $n = $dtmp['N']; + $glyph = $dtmp['G']; + $width = floatval($dtmp['WX']); + + if ($c >= 0) { + $char_map[$c] = $width; + } elseif (isset($n)) { + $char_map[$n] = $width; + } + + break; + } + } + } + + return $unicodeCharMapTables[$font] = $char_map; + } + + public function font_supports_char(string $font, string $char): bool + { + if ($char === "") { + return true; + } + + $font = $this->get_ttf_file($font); + $charMap = $this->getCharMap($font); + $charCode = Helpers::uniord($char, "UTF-8"); + + return \array_key_exists($charCode, $charMap); + } + + 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: &#160; will render   rather than its character. + $text = preg_replace('/&(#(?:x[a-fA-F0-9]+|[0-9]+);)/', '&\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("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 && PHP_MAJOR_VERSION < 8) { + imagedestroy($dst); + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Adapter/PDFLib.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Adapter/PDFLib.php new file mode 100644 index 0000000..ca92a79 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Adapter/PDFLib.php @@ -0,0 +1,1557 @@ + "Courier", + "courier-bold" => "Courier-Bold", + "courier-oblique" => "Courier-Oblique", + "courier-boldoblique" => "Courier-BoldOblique", + "helvetica" => "Helvetica", + "helvetica-bold" => "Helvetica-Bold", + "helvetica-oblique" => "Helvetica-Oblique", + "helvetica-boldoblique" => "Helvetica-BoldOblique", + "times" => "Times-Roman", + "times-roman" => "Times-Roman", + "times-bold" => "Times-Bold", + "times-italic" => "Times-Italic", + "times-bolditalic" => "Times-BoldItalic", + "symbol" => "Symbol", + "zapfdinbats" => "ZapfDingbats", + "zapfdingbats" => "ZapfDingbats", + ]; + + /** + * @var \Dompdf\Dompdf + */ + protected $_dompdf; + + /** + * Instance of PDFLib class + * + * @var \PDFLib + */ + protected $_pdf; + + /** + * Name of temporary file used for PDFs created on disk + * + * @var string + */ + protected $_file; + + /** + * PDF width, in points + * + * @var float + */ + protected $_width; + + /** + * PDF height, in points + * + * @var float + */ + protected $_height; + + /** + * Last fill color used + * + * @var array + */ + protected $_last_fill_color; + + /** + * Last stroke color used + * + * @var array + */ + protected $_last_stroke_color; + + /** + * The current opacity level + * + * @var float|null + */ + protected $_current_opacity; + + /** + * Cache of image handles + * + * @var array + */ + protected $_imgs; + + /** + * Cache of font handles + * + * @var array + */ + protected $_fonts; + + /** + * Cache of fontFile checks + * + * @var array + */ + protected $_fontsFiles; + + /** + * List of objects (templates) to add to multiple pages + * + * @var array + */ + protected $_objs; + + /** + * List of gstate objects created for this PDF (for reuse) + * + * @var array + */ + protected $_gstates = []; + + /** + * 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; + + 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]]; + } + + $this->_width = $size[2] - $size[0]; + $this->_height = $size[3] - $size[1]; + + if ($dompdf === null) { + $this->_dompdf = new Dompdf(); + } else { + $this->_dompdf = $dompdf; + } + $options = $dompdf->getOptions(); + + $this->_pdf = new \PDFLib(); + + $license = $options->getPdflibLicense(); + if (strlen($license) > 0) { + $this->setPDFLibParameter("license", $license); + } + + if ($this->getPDFLibMajorVersion() < 10) { + $this->setPDFLibParameter("textformat", "utf8"); + } + if ($this->getPDFLibMajorVersion() >= 7) { + $this->setPDFLibParameter("errorpolicy", "return"); + // $this->_pdf->set_option('logging={filename=' . \APP_PATH . '/logs/pdflib.log classes={api=1 warning=2}}'); + // $this->_pdf->set_option('errorpolicy=exception'); + } else { + $this->setPDFLibParameter("fontwarning", "false"); + } + + $searchPath = [$options->getFontDir(), $options->getRootDir() . "/lib/fonts"]; + if (empty($searchPath) === false) { + $this->_pdf->set_option('searchpath={{' . implode("} {", $searchPath) . '}}'); + } + + // fetch PDFLib version information for the producer field + $this->_pdf->set_info("Producer Addendum", sprintf("%s + PDFLib %s", $dompdf->version, $this->getPDFLibMajorVersion())); + + // Silence pedantic warnings about missing TZ settings + $tz = @date_default_timezone_get(); + date_default_timezone_set("UTC"); + $this->_pdf->set_info("Date", date("Y-m-d")); + date_default_timezone_set($tz); + + $doc_options = ""; + + if ($options->isPdfAEnabled()) { + $doc_options = "pdfa=PDF/A-3b autoxmp"; + } + + if (self::$IN_MEMORY) { + $this->_pdf->begin_document("", $doc_options); + } else { + $tmp_dir = $options->getTempDir(); + $tmp_name = @tempnam($tmp_dir, "libdompdf_pdf_"); + @unlink($tmp_name); + $this->_file = "$tmp_name.pdf"; + $this->_pdf->begin_document($this->_file, $doc_options); + } + + if ($options->isPdfAEnabled()) { + $iccProfilePath = $options->getRootDir() . '/lib/res/sRGB2014.icc'; + $this->_pdf->load_iccprofile($iccProfilePath, "usage=outputintent"); + } + + $this->_pdf->begin_page_ext($this->_width, $this->_height, ""); + + $this->_page_number = $this->_page_count = 1; + + $this->_imgs = []; + $this->_fonts = []; + $this->_objs = []; + } + + function get_dompdf() + { + return $this->_dompdf; + } + + /** + * Close the pdf + */ + protected function _close() + { + $this->_place_objects(); + + // Close all pages + $this->_pdf->suspend_page(""); + for ($p = 1; $p <= $this->_page_count; $p++) { + $this->_pdf->resume_page("pagenumber=$p"); + $this->_pdf->end_page_ext(""); + } + + $this->_pdf->end_document(""); + } + + + /** + * Returns the PDFLib instance + * + * @return PDFLib + */ + public function get_pdflib() + { + return $this->_pdf; + } + + public function add_info(string $label, string $value): void + { + $this->_pdf->set_info($label, $value); + } + + /** + * Opens a new 'object' (template in PDFLib-speak) + * + * While an object is open, all drawing actions are recorded to the + * object instead of 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 PDFLib::close_object() + * @see PDFLib::add_object() + * + * @return int + */ + public function open_object() + { + $this->_pdf->suspend_page(""); + if ($this->getPDFLibMajorVersion() >= 7) { + $ret = $this->_pdf->begin_template_ext($this->_width, $this->_height, ""); + } else { + $ret = $this->_pdf->begin_template($this->_width, $this->_height); + } + $this->_pdf->save(); + $this->_objs[$ret] = ["start_page" => $this->_page_number]; + + return $ret; + } + + /** + * Reopen an existing object (NOT IMPLEMENTED) + * PDFLib does not seem to support reopening templates. + * + * @param int $object the ID of a previously opened object + * + * @throws Exception + */ + public function reopen_object($object) + { + throw new Exception("PDFLib does not support reopening objects."); + } + + /** + * Close the current template + * + * @see PDFLib::open_object() + */ + public function close_object() + { + $this->_pdf->restore(); + if ($this->getPDFLibMajorVersion() >= 7) { + $this->_pdf->end_template_ext($this->_width, $this->_height); + } else { + $this->_pdf->end_template(); + } + $this->_pdf->resume_page("pagenumber=" . $this->_page_number); + } + + /** + * Adds the specified object to the document + * + * $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 + * + * @param int $object the object handle returned by open_object() + * @param string $where + */ + public function add_object($object, $where = 'all') + { + + if (mb_strpos($where, "next") !== false) { + $this->_objs[$object]["start_page"]++; + $where = str_replace("next", "", $where); + if ($where == "") { + $where = "add"; + } + } + + $this->_objs[$object]["where"] = $where; + } + + /** + * Stops the specified template 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) + { + + if (!isset($this->_objs[$object])) { + return; + } + + $start = $this->_objs[$object]["start_page"]; + $where = $this->_objs[$object]["where"]; + + // Place the object on this page if required + if ($this->_page_number >= $start && + (($this->_page_number % 2 == 0 && $where === "even") || + ($this->_page_number % 2 == 1 && $where === "odd") || + ($where === "all")) + ) { + $this->_pdf->fit_image($object, 0, 0, ""); + } + + $this->_objs[$object] = null; + unset($this->_objs[$object]); + } + + /** + * Add all active objects to the current page + */ + protected function _place_objects() + { + + foreach ($this->_objs as $obj => $props) { + $start = $props["start_page"]; + $where = $props["where"]; + + // Place the object on this page if required + if ($this->_page_number >= $start && + (($this->_page_number % 2 == 0 && $where === "even") || + ($this->_page_number % 2 == 1 && $where === "odd") || + ($where === "all")) + ) { + $this->_pdf->fit_image($obj, 0, 0, ""); + } + } + } + + 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; + } + + /** + * @param $num + */ + public function set_page_number($num) + { + $this->_page_number = (int)$num; + } + + public function set_page_count($count) + { + $this->_page_count = (int)$count; + } + + /** + * Sets the line style + * + * @param float $width + * @param string $cap + * @param string $join + * @param array $dash + */ + protected function _set_line_style($width, $cap, $join, $dash) + { + if (!is_array($dash)) { + $dash = []; + } + + // Work around PDFLib limitation with 0 dash length: + // Value 0 for option 'dasharray' is too small (minimum 1.5e-05) + foreach ($dash as &$d) { + if ($d == 0) { + $d = 1.5e-5; + } + } + + if (count($dash) === 1) { + $dash[] = $dash[0]; + } + + if ($this->getPDFLibMajorVersion() >= 9) { + if (count($dash) > 1) { + $this->_pdf->set_graphics_option("dasharray={" . implode(" ", $dash) . "}"); + } else { + $this->_pdf->set_graphics_option("dasharray=none"); + } + } else { + if (count($dash) > 1) { + $this->_pdf->setdashpattern("dasharray={" . implode(" ", $dash) . "}"); + } else { + $this->_pdf->setdash(0, 0); + } + } + + switch ($join) { + case "miter": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linejoin=0'); + } else { + $this->_pdf->setlinejoin(0); + } + break; + + case "round": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linejoin=1'); + } else { + $this->_pdf->setlinejoin(1); + } + break; + + case "bevel": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linejoin=2'); + } else { + $this->_pdf->setlinejoin(2); + } + break; + + default: + break; + } + + switch ($cap) { + case "butt": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linecap=0'); + } else { + $this->_pdf->setlinecap(0); + } + break; + + case "round": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linecap=1'); + } else { + $this->_pdf->setlinecap(1); + } + break; + + case "square": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linecap=2'); + } else { + $this->_pdf->setlinecap(2); + } + break; + + default: + break; + } + + $this->_pdf->setlinewidth($width); + } + + /** + * Sets the line color + * + * @param array $color array(r,g,b) + */ + protected function _set_stroke_color($color) + { + // TODO: we should check the current PDF stroke color + // instead of the cached value + if ($this->_last_stroke_color == $color) { + // FIXME: do nothing, this optimization is broken by the + // stroke being set as a side effect of other operations + //return; + } + + $alpha = isset($color["alpha"]) ? $color["alpha"] : 1; + if (isset($this->_current_opacity)) { + $alpha *= $this->_current_opacity; + } + + $this->_last_stroke_color = $color; + + if (isset($color[3])) { + $type = "cmyk"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], $color[2], $color[3]]; + } elseif (isset($color[2])) { + $type = "rgb"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], $color[2], 0]; + } else { + $type = "gray"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], 0, 0]; + } + + $this->_set_stroke_opacity($alpha, "Normal"); + $this->_pdf->setcolor("stroke", $type, $c1, $c2, $c3, $c4); + } + + /** + * Sets the fill color + * + * @param array $color array(r,g,b) + */ + protected function _set_fill_color($color) + { + // TODO: we should check the current PDF fill color + // instead of the cached value + if ($this->_last_fill_color == $color) { + // FIXME: do nothing, this optimization is broken by the + // fill being set as a side effect of other operations + //return; + } + + $alpha = isset($color["alpha"]) ? $color["alpha"] : 1; + if (isset($this->_current_opacity)) { + $alpha *= $this->_current_opacity; + } + + $this->_last_fill_color = $color; + + if (isset($color[3])) { + $type = "cmyk"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], $color[2], $color[3]]; + } elseif (isset($color[2])) { + $type = "rgb"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], $color[2], 0]; + } else { + $type = "gray"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], 0, 0]; + } + + $this->_set_fill_opacity($alpha, "Normal"); + $this->_pdf->setcolor("fill", $type, $c1, $c2, $c3, $c4); + } + + /** + * Sets the fill opacity + * + * @param float $opacity + * @param string $mode + */ + public function _set_fill_opacity($opacity, $mode = "Normal") + { + if ($mode === "Normal" && isset($opacity)) { + $this->_set_gstate("opacityfill=$opacity"); + } + } + + /** + * Sets the stroke opacity + * + * @param float $opacity + * @param string $mode + */ + public function _set_stroke_opacity($opacity, $mode = "Normal") + { + if ($mode === "Normal" && isset($opacity)) { + $this->_set_gstate("opacitystroke=$opacity"); + } + } + + public function set_opacity(float $opacity, string $mode = "Normal"): void + { + if ($mode === "Normal") { + $this->_set_gstate("opacityfill=$opacity opacitystroke=$opacity"); + $this->_current_opacity = $opacity; + } + } + + /** + * Sets the gstate + * + * @param $gstate_options + * @return int + */ + public function _set_gstate($gstate_options) + { + if (($gstate = array_search($gstate_options, $this->_gstates)) === false) { + $gstate = $this->_pdf->create_gstate($gstate_options); + $this->_gstates[$gstate] = $gstate_options; + } + + return $this->_pdf->set_gstate($gstate); + } + + public function set_default_view($view, $options = []) + { + // TODO + // http://www.pdflib.com/fileadmin/pdflib/pdf/manuals/PDFlib-8.0.2-API-reference.pdf + /** + * fitheight Fit the page height to the window, with the x coordinate left at the left edge of the window. + * fitrect Fit the rectangle specified by left, bottom, right, and top to the window. + * fitvisible Fit the visible contents of the page (the ArtBox) to the window. + * fitvisibleheight Fit the visible contents of the page to the window with the x coordinate left at the left edge of the window. + * fitvisiblewidth Fit the visible contents of the page to the window with the y coordinate top at the top edge of the window. + * fitwidth Fit the page width to the window, with the y coordinate top at the top edge of the window. + * fitwindow Fit the complete page to the window. + * fixed + */ + //$this->setPDFLibParameter("openaction", $view); + } + + /** + * Loads a specific font and stores the corresponding descriptor. + * + * @param string $font + * @param string $encoding + * @param string $options + * + * @return int the font descriptor for the font + */ + protected function _load_font($font, $encoding = null, $options = "") + { + // Fix for PDFLib's case-sensitive font names + $baseFont = basename($font); + $isNativeFont = false; + $lcBaseFont = strtolower($baseFont); + if (isset(self::$nativeFontsToPDFLib[$lcBaseFont])) { + $baseFont = self::$nativeFontsToPDFLib[$lcBaseFont]; + $isNativeFont = true; + } + + // Embed non-native fonts + if (!$isNativeFont) { + $options .= " embedding=true"; + } + + $options .= " autosubsetting=" . ($this->_dompdf->getOptions()->getIsFontSubsettingEnabled() === false ? "false" : "true"); + + if (is_null($encoding)) { + // Unicode encoding is only available for the commerical + // version of PDFlib and not PDFlib-Lite + if (strlen($this->_dompdf->getOptions()->getPdflibLicense()) > 0) { + $encoding = "unicode"; + } else { + $encoding = "auto"; + } + } + + $key = "$font:$encoding:$options"; + if (isset($this->_fonts[$key])) { + return $this->_fonts[$key]; + } + + // Native fonts are build in, just load it + if ($isNativeFont) { + $this->_fonts[$key] = $this->_pdf->load_font($baseFont, $encoding, $options); + return $this->_fonts[$key]; + } + + $fontOutline = $this->getPDFLibParameter("FontOutline", 1); + if ($fontOutline === "" || $fontOutline < 0) { + $families = $this->_dompdf->getFontMetrics()->getFontFamilies(); + foreach ($families as $files) { + foreach ($files as $file) { + $face = basename($file); + $afm = null; + + if (isset($this->_fontsFiles[$face])) { + continue; + } + + // Prefer ttfs to afms + if (file_exists("$file.ttf")) { + $outline = "$file.ttf"; + } elseif (file_exists("$file.TTF")) { + $outline = "$file.TTF"; + } elseif (file_exists("$file.pfb")) { + $outline = "$file.pfb"; + if (file_exists("$file.afm")) { + $afm = "$file.afm"; + } + } elseif (file_exists("$file.PFB")) { + $outline = "$file.PFB"; + if (file_exists("$file.AFM")) { + $afm = "$file.AFM"; + } + } else { + continue; + } + + $this->_fontsFiles[$face] = true; + + if ($this->getPDFLibMajorVersion() >= 9) { + $this->setPDFLibParameter("FontOutline", '{' . "$face=$outline" . '}'); + } else { + $this->setPDFLibParameter("FontOutline", "\{$face\}=\{$outline\}"); + } + + if (is_null($afm)) { + continue; + } + if ($this->getPDFLibMajorVersion() >= 9) { + $this->setPDFLibParameter("FontAFM", '{' . "$face=$afm" . '}'); + } else { + $this->setPDFLibParameter("FontAFM", "\{$face\}=\{$afm\}"); + } + } + } + } + + $this->_fonts[$key] = $this->_pdf->load_font($baseFont, $encoding, $options); + + return $this->_fonts[$key]; + } + + /** + * 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_line_style($width, $cap, "", $style); + $this->_set_stroke_color($color); + + $y1 = $this->y($y1); + $y2 = $this->y($y2); + + $this->_pdf->moveto($x1, $y1); + $this->_pdf->lineto($x2, $y2); + $this->_pdf->stroke(); + + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + public function arc($x, $y, $r1, $r2, $astart, $aend, $color, $width, $style = [], $cap = "butt") + { + $this->_set_line_style($width, $cap, "", $style); + $this->_set_stroke_color($color); + + $y = $this->y($y); + + $this->_pdf->arc($x, $y, $r1, $astart, $aend); + $this->_pdf->stroke(); + + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + public function rectangle($x1, $y1, $w, $h, $color, $width, $style = [], $cap = "butt") + { + $this->_set_stroke_color($color); + $this->_set_line_style($width, $cap, "", $style); + + $y1 = $this->y($y1) - $h; + + $this->_pdf->rect($x1, $y1, $w, $h); + $this->_pdf->stroke(); + + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + public function filled_rectangle($x1, $y1, $w, $h, $color) + { + $this->_set_fill_color($color); + + $y1 = $this->y($y1) - $h; + + $this->_pdf->rect(floatval($x1), floatval($y1), floatval($w), floatval($h)); + $this->_pdf->fill(); + + $this->_set_fill_opacity($this->_current_opacity, "Normal"); + } + + public function clipping_rectangle($x1, $y1, $w, $h) + { + $this->_pdf->save(); + + $y1 = $this->y($y1) - $h; + + $this->_pdf->rect(floatval($x1), floatval($y1), floatval($w), floatval($h)); + $this->_pdf->clip(); + } + + public function clipping_roundrectangle($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL) + { + if ($this->getPDFLibMajorVersion() < 9) { + $this->clipping_rectangle($x1, $y1, $w, $h); + return; + } + + $this->_pdf->save(); + + // we use 0,0 for the base coordinates for the path points + // since we're drawing the path at the $x1,$y1 coordinates + + $path = 0; + //start: left edge, top end + $path = $this->_pdf->add_path_point($path, 0, 0 - $rTL + $h, "move", ""); + // line: left edge, bottom end + $path = $this->_pdf->add_path_point($path, 0, 0 + $rBL, "line", ""); + // curve: bottom-left corner + if ($rBL > 0) { + $path = $this->_pdf->add_path_point($path, 0 + $rBL, 0, "elliptical", "radius=$rBL clockwise=false"); + } + // line: bottom edge, left end + $path = $this->_pdf->add_path_point($path, 0 - $rBR + $w, 0, "line", ""); + // curve: bottom-right corner + if ($rBR > 0) { + $path = $this->_pdf->add_path_point($path, 0 + $w, 0 + $rBR, "elliptical", "radius=$rBR clockwise=false"); + } + // line: right edge, top end + $path = $this->_pdf->add_path_point($path, 0 + $w, 0 - $rTR + $h, "line", ""); + // curve: top-right corner + if ($rTR > 0) { + $path = $this->_pdf->add_path_point($path, 0 - $rTR + $w, 0 + $h, "elliptical", "radius=$rTR clockwise=false"); + } + // line: top edge, left end + $path = $this->_pdf->add_path_point($path, 0 + $rTL, 0 + $h, "line", ""); + // curve: top-left corner + if ($rTL > 0) { + $path = $this->_pdf->add_path_point($path, 0, 0 - $rTL + $h, "elliptical", "radius=$rTL clockwise=false"); + } + $this->_pdf->draw_path($path, $x1, $this->_height-$y1-$h, "clip=true"); + } + + public function clipping_polygon(array $points): void + { + $this->_pdf->save(); + + $y = $this->y(array_pop($points)); + $x = array_pop($points); + $this->_pdf->moveto($x, $y); + + while (count($points) > 1) { + $y = $this->y(array_pop($points)); + $x = array_pop($points); + $this->_pdf->lineto($x, $y); + } + + $this->_pdf->closepath(); + $this->_pdf->clip(); + } + + public function clipping_end() + { + $this->_pdf->restore(); + } + + public function save() + { + $this->_pdf->save(); + } + + function restore() + { + $this->_pdf->restore(); + } + + public function rotate($angle, $x, $y) + { + $pdf = $this->_pdf; + $pdf->translate($x, $this->_height - $y); + $pdf->rotate(-$angle); + $pdf->translate(-$x, -$this->_height + $y); + } + + public function skew($angle_x, $angle_y, $x, $y) + { + $pdf = $this->_pdf; + $pdf->translate($x, $this->_height - $y); + $pdf->skew($angle_y, $angle_x); // Needs to be inverted + $pdf->translate(-$x, -$this->_height + $y); + } + + public function scale($s_x, $s_y, $x, $y) + { + $pdf = $this->_pdf; + $pdf->translate($x, $this->_height - $y); + $pdf->scale($s_x, $s_y); + $pdf->translate(-$x, -$this->_height + $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->concat($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); + } + + $y = $this->y(array_pop($points)); + $x = array_pop($points); + $this->_pdf->moveto($x, $y); + + while (count($points) > 1) { + $y = $this->y(array_pop($points)); + $x = array_pop($points); + $this->_pdf->lineto($x, $y); + } + + if ($fill) { + $this->_pdf->fill(); + } else { + $this->_pdf->closepath_stroke(); + } + + $this->_set_fill_opacity($this->_current_opacity, "Normal"); + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + 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); + } + + $y = $this->y($y); + + $this->_pdf->circle($x, $y, $r); + + if ($fill) { + $this->_pdf->fill(); + } else { + $this->_pdf->stroke(); + } + + $this->_set_fill_opacity($this->_current_opacity, "Normal"); + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + /** + * 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 (method_exists(Helpers::class, $func_name)) { + $func_name = [Helpers::class, $func_name]; + } elseif (!function_exists($func_name)) { + throw new Exception("Function $func_name() not found. Cannot convert $type image: $image_url. Please install the image PHP extension."); + } + + 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); + if (PHP_MAJOR_VERSION < 8) { + 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") + { + $w = (int)$w; + $h = (int)$h; + + $img_type = Cache::detect_type($img, $this->get_dompdf()->getHttpContext()); + + // Strip file:// prefix + if (substr($img, 0, 7) === "file://") { + $img = substr($img, 7); + } + + if (!isset($this->_imgs[$img])) { + switch (strtolower($img_type)) { + case "webp": + $img = $this->_convert_to_png($img, $img_type); + if ($img === null) { + $img = Cache::$broken_image; + } + $this->image($img, $x, $y, $w, $h, $resolution); + return; + case "gif": + if ($this->getPDFLibMajorVersion() >= 10) { + $img = $this->_convert_to_png($img, $img_type); + if ($img === null) { + $img = Cache::$broken_image; + } + $this->image($img, $x, $y, $w, $h, $resolution); + return; + } + case "bmp": + /** @noinspection PhpMissingBreakStatementInspection */ + case "jpeg": + /** @noinspection PhpMissingBreakStatementInspection */ + case "png": + $image_load_response = $this->_pdf->load_image($img_type, $img, ""); + break; + case "svg": + $image_load_response = $this->_pdf->load_graphics($img_type, $img, ""); + break; + default: + // not handled + $this->image(Cache::$broken_image, $x, $y, $w, $h, $resolution); + return; + } + if ($image_load_response === 0) { + //TODO: should do something with the error message + $error = $this->_pdf->get_errmsg(); + return; + } + $this->_imgs[$img] = $image_load_response; + } + + $img = $this->_imgs[$img]; + + $y = $this->y($y) - $h; + if (strtolower($img_type) === "svg") { + $this->_pdf->fit_graphics($img, $x, $y, 'boxsize={' . "$w $h" . '} fitmethod=entire'); + } else { + $this->_pdf->fit_image($img, $x, $y, 'boxsize={' . "$w $h" . '} fitmethod=entire'); + } + } + + public function text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_spacing = 0, $char_spacing = 0, $angle = 0) + { + if ($size == 0) { + return; + } + + $fh = $this->_load_font($font); + + $this->_pdf->setfont($fh, $size); + $this->_set_fill_color($color); + + $y = $this->y($y) - $this->get_font_height($font, $size); + + $word_spacing = (float)$word_spacing; + $char_spacing = (float)$char_spacing; + $angle = -(float)$angle; + + $this->_pdf->fit_textline($text, $x, $y, "rotate=$angle wordspacing=$word_spacing charspacing=$char_spacing "); + + $this->_set_fill_opacity($this->_current_opacity, "Normal"); + } + + public function javascript($code) + { + if (strlen($this->_dompdf->getOptions()->getPdflibLicense()) > 0) { + $this->_pdf->create_action("JavaScript", $code); + } + } + + public function add_named_dest($anchorname) + { + $this->_pdf->add_nameddest($anchorname, ""); + } + + 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->create_annotation($x, $y, $x + $width, $y + $height, 'Link', + "contents={$url} destname=" . substr($url, 1) . " linewidth=0"); + } + } else { + //TODO: PDFLib::create_action does not permit non-HTTP links for URI actions + $action = $this->_pdf->create_action("URI", "url={{$url}}"); + // add the annotation only if the action was created + if ($action !== 0) { + $this->_pdf->create_annotation($x, $y, $x + $width, $y + $height, 'Link', "contents={{$url}} action={activate=$action} linewidth=0"); + } + } + } + + public function font_supports_char(string $font, string $char): bool + { + if ($char === "") { + return true; + } + + $fh = $this->_load_font($font); + if ($fh === 0) { + return false; + } + $this->_pdf->setfont($fh, 10); + + // unicode character glyph id lookup supports both the character and the unicode ordinal value + // because some characters can not be specified directly we'll specify the ordinal for all characters + // known problematic characters: "{", "}", " ", "=", "\u{feff}" + $char_code = Helpers::uniord($char, "UTF-8"); + $options = "unicode=$char_code"; + $glyphid = (int) $this->_pdf->info_font($fh, "glyphid", $options); + + return $glyphid !== -1; + } + + public function get_text_width($text, $font, $size, $word_spacing = 0.0, $letter_spacing = 0.0) + { + if ($size == 0) { + return 0.0; + } + + $fh = $this->_load_font($font); + + // Determine the additional width due to extra spacing + $num_spaces = mb_substr_count($text, " "); + $delta = $word_spacing * $num_spaces; + + if ($letter_spacing) { + $num_chars = mb_strlen($text, "UTF-8"); + $delta += $num_chars * $letter_spacing; + } + + return $this->_pdf->stringwidth($text, $fh, $size) + $delta; + } + + public function get_font_height($font, $size) + { + if ($size == 0) { + return 0.0; + } + + $fh = $this->_load_font($font); + + $this->_pdf->setfont($fh, $size); + + $asc = $this->_pdf->info_font($fh, "ascender", "fontsize=$size"); + $desc = $this->_pdf->info_font($fh, "descender", "fontsize=$size"); + + // $desc is usually < 0, + $ratio = $this->_dompdf->getOptions()->getFontHeightRatio(); + + return (abs($asc) + abs($desc)) * $ratio; + } + + public function get_font_baseline($font, $size) + { + $ratio = $this->_dompdf->getOptions()->getFontHeightRatio(); + + return $this->get_font_height($font, $size) / $ratio * 1.1; + } + + /** + * 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); + }); + } + + public function new_page() + { + // Add objects to the current page + $this->_place_objects(); + + $this->_pdf->suspend_page(""); + $this->_pdf->begin_page_ext($this->_width, $this->_height, ""); + $this->_page_number = ++$this->_page_count; + } + + protected function processPageScript(callable $callback): void + { + $this->_pdf->suspend_page(""); + + for ($p = 1; $p <= $this->_page_count; $p++) { + $this->_pdf->resume_page("pagenumber=$p"); + + $fontMetrics = $this->_dompdf->getFontMetrics(); + $callback($p, $this->_page_count, $this, $fontMetrics); + + $this->_pdf->suspend_page(""); + } + + $this->_pdf->resume_page("pagenumber=" . $this->_page_number); + } + + /** + * @throws Exception + */ + 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; + } + + if ($options["compress"]) { + $this->setPDFLibValue("compress", 6); + } else { + $this->setPDFLibValue("compress", 0); + } + + $this->_close(); + + $data = ""; + + if (self::$IN_MEMORY) { + $data = $this->_pdf->get_buffer(); + $size = mb_strlen($data, "8bit"); + } else { + $size = filesize($this->_file); + } + + header("Content-Type: application/pdf"); + header("Content-Length: " . $size); + + $filename = str_replace(["\n", "'"], "", basename($filename, ".pdf")) . ".pdf"; + $attachment = $options["Attachment"] ? "attachment" : "inline"; + header(Helpers::buildContentDispositionHeader($attachment, $filename)); + + if (self::$IN_MEMORY) { + echo $data; + } else { + // Chunked readfile() + $chunk = (1 << 21); // 2 MB + $fh = fopen($this->_file, "rb"); + if (!$fh) { + throw new Exception("Unable to load temporary PDF file: " . $this->_file); + } + + while (!feof($fh)) { + echo fread($fh, $chunk); + } + fclose($fh); + + //debugpng + if ($this->_dompdf->getOptions()->getDebugPng()) { + print '[pdflib stream unlink ' . $this->_file . ']'; + } + if (!$this->_dompdf->getOptions()->getDebugKeepTemp()) { + unlink($this->_file); + } + $this->_file = null; + unset($this->_file); + } + + flush(); + } + + public function output($options = []) + { + if (!isset($options["compress"])) { + $options["compress"] = true; + } + + if ($options["compress"]) { + $this->setPDFLibValue("compress", 6); + } else { + $this->setPDFLibValue("compress", 0); + } + + $this->_close(); + + if (self::$IN_MEMORY) { + $data = $this->_pdf->get_buffer(); + } else { + $data = file_get_contents($this->_file); + + //debugpng + if ($this->_dompdf->getOptions()->getDebugPng()) { + print '[pdflib output unlink ' . $this->_file . ']'; + } + if (!$this->_dompdf->getOptions()->getDebugKeepTemp()) { + unlink($this->_file); + } + $this->_file = null; + unset($this->_file); + } + + return $data; + } + + /** + * @param string $keyword + * @param string $optlist + * @return mixed + */ + protected function getPDFLibParameter($keyword, $optlist = "") + { + if ($this->getPDFLibMajorVersion() >= 9) { + return $this->_pdf->get_option($keyword, ""); + } + + return $this->_pdf->get_parameter($keyword, $optlist); + } + + /** + * @param string $keyword + * @param string $value + * @return mixed + */ + protected function setPDFLibParameter($keyword, $value) + { + if ($this->getPDFLibMajorVersion() >= 9) { + return $this->_pdf->set_option($keyword . "=" . $value); + } + + return $this->_pdf->set_parameter($keyword, $value); + } + + /** + * @param string $keyword + * @param string $optlist + * @return mixed + */ + protected function getPDFLibValue($keyword, $optlist = "") + { + if ($this->getPDFLibMajorVersion() >= 9) { + return $this->getPDFLibParameter($keyword, $optlist); + } + + return $this->_pdf->get_value($keyword); + } + + /** + * @param string $keyword + * @param string $value + * @return mixed + */ + protected function setPDFLibValue($keyword, $value) + { + if ($this->getPDFLibMajorVersion() >= 9) { + return $this->setPDFLibParameter($keyword, $value); + } + + return $this->_pdf->set_value($keyword, $value); + } + + /** + * @return int + */ + protected function getPDFLibMajorVersion() + { + if (is_null(self::$MAJOR_VERSION)) { + if (method_exists($this->_pdf, "get_option")) { + self::$MAJOR_VERSION = abs(intval($this->_pdf->get_option("major", ""))); + } else { + self::$MAJOR_VERSION = abs(intval($this->_pdf->get_value("major", ""))); + } + } + + return self::$MAJOR_VERSION; + } +} + +// Workaround for idiotic limitation on statics... +PDFLib::$PAPER_SIZES = CPDF::$PAPER_SIZES; diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Canvas.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Canvas.php new file mode 100644 index 0000000..7199456 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Canvas.php @@ -0,0 +1,487 @@ + 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 ... 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; + + /** + * Determines if the font supports the given character + * + * @param string $font The font file to use + * @param string $char The character to check + * + * @return bool + */ + function font_supports_char(string $font, string $char): bool; + + /** + * 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 = []); +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/CanvasFactory.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/CanvasFactory.php new file mode 100644 index 0000000..4449d5d --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/CanvasFactory.php @@ -0,0 +1,68 @@ +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 (class_exists($backend, false)) { + $class = $backend; + } elseif ($backend === "gd" && extension_loaded('gd')) { + $class = "Dompdf\\Adapter\\GD"; + } else { + $class = "Dompdf\\Adapter\\CPDF"; + } + } + } + + $instance = new $class($paper, $orientation, $dompdf); + + $class_interfaces = class_implements($class, false); + if (!$class_interfaces || !in_array("Dompdf\\Canvas", $class_interfaces)) { + $class = "Dompdf\\Adapter\\CPDF"; + $instance = new $class($paper, $orientation, $dompdf); + } + + return $instance; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Cellmap.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Cellmap.php new file mode 100644 index 0000000..f6f3174 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Cellmap.php @@ -0,0 +1,1009 @@ + 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 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) 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"); + } + } + + if ($frame !== $this->_table) { + // Clear borders for rows and row groups. For the collapsed + // model, they have been resolved and are used by the cells now. + // For the separated model, they are ignored per spec + $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; + } + + // Column calculation for fixed-layout tables should ony use the first row's cells + // https://www.w3.org/TR/CSS2/tables.html#fixed-table-layout + if ($this->_fixed_layout && !in_array(0, $frame_info["rows"], true)) { + 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->_fixed_layout ? 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->_fixed_layout ? 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:
"; + $str .= Helpers::pre_r($this->_columns, true); + $str .= "Rows:
"; + $str .= Helpers::pre_r($this->_rows, true); + + $str .= "Frames:
"; + $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(["
", "", ""], + ["\n", chr(27) . "[01;33m", chr(27) . "[0m"], + $str)); + } + + return $str; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/AttributeTranslator.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/AttributeTranslator.php new file mode 100644 index 0000000..29a4994 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/AttributeTranslator.php @@ -0,0 +1,680 @@ + 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' => '_set_list_style_type', + ], + 'ul' => [ + 'compact' => 'margin: 0.5em 0;', + 'type' => '_set_list_style_type', + ], + 'li' => [ + 'type' => '_set_list_style_type', + '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 - (float)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 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, "; "); + } + + protected static function _set_list_style_type(\DOMElement $node, string $value): string + { + $v = trim($value); + + switch ($v) { + case "1": + $type = "decimal"; + break; + case "a": + $type = "lower-alpha"; + break; + case "A": + $type = "upper-alpha"; + break; + case "i": + $type = "lower-roman"; + break; + case "I": + $type = "upper-roman"; + break; + default: + $type = $v; + break; + } + + return "list-style-type: $type;"; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Color.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Color.php new file mode 100644 index 0000000..d92d67d --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Color.php @@ -0,0 +1,339 @@ + "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; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/Attr.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/Attr.php new file mode 100644 index 0000000..6a227dd --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/Attr.php @@ -0,0 +1,26 @@ +attribute = $attribute; + } + + public function equals(ContentPart $other): bool + { + return $other instanceof self + && $other->attribute === $this->attribute; + } + + public function __toString(): string + { + return "attr($this->attribute)"; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/CloseQuote.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/CloseQuote.php new file mode 100644 index 0000000..88c4af4 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/CloseQuote.php @@ -0,0 +1,10 @@ +name = $name; + $this->style = $style; + } + + public function equals(ContentPart $other): bool + { + return $other instanceof self + && $other->name === $this->name + && $other->style === $this->style; + } + + public function __toString(): string + { + return "counter($this->name, $this->style)"; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/Counters.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/Counters.php new file mode 100644 index 0000000..30ca018 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/Counters.php @@ -0,0 +1,40 @@ +name = $name; + $this->string = $string; + $this->style = $style; + } + + public function equals(ContentPart $other): bool + { + return $other instanceof self + && $other->name === $this->name + && $other->string === $this->string + && $other->style === $this->style; + } + + public function __toString(): string + { + return "counters($this->name, \"$this->string\", $this->style)"; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/NoCloseQuote.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/NoCloseQuote.php new file mode 100644 index 0000000..2041105 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/NoCloseQuote.php @@ -0,0 +1,10 @@ +string = $string; + } + + public function equals(ContentPart $other): bool + { + return $other instanceof self + && $other->string === $this->string; + } + + public function __toString(): string + { + return '"' . $this->string . '"'; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/Url.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/Url.php new file mode 100644 index 0000000..d1ca5a2 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Content/Url.php @@ -0,0 +1,26 @@ +url = $url; + } + + public function equals(ContentPart $other): bool + { + return $other instanceof self + && $other->url === $this->url; + } + + public function __toString(): string + { + return "url(\"" . str_replace("\"", "\\\"", $this->url) . "\")"; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Style.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Style.php new file mode 100644 index 0000000..aa575ac --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Style.php @@ -0,0 +1,4631 @@ +margin_top = 10.0; + * echo $style->margin_top; // Returns `10.0` + * ``` + * + * To declare a property from a string, use {@link Style::set_prop()}: + * + * ``` + * $style->set_prop("margin_top", "1em"); + * echo $style->get_specified("margin_top"); // Returns `1em` + * echo $style->margin_top; // Returns `12.0`, assuming the default font size + * ``` + * + * Actual CSS parsing is performed in the {@link Stylesheet} class. + * + * @property string $azimuth + * @property string $background_attachment + * @property array|string $background_color + * @property string $background_image Image URL or `none` + * @property string $background_image_resolution + * @property array $background_position Pair of `[x, y]`, each value being a length in pt or a percentage value + * @property string $background_repeat + * @property array|string $background_size `cover`, `contain`, or `[width, height]`, each being a length, percentage, or `auto` + * @property string $border_collapse + * @property string $border_color Only use for setting all sides to the same color + * @property float[] $border_spacing Pair of `[horizontal, vertical]` spacing + * @property string $border_style Only use for setting all sides to the same style + * @property array|string $border_top_color + * @property array|string $border_right_color + * @property array|string $border_bottom_color + * @property array|string $border_left_color + * @property string $border_top_style Valid border style + * @property string $border_right_style Valid border style + * @property string $border_bottom_style Valid border style + * @property string $border_left_style Valid border style + * @property float $border_top_width Length in pt + * @property float $border_right_width Length in pt + * @property float $border_bottom_width Length in pt + * @property float $border_left_width Length in pt + * @property string $border_width Only use for setting all sides to the same width + * @property float|string $border_bottom_left_radius Radius in pt or a percentage value + * @property float|string $border_bottom_right_radius Radius in pt or a percentage value + * @property float|string $border_top_left_radius Radius in pt or a percentage value + * @property float|string $border_top_right_radius Radius in pt or a percentage value + * @property string $border_radius Only use for setting all corners to the same radius + * @property float|string $bottom Length in pt, a percentage value, or `auto` + * @property string $caption_side + * @property string $clear + * @property string $clip + * @property array|string $color + * @property ContentPart[]|string $content List of content components, `normal`, or `none` + * @property array|string $counter_increment Array defining the counters to increment or `none` + * @property array|string $counter_reset Array defining the counters to reset or `none` + * @property string $cue_after + * @property string $cue_before + * @property string $cue + * @property string $cursor + * @property string $direction + * @property string $display + * @property string $elevation + * @property string $empty_cells + * @property string $float + * @property string $font_family + * @property float $font_size Length in pt + * @property string $font_style `normal`, `italic`, or `oblique` + * @property string $font_variant + * @property int $font_weight Number in the range [1, 1000] + * @property float|string $height Length in pt, a percentage value, or `auto` + * @property string $image_resolution + * @property string $inset Only use for setting all box insets to the same length + * @property float|string $left Length in pt, a percentage value, or `auto` + * @property float $letter_spacing Length in pt + * @property float $line_height Length in pt + * @property string $list_style_image Image URL or `none` + * @property string $list_style_position `inside` or `outside` + * @property string $list_style_type + * @property float|string $margin_right Length in pt, a percentage value, or `auto` + * @property float|string $margin_left Length in pt, a percentage value, or `auto` + * @property float|string $margin_top Length in pt, a percentage value, or `auto` + * @property float|string $margin_bottom Length in pt, a percentage value, or `auto` + * @property string $margin Only use for setting all sides to the same length + * @property float|string $max_height Length in pt, a percentage value, or `none` + * @property float|string $max_width Length in pt, a percentage value, or `none` + * @property float|string $min_height Length in pt, a percentage value, or `auto` + * @property float|string $min_width Length in pt, a percentage value, or `auto` + * @property float $opacity Number in the range [0, 1] + * @property int $orphans + * @property array|string $outline_color + * @property string $outline_style Valid border style, except for `hidden` + * @property float $outline_width Length in pt + * @property float $outline_offset Length in pt + * @property string $overflow + * @property string $overflow_wrap + * @property float|string $padding_top Length in pt or a percentage value + * @property float|string $padding_right Length in pt or a percentage value + * @property float|string $padding_bottom Length in pt or a percentage value + * @property float|string $padding_left Length in pt or a percentage value + * @property string $padding Only use for setting all sides to the same length + * @property string $page_break_after + * @property string $page_break_before + * @property string $page_break_inside + * @property string $pause_after + * @property string $pause_before + * @property string $pause + * @property string $pitch_range + * @property string $pitch + * @property string $play_during + * @property string $position + * @property array|string $quotes List of quote pairs, or `none` + * @property string $richness + * @property float|string $right Length in pt, a percentage value, or `auto` + * @property float[]|string $size Pair of `[width, height]` or `auto` + * @property string $speak_header + * @property string $speak_numeral + * @property string $speak_punctuation + * @property string $speak + * @property string $speech_rate + * @property string $src + * @property string $stress + * @property string $table_layout + * @property string $text_align + * @property string $text_decoration + * @property float|string $text_indent Length in pt or a percentage value + * @property string $text_transform + * @property float|string $top Length in pt, a percentage value, or `auto` + * @property array $transform List of transforms + * @property array $transform_origin Triplet of `[x, y, z]`, each value being a length in pt, or a percentage value for x and y + * @property string $unicode_bidi + * @property string $unicode_range + * @property string $vertical_align + * @property string $visibility + * @property string $voice_family + * @property string $volume + * @property string $white_space + * @property int $widows + * @property float|string $width Length in pt, a percentage value, or `auto` + * @property string $word_break + * @property float $word_spacing Length in pt + * @property int|string $z_index Integer value or `auto` + * @property string $_dompdf_keep + * + * @package dompdf + */ +class Style +{ + protected const CSS_IDENTIFIER = "-?[_a-zA-Z]+[_a-zA-Z0-9-]*"; + protected const CSS_INTEGER = "[+-]?\d+"; + protected const CSS_NUMBER = "[+-]?\d*\.?\d+(?:[eE][+-]?\d+)?"; + protected const CSS_STRING = "" . + '"(?>(?:\\\\["]|[^"])*)(?(?:\\\\[']|[^'])*)(? true, + // Comparison Functions + "min" => true, + "max" => true, + "clamp" => true, + // Stepped Value Functions + "round" => true, // Not fully supported + "mod" => true, + "rem" => true, + // Trigonometric Functions + "sin" => true, + "cos" => true, + "tan" => true, + "asin" => true, + "acos" => true, + "atan" => true, + "atan2" => true, + // Exponential Functions + "pow" => true, + "sqrt" => true, + "hypot" => true, + "log" => true, + "exp" => true, + // Sign-Related Functions + "abs" => true, + "sign" => true + ]; + + /** + * https://www.w3.org/TR/css-values-3/#custom-idents + */ + protected const CUSTOM_IDENT_FORBIDDEN = ["inherit", "initial", "unset", "default"]; + + /** + * Default font size, in points. + * + * @var float + */ + public static $default_font_size = 12; + + /** + * Default line height, as a fraction of the font size. + * + * @var float + */ + public static $default_line_height = 1.2; + + /** + * Default "absolute" font sizes relative to the default font-size + * https://www.w3.org/TR/css-fonts-3/#absolute-size-value + * + * @var array + */ + public static $font_size_keywords = [ + "xx-small" => 0.6, // 3/5 + "x-small" => 0.75, // 3/4 + "small" => 0.889, // 8/9 + "medium" => 1, // 1 + "large" => 1.2, // 6/5 + "x-large" => 1.5, // 3/2 + "xx-large" => 2.0, // 2/1 + ]; + + /** + * List of valid text-align keywords. + */ + public const TEXT_ALIGN_KEYWORDS = ["left", "right", "center", "justify"]; + + /** + * List of valid vertical-align keywords. + */ + public const VERTICAL_ALIGN_KEYWORDS = ["baseline", "bottom", "middle", + "sub", "super", "text-bottom", "text-top", "top"]; + + /** + * List of all block-level (outer) display types. + * * https://www.w3.org/TR/css-display-3/#display-type + * * https://www.w3.org/TR/css-display-3/#block-level + */ + public const BLOCK_LEVEL_TYPES = [ + "block", + // "flow-root", + "list-item", + // "flex", + // "grid", + "table" + ]; + + /** + * List of all inline-level (outer) display types. + * * https://www.w3.org/TR/css-display-3/#display-type + * * https://www.w3.org/TR/css-display-3/#inline-level + */ + public const INLINE_LEVEL_TYPES = [ + "inline", + "inline-block", + // "inline-flex", + // "inline-grid", + "inline-table" + ]; + + /** + * List of all table-internal (outer) display types. + * * https://www.w3.org/TR/css-display-3/#layout-specific-display + */ + public const TABLE_INTERNAL_TYPES = [ + "table-row-group", + "table-header-group", + "table-footer-group", + "table-row", + "table-cell", + "table-column-group", + "table-column", + "table-caption" + ]; + + /** + * List of all inline (inner) display types. + */ + public const INLINE_TYPES = ["inline"]; + + /** + * List of all block (inner) display types. + */ + public const BLOCK_TYPES = ["block", "inline-block", "table-cell", "list-item"]; + + /** + * List of all table (inner) display types. + */ + public const TABLE_TYPES = ["table", "inline-table"]; + + /** + * Lookup table for valid display types. Initially computed from the + * different constants. + * + * @var array + */ + protected static $valid_display_types = []; + + /** + * List of all positioned types. + */ + public const POSITIONED_TYPES = ["relative", "absolute", "fixed"]; + + /** + * List of valid border styles. + */ + public const BORDER_STYLES = [ + "none", "hidden", + "dotted", "dashed", "solid", + "double", "groove", "ridge", "inset", "outset" + ]; + + /** + * List of valid outline-style values. + * Same as the border styles, except `auto` is allowed, `hidden` is not. + * + * @link https://www.w3.org/TR/css-ui-4/#typedef-outline-line-style + */ + protected const OUTLINE_STYLES = [ + "auto", "none", + "dotted", "dashed", "solid", + "double", "groove", "ridge", "inset", "outset" + ]; + + /** + * Map of CSS shorthand properties and their corresponding sub-properties. + * The order of the sub-properties is relevant for the fallback getter, + * which is used in case no specific getter method is defined. + * + * @var array + */ + protected static $_props_shorthand = [ + "background" => [ + "background_image", + "background_position", + "background_size", + "background_repeat", + // "background_origin", + // "background_clip", + "background_attachment", + "background_color" + ], + "border" => [ + "border_top_width", + "border_right_width", + "border_bottom_width", + "border_left_width", + "border_top_style", + "border_right_style", + "border_bottom_style", + "border_left_style", + "border_top_color", + "border_right_color", + "border_bottom_color", + "border_left_color" + ], + "border_top" => [ + "border_top_width", + "border_top_style", + "border_top_color" + ], + "border_right" => [ + "border_right_width", + "border_right_style", + "border_right_color" + ], + "border_bottom" => [ + "border_bottom_width", + "border_bottom_style", + "border_bottom_color" + ], + "border_left" => [ + "border_left_width", + "border_left_style", + "border_left_color" + ], + "border_width" => [ + "border_top_width", + "border_right_width", + "border_bottom_width", + "border_left_width" + ], + "border_style" => [ + "border_top_style", + "border_right_style", + "border_bottom_style", + "border_left_style" + ], + "border_color" => [ + "border_top_color", + "border_right_color", + "border_bottom_color", + "border_left_color" + ], + "border_radius" => [ + "border_top_left_radius", + "border_top_right_radius", + "border_bottom_right_radius", + "border_bottom_left_radius" + ], + "font" => [ + "font_family", + "font_size", + // "font_stretch", + "font_style", + "font_variant", + "font_weight", + "line_height" + ], + "inset" => [ + "top", + "right", + "bottom", + "left" + ], + "list_style" => [ + "list_style_image", + "list_style_position", + "list_style_type" + ], + "margin" => [ + "margin_top", + "margin_right", + "margin_bottom", + "margin_left" + ], + "padding" => [ + "padding_top", + "padding_right", + "padding_bottom", + "padding_left" + ], + "outline" => [ + "outline_width", + "outline_style", + "outline_color" + ] + ]; + + /** + * Maps legacy property names to actual property names. + * + * @var array + */ + protected static $_props_alias = [ + "word_wrap" => "overflow_wrap", + "_dompdf_background_image_resolution" => "background_image_resolution", + "_dompdf_image_resolution" => "image_resolution", + "_webkit_transform" => "transform", + "_webkit_transform_origin" => "transform_origin" + ]; + + /** + * Default style values. + * + * @link https://www.w3.org/TR/CSS21/propidx.html + * + * @var array + */ + protected static $_defaults = null; + + /** + * Lookup table for properties that inherit by default. + * + * @link https://www.w3.org/TR/CSS21/propidx.html + * + * @var array + */ + protected static $_inherited = [ + "azimuth" => true, + "background_image_resolution" => true, + "border_collapse" => true, + "border_spacing" => true, + "caption_side" => true, + "color" => true, + "cursor" => true, + "direction" => true, + "elevation" => true, + "empty_cells" => true, + "font_family" => true, + "font_size" => true, + "font_style" => true, + "font_variant" => true, + "font_weight" => true, + "font" => true, + "image_resolution" => true, + "letter_spacing" => true, + "line_height" => true, + "list_style_image" => true, + "list_style_position" => true, + "list_style_type" => true, + "list_style" => true, + "orphans" => true, + "overflow_wrap" => true, + "pitch_range" => true, + "pitch" => true, + "quotes" => true, + "richness" => true, + "speak_header" => true, + "speak_numeral" => true, + "speak_punctuation" => true, + "speak" => true, + "speech_rate" => true, + "stress" => true, + "text_align" => true, + "text_indent" => true, + "text_transform" => true, + "visibility" => true, + "voice_family" => true, + "volume" => true, + "white_space" => true, + "widows" => true, + "word_break" => true, + "word_spacing" => true + ]; + + /** + * @var array + */ + protected static $_dependency_map = [ + "border_top_style" => [ + "border_top_width" + ], + "border_bottom_style" => [ + "border_bottom_width" + ], + "border_left_style" => [ + "border_left_width" + ], + "border_right_style" => [ + "border_right_width" + ], + "direction" => [ + "text_align" + ], + "font_size" => [ + "background_position", + "background_size", + "border_top_width", + "border_right_width", + "border_bottom_width", + "border_left_width", + "border_top_left_radius", + "border_top_right_radius", + "border_bottom_right_radius", + "border_bottom_left_radius", + "inset", + "letter_spacing", + "line_height", + "margin_top", + "margin_right", + "margin_bottom", + "margin_left", + "outline_width", + "outline_offset", + "padding_top", + "padding_right", + "padding_bottom", + "padding_left", + "word_spacing", + "width", + "height", + "min-width", + "min-height", + "max-width", + "max-height" + ], + "float" => [ + "display" + ], + "position" => [ + "display" + ], + "outline_style" => [ + "outline_width" + ] + ]; + + /** + * Lookup table for dependent properties. Initially computed from the + * dependency map. + * + * @var array + */ + protected static $_dependent_props = []; + + /** + * Caches method_exists result + * + * @var array + */ + protected static $_methods_cache = []; + + /** + * The stylesheet this style belongs to + * + * @var Stylesheet + */ + protected $_stylesheet; + + /** + * Media queries attached to the style + * + * This is a two-dimensional array where the first dimension represents + * the media query grouping (logic-or) and the second dimension the + * media queries within the grouping. + * + * The structure of the actual query element is: + * - media query feature + * - media query value or condition + * - media query operator (e.g., not) + * + * @var array + */ + protected $_media_queries; + + /** + * Properties set by an `!important` declaration. + * + * @var array + */ + protected $_important_props = []; + + /** + * Specified (or declared) values of the CSS properties. + * + * https://www.w3.org/TR/css-cascade-3/#value-stages + * + * @var array + */ + protected $_props = []; + + /** + * Used to track which CSS property were set directly versus + * those set via shorthand property + * + * @var array + */ + protected $_props_specified = []; + + /** + * Computed values of the CSS properties. + * + * @var array + */ + protected $_props_computed = []; + + /** + * Used values of the CSS properties. + * + * @var array + */ + protected $_props_used = []; + + /** + * Marks properties with non-final used values that should be cleared on + * style reset. + * + * @var array + */ + protected $non_final_used = []; + + /** + * Used to track CSS property assignment entry/exit in order to watch + * for circular dependencies. + * + * @var array + */ + protected $_prop_stack = []; + + /** + * Used to track CSS variable resolution entry/exit in order to watch + * for circular dependencies. + * + * @var array + */ + protected $_var_stack = []; + + /** + * Style of the parent element in document tree. + * + * @var Style + */ + protected $parent_style; + + /** + * @var Frame|null + */ + protected $_frame; + + /** + * The origin of the style + * + * @var int + */ + protected $_origin = Stylesheet::ORIG_AUTHOR; + + /** + * The computed bottom spacing + * + * @var float|string|null + */ + private $_computed_bottom_spacing = null; + + /** + * @var bool|null + */ + private $has_border_radius_cache = null; + + /** + * @var array|null + */ + private $resolved_border_radius = null; + + /** + * @var FontMetrics + */ + private $fontMetrics; + + /** + * @param Stylesheet $stylesheet The stylesheet the style is associated with. + * @param int $origin + */ + public function __construct(Stylesheet $stylesheet, int $origin = Stylesheet::ORIG_AUTHOR) + { + $this->fontMetrics = $stylesheet->getFontMetrics(); + + $this->_stylesheet = $stylesheet; + $this->_media_queries = []; + $this->_origin = $origin; + $this->parent_style = null; + + if (!isset(self::$_defaults)) { + + // Shorthand + $d =& self::$_defaults; + + // All CSS 2.1 properties, and their default values + // Some properties are specified with their computed value for + // efficiency; this only works if the computed value is not + // dependent on another property + $d["azimuth"] = "center"; + $d["background_attachment"] = "scroll"; + $d["background_color"] = "transparent"; + $d["background_image"] = "none"; + $d["background_image_resolution"] = "normal"; + $d["background_position"] = [0.0, 0.0]; + $d["background_repeat"] = "repeat"; + $d["background"] = ""; + $d["border_collapse"] = "separate"; + $d["border_color"] = ""; + $d["border_spacing"] = [0.0, 0.0]; + $d["border_style"] = ""; + $d["border_top"] = ""; + $d["border_right"] = ""; + $d["border_bottom"] = ""; + $d["border_left"] = ""; + $d["border_top_color"] = "currentcolor"; + $d["border_right_color"] = "currentcolor"; + $d["border_bottom_color"] = "currentcolor"; + $d["border_left_color"] = "currentcolor"; + $d["border_top_style"] = "none"; + $d["border_right_style"] = "none"; + $d["border_bottom_style"] = "none"; + $d["border_left_style"] = "none"; + $d["border_top_width"] = "medium"; + $d["border_right_width"] = "medium"; + $d["border_bottom_width"] = "medium"; + $d["border_left_width"] = "medium"; + $d["border_width"] = ""; + $d["border_bottom_left_radius"] = 0.0; + $d["border_bottom_right_radius"] = 0.0; + $d["border_top_left_radius"] = 0.0; + $d["border_top_right_radius"] = 0.0; + $d["border_radius"] = ""; + $d["border"] = ""; + $d["bottom"] = "auto"; + $d["caption_side"] = "top"; + $d["clear"] = "none"; + $d["clip"] = "auto"; + $d["color"] = "#000000"; + $d["content"] = "normal"; + $d["counter_increment"] = "none"; + $d["counter_reset"] = "none"; + $d["cue_after"] = "none"; + $d["cue_before"] = "none"; + $d["cue"] = ""; + $d["cursor"] = "auto"; + $d["direction"] = "ltr"; + $d["display"] = "inline"; + $d["elevation"] = "level"; + $d["empty_cells"] = "show"; + $d["float"] = "none"; + $d["font_family"] = $stylesheet->get_dompdf()->getOptions()->getDefaultFont(); + $d["font_size"] = "medium"; + $d["font_style"] = "normal"; + $d["font_variant"] = "normal"; + $d["font_weight"] = 400; + $d["font"] = ""; + $d["height"] = "auto"; + $d["image_resolution"] = "normal"; + $d["inset"] = ""; + $d["left"] = "auto"; + $d["letter_spacing"] = "normal"; + $d["line_height"] = "normal"; + $d["list_style_image"] = "none"; + $d["list_style_position"] = "outside"; + $d["list_style_type"] = "disc"; + $d["list_style"] = ""; + $d["margin_right"] = 0.0; + $d["margin_left"] = 0.0; + $d["margin_top"] = 0.0; + $d["margin_bottom"] = 0.0; + $d["margin"] = ""; + $d["max_height"] = "none"; + $d["max_width"] = "none"; + $d["min_height"] = "auto"; + $d["min_width"] = "auto"; + $d["orphans"] = 2; + $d["outline_color"] = "currentcolor"; // "invert" special color is not supported + $d["outline_style"] = "none"; + $d["outline_width"] = "medium"; + $d["outline_offset"] = 0.0; + $d["outline"] = ""; + $d["overflow"] = "visible"; + $d["overflow_wrap"] = "normal"; + $d["padding_top"] = 0.0; + $d["padding_right"] = 0.0; + $d["padding_bottom"] = 0.0; + $d["padding_left"] = 0.0; + $d["padding"] = ""; + $d["page_break_after"] = "auto"; + $d["page_break_before"] = "auto"; + $d["page_break_inside"] = "auto"; + $d["pause_after"] = "0"; + $d["pause_before"] = "0"; + $d["pause"] = ""; + $d["pitch_range"] = "50"; + $d["pitch"] = "medium"; + $d["play_during"] = "auto"; + $d["position"] = "static"; + $d["quotes"] = "auto"; + $d["richness"] = "50"; + $d["right"] = "auto"; + $d["size"] = "auto"; // @page + $d["speak_header"] = "once"; + $d["speak_numeral"] = "continuous"; + $d["speak_punctuation"] = "none"; + $d["speak"] = "normal"; + $d["speech_rate"] = "medium"; + $d["stress"] = "50"; + $d["table_layout"] = "auto"; + $d["text_align"] = ""; + $d["text_decoration"] = "none"; + $d["text_indent"] = 0.0; + $d["text_transform"] = "none"; + $d["top"] = "auto"; + $d["unicode_bidi"] = "normal"; + $d["vertical_align"] = "baseline"; + $d["visibility"] = "visible"; + $d["voice_family"] = ""; + $d["volume"] = "medium"; + $d["white_space"] = "normal"; + $d["widows"] = 2; + $d["width"] = "auto"; + $d["word_break"] = "normal"; + $d["word_spacing"] = "normal"; + $d["z_index"] = "auto"; + + // CSS3 + $d["opacity"] = 1.0; + $d["background_size"] = ["auto", "auto"]; + $d["transform"] = []; + $d["transform_origin"] = ["50%", "50%", 0.0]; + + // for @font-face + $d["src"] = ""; + $d["unicode_range"] = ""; + + // vendor-prefixed properties + $d["_dompdf_keep"] = ""; + + // Compute dependent props from dependency map + foreach (self::$_dependency_map as $props) { + foreach ($props as $prop) { + self::$_dependent_props[$prop] = true; + } + } + + // Compute valid display-type lookup table + self::$valid_display_types = [ + "none" => true, + "-dompdf-br" => true, + "-dompdf-image" => true, + "-dompdf-list-bullet" => true, + "-dompdf-page" => true + ]; + foreach (self::BLOCK_LEVEL_TYPES as $val) { + self::$valid_display_types[$val] = true; + } + foreach (self::INLINE_LEVEL_TYPES as $val) { + self::$valid_display_types[$val] = true; + } + foreach (self::TABLE_INTERNAL_TYPES as $val) { + self::$valid_display_types[$val] = true; + } + } + } + + /** + * Clear all non-final used values. + */ + public function reset(): void + { + foreach (array_keys($this->non_final_used) as $prop) { + unset($this->_props_used[$prop]); + } + + $this->non_final_used = []; + } + + /** + * @param array $media_queries + */ + public function set_media_queries(array $media_queries): void + { + $this->_media_queries = $media_queries; + } + + /** + * @return array + */ + public function get_media_queries(): array + { + return $this->_media_queries; + } + + /** + * @param Frame $frame + */ + public function set_frame(Frame $frame): void + { + $this->_frame = $frame; + } + + /** + * @return Frame|null + */ + public function get_frame(): ?Frame + { + return $this->_frame; + } + + /** + * @param int $origin + */ + public function set_origin(int $origin): void + { + $this->_origin = $origin; + } + + /** + * @return int + */ + public function get_origin(): int + { + return $this->_origin; + } + + /** + * Returns the {@link Stylesheet} the style is associated with. + * + * @return Stylesheet + */ + public function get_stylesheet(): Stylesheet + { + return $this->_stylesheet; + } + + public function is_custom_property(string $prop): bool + { + return \substr($prop, 0, 2) === "--"; + } + + public function is_absolute(): bool + { + $position = $this->__get("position"); + return $position === "absolute" || $position === "fixed"; + } + + public function is_in_flow(): bool + { + $float = $this->__get("float"); + return $float === "none" && !$this->is_absolute(); + } + + /** + * Converts any CSS length value into an absolute length in points. + * + * length_in_pt() takes a single length (e.g. '1em') or an array of + * lengths and returns an absolute length. If an array is passed, then + * the return value is the sum of all elements. If any of the lengths + * provided are "auto" or "none" then that value is returned. + * + * If a reference size is not provided, the current font size is used. + * + * @param float|string|array $length The numeric length (or string measurement) or array of lengths to resolve. + * @param float|null $ref_size An absolute reference size to resolve percentage lengths. + * + * @return float|string + */ + public function length_in_pt($length, ?float $ref_size = null) + { + $font_size = $this->__get("font_size"); + $ref_size = $ref_size ?? $font_size; + + if (!\is_array($length)) { + $length = [$length]; + } + + $ret = 0.0; + + foreach ($length as $l) { + if ($l === "auto" || $l === "none") { + return $l; + } + + // Assume numeric values are already in points + if (is_numeric($l)) { + $ret += (float) $l; + continue; + } + + $val = $this->single_length_in_pt((string) $l, $ref_size, $font_size); + $ret += $val ?? 0; + } + + return $ret; + } + + /** + * Convert a length declaration to pt. + * + * @param string $l The length declaration. + * @param float $ref_size Reference size for percentage declarations. + * @param float|null $font_size Font size for resolving font-size relative units. + * + * @return float|null The length in pt, or `null` for invalid declarations. + */ + protected function single_length_in_pt(string $l, float $ref_size = 0, ?float $font_size = null): ?float + { + static $cache = []; + + $font_size = $font_size ?? $this->__get("font_size"); + $dpi = $this->_stylesheet->get_dompdf()->getOptions()->getDpi(); + + $key = "$l/$dpi/$ref_size/$font_size"; + + if (\array_key_exists($key, $cache)) { + return $cache[$key]; + } + + $number = self::CSS_NUMBER; + $pattern = "/^($number)([a-zA-Z%]*)?$/"; + + if (!preg_match($pattern, $l, $matches)) { + $ident = self::CSS_IDENTIFIER; + $pattern = "/^($ident)\(.*\)$/i"; + if (preg_match($pattern, $l)) { + $value = $this->evaluate_func($this->parse_func($l), $ref_size, $font_size); + return $cache[$key] = $value; + } + return null; + } + + $v = (float) $matches[1]; + $unit = strtolower($matches[2]); + + if ($unit === "") { + // Legacy support for unitless values, not covered by spec. Might + // want to restrict this to unitless `0` in the future + $value = $v; + } + + elseif ($unit === "%") { + $value = $v / 100 * $ref_size; + } + + elseif ($unit === "px") { + $value = ($v * 72) / $dpi; + } + + elseif ($unit === "pt") { + $value = $v; + } + + elseif ($unit === "rem") { + $tree = $this->_stylesheet->get_dompdf()->getTree(); + $root_style = $tree !== null ? $tree->get_root()->get_style() : null; + $root_font_size = $root_style === null || $root_style === $this + ? $font_size + : $root_style->__get("font_size"); + $value = $v * $root_font_size; + + // Skip caching if the root style is not available yet, as to avoid + // incorrectly cached values if the root font size is different from + // the default + if ($root_style === null) { + return $value; + } + } + + elseif ($unit === "em") { + $value = $v * $font_size; + } + + elseif ($unit === "cm") { + $value = $v * 72 / 2.54; + } + + elseif ($unit === "mm") { + $value = $v * 72 / 25.4; + } + + elseif ($unit === "ex") { + // FIXME: em:ex ratio? + $value = $v * $font_size / 2; + } + + elseif ($unit === "in") { + $value = $v * 72; + } + + elseif ($unit === "pc") { + $value = $v * 12; + } + + else { + // Invalid or unsupported declaration + $value = null; + } + + return $cache[$key] = $value; + } + + /** + * Shunting-yard Algorithm + * @param string $expr infix expression + * @return array + */ + private function parse_func(string $expr): array + { + if (substr_count($expr, '(') !== substr_count($expr, ')')) { + return []; + } + + $expr = str_replace(['(', ')', '*', '/', ','], [' ( ', ' ) ', ' * ', ' / ', ' , '], $expr); + $expr = trim(preg_replace('/\s+/', ' ', $expr)); + + if ($expr === '') { + return []; + } + + $precedence = ['*' => 3, '/' => 3, '+' => 2, '-' => 2, ',' => 1]; + + $opStack = []; + $queue = []; + + $parts = explode(' ', $expr); + + foreach ($parts as $part) { + if ($part === '(') { + $opStack[] = $part; + } elseif (\array_key_exists(strtolower($part), self::CSS_MATH_FUNCTIONS)) { + $opStack[] = strtolower($part); + } elseif ($part === ')') { + while (\count($opStack) > 0 && end($opStack) !== '(' && !\array_key_exists(end($opStack), self::CSS_MATH_FUNCTIONS)) { + $queue[] = array_pop($opStack); + } + if (end($opStack) === '(') { + array_pop($opStack); + } + if (\count($opStack) > 0 && \array_key_exists(end($opStack), self::CSS_MATH_FUNCTIONS)) { + $queue[] = array_pop($opStack); + } + } elseif (\array_key_exists($part, $precedence)) { + while (\count($opStack) > 0 && end($opStack) !== '(' && $precedence[end($opStack)] >= $precedence[$part]) { + $queue[] = array_pop($opStack); + } + $opStack[] = $part; + } else { + $queue[] = $part; + } + } + + while (\count($opStack) > 0) { + $queue[] = array_pop($opStack); + } + + return $queue; + } + + /** + * Reverse Polish Notation + * @param array $rpn + * @param float $ref_size + * @param float|null $font_size + * @return float|null + */ + private function evaluate_func(array $rpn, float $ref_size = 0, ?float $font_size = null): ?float + { + if (\count($rpn) === 0) { + return null; + } + + $ops = ['*', '/', '+', '-', ',']; + + $stack = []; + + foreach ($rpn as $part) { + if (\array_key_exists($part, self::CSS_MATH_FUNCTIONS)) { + $argv = array_pop($stack); + if (!is_array($argv)) { + $argv = [$argv]; + } + $argc = \count($argv); + switch ($part) { + case 'abs': + case 'acos': + case 'asin': + case 'atan': + case 'cos': + case 'exp': + case 'sin': + case 'sqrt': + case 'tan': + if ($argc !== 1) { + return null; + } + $stack[] = call_user_func_array($part, $argv); + break; + case 'atan2': + case 'hypot': + case 'pow': + if ($argc !== 2) { + return null; + } + $stack[] = call_user_func_array($part, $argv); + break; + case 'log': + if ($argc === 1) { + $stack[] = log($argv[0]); + } elseif ($argc === 2) { + $stack[] = log($argv[0], $argv[1]); + } else { + return null; + } + break; + case 'max': + $stack[] = max($argv); + break; + case 'min': + $stack[] = min($argv); + break; + case 'mod': + if ($argc !== 2 || $argv[1] === 0.0) { + return null; + } + if ($argv[1] > 0) { + $stack[] = $argv[0] - floor($argv[0] / $argv[1]) * $argv[1]; + } else { + $stack[] = $argv[0] - ceil($argv[0] * -1 / $argv[1]) * $argv[1] * -1 ; + } + break; + case 'rem': + if ($argc !== 2 || $argv[1] === 0.0) { + return null; + } + $stack[] = $argv[0] - (intval($argv[0] / $argv[1]) * $argv[1]); + break; + case 'round': + if ($argc !== 2 || $argv[1] === 0.0) { + return null; + } + if ($argv[0] >= 0) { + $stack[] = round($argv[0] / $argv[1], 0, PHP_ROUND_HALF_UP) * $argv[1]; + } else { + $stack[] = round($argv[0] / $argv[1], 0, PHP_ROUND_HALF_DOWN) * $argv[1]; + } + break; + case 'calc': + if ($argc !== 1) { + return null; + } + $stack[] = $argv[0]; + break; + case 'clamp': + if ($argc !== 3) { + return null; + } + $stack[] = max($argv[0], min($argv[1], $argv[2])); + break; + case 'sign': + if ($argc !== 1) { + return null; + } + $stack[] = $argv[0] == 0 ? 0.0 : ($argv[0] / abs($argv[0])); + break; + default: + return null; + } + } elseif (\in_array($part, $ops, true)) { + $rightValue = array_pop($stack); + $leftValue = array_pop($stack); + if ($rightValue === null || $leftValue === null) { + return null; + } + switch ($part) { + case '*': + $stack[] = $leftValue * $rightValue; + break; + case '/': + if ($rightValue === 0.0) { + return null; + } + $stack[] = $leftValue / $rightValue; + break; + case '+': + $stack[] = $leftValue + $rightValue; + break; + case '-': + $stack[] = $leftValue - $rightValue; + break; + case ',': + if (is_array($leftValue)) { + $leftValue[] = $rightValue; + $stack[] = $leftValue; + } else { + $stack[] = [$leftValue, $rightValue]; + } + break; + } + } else { + $val = $this->single_length_in_pt($part, $ref_size, $font_size); + if ($val === null) { + return null; + } + $stack[] = $val; + } + } + + if (\count($stack) > 1) { + return null; + } + + return floatval(end($stack)); + } + + /** + * Resolves the actual values for used CSS custom properties. + * + * This function receives the whole content of the var() function, which + * can also include a fallback value. + */ + private function parse_var($matches) { + $variable = is_array($matches) ? $matches[1] : $matches; + + if (\in_array($variable, $this->_var_stack, true)) { + return null; + } + array_push($this->_var_stack, $variable); + + // Split property name and an optional fallback value. + [$custom_prop, $fallback] = explode(',', $variable, 2) + ['', '']; + $fallback = trim($fallback); + + // Try to retrieve the custom property value, or use the fallback value + // if the value could not be resolved. + $value = $this->computed($custom_prop) ?? $fallback; + + // If the resolved value also has vars in it, resolve again. + $pattern = self::CSS_VAR; + $value = preg_replace_callback( + "/$pattern/", + [$this, "parse_var"], + $value); + + array_pop($this->_var_stack); + return $value ?: null; + } + + /** + * Resolve inherited property values using the provided parent style or the + * default values, in case no parent style exists. + * + * https://www.w3.org/TR/css-cascade-3/#inheriting + * + * @param Style|null $parent + */ + public function inherit(?Style $parent = null): void + { + $this->parent_style = $parent; + + // Clear the computed font size, as it might depend on the parent + // font size + unset($this->_props_computed["font_size"]); + unset($this->_props_used["font_size"]); + + if ($parent) { + // For properties that inherit by default: When the cascade did + // not result in a value, inherit the parent value. Inheritance + // is handled via the specific sub-properties for shorthands. Custom + // properties (variables) are selected by the -- prefix. + foreach ($parent->_props as $prop => $val) { + if ( + !isset($this->_props[$prop]) + && ( + isset(self::$_inherited[$prop]) + || $this->is_custom_property($prop) + ) + ) { + $parent_val = $parent->computed($prop); + + if ($this->is_custom_property($prop)) { + $this->set_prop($prop, $parent_val); + } else { + $this->_props[$prop] = $parent_val; + $this->_props_computed[$prop] = $parent_val; + $this->_props_used[$prop] = null; + } + } + } + } + + foreach ($this->_props as $prop => $val) { + if ($val === "inherit") { + if ($parent && isset($parent->_props[$prop])) { + $parent_val = $parent->computed($prop); + + if ($this->is_custom_property($prop)) { + $this->set_prop($prop, $parent_val); + } else { + $this->_props[$prop] = $parent_val; + $this->_props_computed[$prop] = $parent_val; + $this->_props_used[$prop] = null; + } + } else { + if ($this->is_custom_property($prop)) { + $this->set_prop($prop, "unset"); + } else { + // Parent prop not set, use default + $this->_props[$prop] = self::$_defaults[$prop]; + unset($this->_props_computed[$prop]); + unset($this->_props_used[$prop]); + } + } + } + } + } + + /** + * Override properties in this style with those in $style + * + * @param Style $style + */ + public function merge(Style $style): void + { + foreach ($style->_props as $prop => $val) { + $important = isset($style->_important_props[$prop]); + + // `!important` declarations take precedence over normal ones + if (!$important && isset($this->_important_props[$prop])) { + continue; + } + + if ($important) { + $this->_important_props[$prop] = true; + } + + if ($this->is_custom_property($prop)) { + $this->set_prop($prop, $val, $important); + } else { + $this->_props[$prop] = $val; + } + + // Copy an existing computed value only for non-dependent + // properties; otherwise it may be invalid for the current style + if (!isset(self::$_dependent_props[$prop]) + && \array_key_exists($prop, $style->_props_computed) + ) { + $this->_props_computed[$prop] = $style->_props_computed[$prop]; + $this->_props_used[$prop] = null; + } else { + unset($this->_props_computed[$prop]); + unset($this->_props_used[$prop]); + } + + if (\array_key_exists($prop, $style->_props_specified)) { + $this->_props_specified[$prop] = true; + } + } + + // re-evalutate CSS variables + foreach (array_keys($this->_props) as $prop) { + if (!$this->is_custom_property($prop)) { + continue; + } + $this->set_prop($prop, $this->_props[$prop], isset($this->_important_props[$prop])); + } + } + + /** + * Clear information about important declarations after the style has been + * finalized during stylesheet loading. + */ + public function clear_important(): void + { + $this->_important_props = []; + } + + /** + * Clear border-radius and bottom-spacing cache as necessary when a given + * property is set. + * + * @param string $prop The property that is set. + */ + protected function clear_cache(string $prop): void + { + // Clear border-radius cache on setting any border-radius + // property + if ($prop === "border_top_left_radius" + || $prop === "border_top_right_radius" + || $prop === "border_bottom_left_radius" + || $prop === "border_bottom_right_radius" + ) { + $this->has_border_radius_cache = null; + $this->resolved_border_radius = null; + } + + // Clear bottom-spacing cache if necessary. Border style can + // disable/enable border calculations + if ($prop === "margin_bottom" + || $prop === "padding_bottom" + || $prop === "border_bottom_width" + || $prop === "border_bottom_style" + ) { + $this->_computed_bottom_spacing = null; + } + } + + /** + * Set a style property from a value declaration. + * + * Setting `$clear_dependencies` to `false` is useful for saving a bit of + * unnecessary work while loading stylesheets. + * + * @param string $prop The property to set. + * @param mixed $val The value declaration or computed value. + * @param bool $important Whether the declaration is important. + * @param bool $clear_dependencies Whether to clear computed values of dependent properties. + */ + public function set_prop(string $prop, $val, bool $important = false, bool $clear_dependencies = true): void + { + // Skip some checks for CSS custom properties. + if (!$this->is_custom_property($prop)) { + + $prop = str_replace("-", "_", $prop); + + // Legacy property aliases + if (isset(self::$_props_alias[$prop])) { + $prop = self::$_props_alias[$prop]; + } + + if (!isset(self::$_defaults[$prop])) { + global $_dompdf_warnings; + $_dompdf_warnings[] = "'$prop' is not a recognized CSS property."; + return; + } + } + $this->_props_specified[$prop] = true; + + // Trim declarations unconditionally, but only lower-case for comparison + // with the general keywords. Properties must handle case-insensitive + // comparisons individually + if (\is_string($val)) { + $val = trim($val); + $lower = strtolower($val); + + if ($lower === "initial" || $lower === "inherit" || $lower === "unset") { + $val = $lower; + } + } + + if (isset(self::$_props_shorthand[$prop])) { + // Shorthand properties directly set their respective sub-properties + // https://www.w3.org/TR/css-cascade-3/#shorthand + if ($val === "initial" || $val === "inherit" || $val === "unset") { + foreach (self::$_props_shorthand[$prop] as $sub_prop) { + $this->set_prop($sub_prop, $val, $important, $clear_dependencies); + } + } else { + $method = "_set_$prop"; + + // Resolve the CSS custom property value(s). + $pattern = self::CSS_VAR; + + // Always set the specified value for properties that use CSS variables + // so that an invalid initial value does not prevent re-computation later. + $this->_props[$prop] = $val; + + //TODO: we shouldn't need to parse this twice + preg_match_all("/$pattern/", $val, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + if ($this->parse_var($match) === null) { + // unset specified as for specific prop under expectation it will be overridden + foreach (self::$_props_shorthand[$prop] as $sub_prop) { + unset($this->_props_specified[$sub_prop]); + } + return; + } + } + $val = preg_replace_callback( + "/$pattern/", + [$this, "parse_var"], + $val); + + if (!isset(self::$_methods_cache[$method])) { + self::$_methods_cache[$method] = method_exists($this, $method); + } + + if (self::$_methods_cache[$method]) { + $values = $this->$method($val); + + if ($values === []) { + return; + } + + // Each missing sub-property is assigned its initial value + // https://www.w3.org/TR/css-cascade-3/#shorthand + foreach (self::$_props_shorthand[$prop] as $sub_prop) { + $sub_val = $values[$sub_prop] ?? self::$_defaults[$sub_prop]; + $this->set_prop($sub_prop, $sub_val, $important, $clear_dependencies); + unset($this->_props_specified[$sub_prop]); + } + } + } + } else { + // Legacy support for `word-break: break-word` + // https://www.w3.org/TR/css-text-3/#valdef-word-break-break-word + if ($prop === "word_break" + && \is_string($val) && strcasecmp($val, "break-word") === 0 + ) { + $val = "normal"; + $this->set_prop("overflow_wrap", "anywhere", $important, $clear_dependencies); + } + + // `!important` declarations take precedence over normal ones + if (!$important && isset($this->_important_props[$prop])) { + return; + } + + if ($important) { + $this->_important_props[$prop] = true; + } + + // https://www.w3.org/TR/css-cascade-3/#inherit-initial + if ($val === "unset") { + $val = isset(self::$_inherited[$prop]) || $this->is_custom_property($prop) ? "inherit" : "initial"; + } + + // https://www.w3.org/TR/css-cascade-3/#valdef-all-initial + if ($val === "initial" && !$this->is_custom_property($prop)) { + $val = self::$_defaults[$prop]; + } + + // Always set the specified value for properties that use CSS variables + // so that an invalid initial value does not prevent re-computation later. + if (\is_string($val) && \preg_match("/" . self::CSS_VAR . "/", $val)) { + $this->_props[$prop] = $val; + } + + $computed = $this->compute_prop($prop, $val); + + // Skip invalid declarations + if ($computed === null) { + return; + } + + $this->_props[$prop] = $val; + $this->_props_computed[$prop] = $computed; + $this->_props_used[$prop] = null; + + //TODO: this should be a directed dependency map + if ($this->is_custom_property($prop) && !\in_array($prop, $this->_prop_stack, true)) { + array_push($this->_prop_stack, $prop); + $specified_props = array_filter($this->_props, function($key) { + return \array_key_exists($key, $this->_props_specified); + }, ARRAY_FILTER_USE_KEY); // copy existing props filtered by those set explicitly before parsing vars + foreach ($specified_props as $specified_prop => $specified_value) { + if (!$this->is_custom_property($specified_prop) || strpos($specified_value, "var($prop") !== false) { + $this->set_prop($specified_prop, $specified_value, isset($this->_important_props[$specified_prop]), true); + if (isset(self::$_props_shorthand[$specified_prop])) { + foreach (self::$_props_shorthand[$specified_prop] as $sub_prop) { + if (\array_key_exists($sub_prop, $specified_props)) { + $this->set_prop($sub_prop, $specified_props[$sub_prop], isset($this->_important_props[$sub_prop]), true); + } + } + } + } + } + array_pop($this->_prop_stack); + } + + if ($clear_dependencies) { + // Clear the computed values of any dependent properties, so + // they can be re-computed + if (isset(self::$_dependency_map[$prop])) { + foreach (self::$_dependency_map[$prop] as $dependent) { + unset($this->_props_computed[$dependent]); + unset($this->_props_used[$dependent]); + } + } + + $this->clear_cache($prop); + } + } + } + + /** + * Get the specified value of a style property. + * + * @param string $prop + * + * @return mixed + * @throws Exception + */ + public function get_specified(string $prop) + { + // Legacy property aliases + if (isset(self::$_props_alias[$prop])) { + $prop = self::$_props_alias[$prop]; + } + + if (!isset(self::$_defaults[$prop]) && !$this->is_custom_property($prop)) { + throw new Exception("'$prop' is not a recognized CSS property."); + } + + return $this->_props[$prop] ?? self::$_defaults[$prop]; + } + + /** + * Set a style property to its final value. + * + * This sets the specified and used value of the style property to the given + * value, meaning the value is not parsed and thus should have a type + * compatible with the property. + * + * If a shorthand property is specified, all of its sub-properties are set + * to the given value. + * + * @param string $prop The property to set. + * @param mixed $val The final value of the property. + * + * @throws Exception + */ + public function __set(string $prop, $val) + { + // Legacy property aliases + if (isset(self::$_props_alias[$prop])) { + $prop = self::$_props_alias[$prop]; + } + + if (!isset(self::$_defaults[$prop]) && !$this->is_custom_property($prop)) { + throw new Exception("'$prop' is not a recognized CSS property."); + } + + if (isset(self::$_props_shorthand[$prop])) { + foreach (self::$_props_shorthand[$prop] as $sub_prop) { + $this->__set($sub_prop, $val); + } + } else { + $this->_props[$prop] = $val; + $this->_props_computed[$prop] = $val; + $this->_props_used[$prop] = $val; + + $this->clear_cache($prop); + } + } + + /** + * Set the used value of a style property. + * + * Used values are cleared on style reset. + * + * If a shorthand property is specified, all of its sub-properties are set + * to the given value. + * + * @param string $prop The property to set. + * @param mixed $val The used value of the property. + * + * @throws Exception + */ + public function set_used(string $prop, $val): void + { + // Legacy property aliases + if (isset(self::$_props_alias[$prop])) { + $prop = self::$_props_alias[$prop]; + } + + if (!isset(self::$_defaults[$prop])) { + throw new Exception("'$prop' is not a recognized CSS property."); + } + + if (isset(self::$_props_shorthand[$prop])) { + foreach (self::$_props_shorthand[$prop] as $sub_prop) { + $this->set_used($sub_prop, $val); + } + } else { + $this->_props_used[$prop] = $val; + $this->non_final_used[$prop] = true; + } + } + + /** + * Get the used or computed value of a style property, depending on whether + * the used value has been determined yet. + * + * @param string $prop + * + * @return mixed + * @throws Exception + */ + public function __get(string $prop) + { + // Legacy property aliases + if (isset(self::$_props_alias[$prop])) { + $prop = self::$_props_alias[$prop]; + } + + if (!isset(self::$_defaults[$prop]) && !$this->is_custom_property($prop)) { + throw new Exception("'$prop' is not a recognized CSS property."); + } + + if (isset($this->_props_used[$prop])) { + return $this->_props_used[$prop]; + } + + $method = "_get_$prop"; + + if (!isset(self::$_methods_cache[$method])) { + self::$_methods_cache[$method] = method_exists($this, $method); + } + + if (isset(self::$_props_shorthand[$prop])) { + // Don't cache shorthand values, always use getter. If no dedicated + // getter exists, use a simple fallback getter concatenating all + // sub-property values + if (self::$_methods_cache[$method]) { + return $this->$method(); + } else { + return implode(" ", array_map(function ($sub_prop) { + $val = $this->__get($sub_prop); + return \is_array($val) ? implode(" ", $val) : $val; + }, self::$_props_shorthand[$prop])); + } + } else { + $computed = $this->computed($prop); + $used = self::$_methods_cache[$method] + ? $this->$method($computed) + : $computed; + + $this->_props_used[$prop] = $used; + return $used; + } + } + + /** + * @param string $prop The property to compute. + * @param mixed $val The value to compute. Non-string values are treated as already computed. + * + * @return mixed The computed value. + */ + protected function compute_prop(string $prop, $val) + { + // During style merge, the parent style is not available yet, so + // temporarily use the initial value for `inherit` properties. The + // keyword is properly resolved during inheritance + if ($val === "inherit" && !$this->is_custom_property($prop)) { + $val = self::$_defaults[$prop]; + } + + // Check for values which are already computed + if (!\is_string($val)) { + return $val; + } + + // Resolve the CSS custom property value(s). + $pattern = self::CSS_VAR; + $val = preg_replace_callback( + "/$pattern/", + [$this, "parse_var"], + $val); + + $method = "_compute_$prop"; + + if (!isset(self::$_methods_cache[$method])) { + self::$_methods_cache[$method] = method_exists($this, $method); + } + + if (self::$_methods_cache[$method]) { + return $this->$method($val); + } elseif ($val !== "") { + return strtolower($val); + } else { + return null; + } + } + + /** + * Get the computed value for the given property. + * + * @param string $prop The property to get the computed value of. + * + * @return mixed The computed value. + */ + protected function computed(string $prop) + { + if (!\array_key_exists($prop, $this->_props_computed)) { + if (!\array_key_exists($prop, $this->_props) && $this->is_custom_property($prop)) { + return null; + } + $val = $this->_props[$prop] ?? self::$_defaults[$prop]; + $computed = $this->compute_prop($prop, $val); + + if ($computed === null) { + if ($this->is_custom_property($prop)) { + return null; + } + $computed = $this->compute_prop($prop, self::$_defaults[$prop]); + } + + $this->_props_computed[$prop] = $computed; + } + + return $this->_props_computed[$prop]; + } + + /** + * @param float $cbw The width of the containing block. + * @return float|string|null + */ + public function computed_bottom_spacing(float $cbw) + { + // Caching the bottom spacing independently of the given width is a bit + // iffy, but should be okay, as the containing block should only + // potentially change after a page break, and the style is reset in that + // case + if ($this->_computed_bottom_spacing !== null) { + return $this->_computed_bottom_spacing; + } + return $this->_computed_bottom_spacing = $this->length_in_pt( + [ + $this->margin_bottom, + $this->padding_bottom, + $this->border_bottom_width + ], + $cbw + ); + } + + /** + * Returns an `array(r, g, b, "r" => r, "g" => g, "b" => b, "alpha" => alpha, "hex" => "#rrggbb")` + * based on the provided CSS color value. + * + * @param string|null $color + * @return array|string|null + */ + public function munge_color($color) + { + return Color::parse($color); + } + + /** + * @return string + */ + public function get_font_family_raw(): string + { + return trim($this->_props["font_family"], " \t\n\r\x0B\"'"); + } + + /** + * @return string[] + */ + public function get_font_family_computed(): array + { + return $this->computed("font_family"); + } + + /** + * Getter for the `font-family` CSS property. + * + * Uses the {@link FontMetrics} class to resolve the font family into an + * actual font file. + * + * @param string[] $computed + * @return string + * + * @throws Exception + * + * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-family + */ + protected function _get_font_family($computed): string + { + // TODO: It probably makes sense to perform the font selection outside + // the Style class completely. It is now done primarily in + // `FrameDecorator\Text::apply_font_mapping` + + // Select the appropriate font. First determine the subtype, then check + // the specified font-families for a candidate. + + $fontMetrics = $this->getFontMetrics(); + $weight = $this->__get("font_weight"); + $fontStyle = $this->__get("font_style"); + $subtype = $fontMetrics->getType($weight . ' ' . $fontStyle); + + foreach ($computed as $family) { + $font = $fontMetrics->getFont($family, $subtype); + + if ($font !== null) { + return $font; + } + } + + $font = $fontMetrics->getFont(null, $subtype); + + if ($font !== null) { + return $font; + } + + $specified = implode(", ", $computed); + throw new Exception("Unable to find a suitable font replacement for: '$specified'"); + } + + /** + * @param float $computed + * @return float + * + * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-size + */ + protected function _get_font_size($computed) + { + // Computed value may be negative when specified via `calc()` + return max($computed, 0.0); + } + + /** + * @param float|string $computed + * @return float + * + * @link https://www.w3.org/TR/css-text-4/#word-spacing-property + */ + protected function _get_word_spacing($computed) + { + if (\is_float($computed)) { + return $computed; + } + + // Resolve percentage values + $font_size = $this->__get("font_size"); + return $this->single_length_in_pt($computed, $font_size); + } + + /** + * @param float|string $computed + * @return float + * + * @link https://www.w3.org/TR/css-text-4/#letter-spacing-property + */ + protected function _get_letter_spacing($computed) + { + if (\is_float($computed)) { + return $computed; + } + + // Resolve percentage values + $font_size = $this->__get("font_size"); + return $this->single_length_in_pt($computed, $font_size); + } + + /** + * @param float|string $computed + * @return float + * + * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-line-height + */ + protected function _get_line_height($computed) + { + // Lengths have been computed to float, number values to string + if (\is_float($computed)) { + // Computed value may be negative when specified via `calc()` + return max($computed, 0.0); + } + + $font_size = $this->__get("font_size"); + $factor = $computed === "normal" + ? self::$default_line_height + : (float) $computed; + + return $factor * $font_size; + } + + /** + * @param string $computed + * @param bool $current_is_parent + * + * @return array|string + */ + protected function get_color_value($computed, bool $current_is_parent = false) + { + if ($computed === "currentcolor") { + // https://www.w3.org/TR/css-color-4/#resolving-other-colors + if ($current_is_parent) { + // Use the `color` value from the parent for the `color` + // property itself + return isset($this->parent_style) + ? $this->parent_style->__get("color") + : $this->munge_color(self::$_defaults["color"]); + } + + return $this->__get("color"); + } + + return $this->munge_color($computed) ?? "transparent"; + } + + /** + * Returns the color as an array + * + * The array has the following format: + * `array(r, g, b, "r" => r, "g" => g, "b" => b, "alpha" => alpha, "hex" => "#rrggbb")` + * + * @param string $computed + * @return array|string + * + * @link https://www.w3.org/TR/CSS21/colors.html#propdef-color + */ + protected function _get_color($computed) + { + return $this->get_color_value($computed, true); + } + + /** + * Returns the background color as an array + * + * See {@link Style::_get_color()} for format of the color array. + * + * @param string $computed + * @return array|string + * + * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-color + */ + protected function _get_background_color($computed) + { + return $this->get_color_value($computed); + } + + /** + * Returns the background image URI, or "none" + * + * @param string $computed + * @return string + * + * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-image + */ + protected function _get_background_image($computed): string + { + return $this->_stylesheet->resolve_url($computed, true); + } + + /** + * Returns the border color as an array + * + * See {@link Style::_get_color()} for format of the color array. + * + * @param string $computed + * @return array|string + * + * @link https://www.w3.org/TR/CSS21/box.html#border-color-properties + */ + protected function _get_border_top_color($computed) + { + return $this->get_color_value($computed); + } + + /** + * @param string $computed + * @return array|string + */ + protected function _get_border_right_color($computed) + { + return $this->get_color_value($computed); + } + + /** + * @param string $computed + * @return array|string + */ + protected function _get_border_bottom_color($computed) + { + return $this->get_color_value($computed); + } + + /** + * @param string $computed + * @return array|string + */ + protected function _get_border_left_color($computed) + { + return $this->get_color_value($computed); + } + + /** + * Return an array of all border properties. + * + * The returned array has the following structure: + * + * ``` + * array("top" => array("width" => [border-width], + * "style" => [border-style], + * "color" => [border-color (array)]), + * "bottom" ... ) + * ``` + * + * @return array + */ + public function get_border_properties(): array + { + return [ + "top" => [ + "width" => $this->__get("border_top_width"), + "style" => $this->__get("border_top_style"), + "color" => $this->__get("border_top_color"), + ], + "bottom" => [ + "width" => $this->__get("border_bottom_width"), + "style" => $this->__get("border_bottom_style"), + "color" => $this->__get("border_bottom_color"), + ], + "right" => [ + "width" => $this->__get("border_right_width"), + "style" => $this->__get("border_right_style"), + "color" => $this->__get("border_right_color"), + ], + "left" => [ + "width" => $this->__get("border_left_width"), + "style" => $this->__get("border_left_style"), + "color" => $this->__get("border_left_color"), + ], + ]; + } + + /** + * Return a single border-side property + * + * @param string $side + * @return string + */ + protected function get_border_side(string $side): string + { + $color = $this->__get("border_{$side}_color"); + + return $this->__get("border_{$side}_width") . " " . + $this->__get("border_{$side}_style") . " " . + (\is_array($color) ? $color["hex"] : $color); + } + + /** + * Return full border properties as a string + * + * Border properties are returned just as specified in CSS: + * `[width] [style] [color]` + * e.g. "1px solid blue" + * + * @return string + * + * @link https://www.w3.org/TR/CSS21/box.html#border-shorthand-properties + */ + protected function _get_border_top(): string + { + return $this->get_border_side("top"); + } + + /** + * @return string + */ + protected function _get_border_right(): string + { + return $this->get_border_side("right"); + } + + /** + * @return string + */ + protected function _get_border_bottom(): string + { + return $this->get_border_side("bottom"); + } + + /** + * @return string + */ + protected function _get_border_left(): string + { + return $this->get_border_side("left"); + } + + public function has_border_radius(): bool + { + if (isset($this->has_border_radius_cache)) { + return $this->has_border_radius_cache; + } + + // Use a fixed ref size here. We don't know the border-box width here + // and font size might be 0. Since we are only interested in whether + // there is any border radius at all, this should do + $tl = (float) $this->length_in_pt($this->border_top_left_radius, 12); + $tr = (float) $this->length_in_pt($this->border_top_right_radius, 12); + $br = (float) $this->length_in_pt($this->border_bottom_right_radius, 12); + $bl = (float) $this->length_in_pt($this->border_bottom_left_radius, 12); + + $this->has_border_radius_cache = $tl + $tr + $br + $bl > 0; + return $this->has_border_radius_cache; + } + + /** + * Get the final border-radius values to use. + * + * Percentage values are resolved relative to the width of the border box. + * The border radius is additionally scaled for the given render box, and + * constrained by its width and height. + * + * @param float[] $border_box The border box of the frame. + * @param float[]|null $render_box The box to resolve the border radius for. + * + * @return float[] A 4-tuple of top-left, top-right, bottom-right, and bottom-left radius. + */ + public function resolve_border_radius( + array $border_box, + ?array $render_box = null + ): array { + $render_box = $render_box ?? $border_box; + $use_cache = $render_box === $border_box; + + if ($use_cache && isset($this->resolved_border_radius)) { + return $this->resolved_border_radius; + } + + [$x, $y, $w, $h] = $border_box; + + // Resolve percentages relative to width, as long as we have no support + // for per-axis radii + $tl = (float) $this->length_in_pt($this->border_top_left_radius, $w); + $tr = (float) $this->length_in_pt($this->border_top_right_radius, $w); + $br = (float) $this->length_in_pt($this->border_bottom_right_radius, $w); + $bl = (float) $this->length_in_pt($this->border_bottom_left_radius, $w); + + if ($tl + $tr + $br + $bl > 0) { + [$rx, $ry, $rw, $rh] = $render_box; + + $t_offset = $y - $ry; + $r_offset = $rx + $rw - $x - $w; + $b_offset = $ry + $rh - $y - $h; + $l_offset = $x - $rx; + + if ($tl > 0) { + $tl = max($tl + ($t_offset + $l_offset) / 2, 0); + } + if ($tr > 0) { + $tr = max($tr + ($t_offset + $r_offset) / 2, 0); + } + if ($br > 0) { + $br = max($br + ($b_offset + $r_offset) / 2, 0); + } + if ($bl > 0) { + $bl = max($bl + ($b_offset + $l_offset) / 2, 0); + } + + if ($tl + $bl > $rh) { + $f = $rh / ($tl + $bl); + $tl = $f * $tl; + $bl = $f * $bl; + } + if ($tr + $br > $rh) { + $f = $rh / ($tr + $br); + $tr = $f * $tr; + $br = $f * $br; + } + if ($tl + $tr > $rw) { + $f = $rw / ($tl + $tr); + $tl = $f * $tl; + $tr = $f * $tr; + } + if ($bl + $br > $rw) { + $f = $rw / ($bl + $br); + $bl = $f * $bl; + $br = $f * $br; + } + } + + $values = [$tl, $tr, $br, $bl]; + + if ($use_cache) { + $this->resolved_border_radius = $values; + } + + return $values; + } + + /** + * Returns the outline color as an array + * + * See {@link Style::_get_color()} for format of the color array. + * + * @param string $computed + * @return array|string + * + * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-color + */ + protected function _get_outline_color($computed) + { + return $this->get_color_value($computed); + } + + /** + * @param string $computed + * @return string + * + * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-style + */ + protected function _get_outline_style($computed): string + { + return $computed === "auto" ? "solid" : $computed; + } + + /** + * Return full outline properties as a string + * + * Outline properties are returned just as specified in CSS: + * `[width] [style] [color]` + * e.g. "1px solid blue" + * + * @return string + * + * @link https://www.w3.org/TR/CSS21/box.html#border-shorthand-properties + */ + protected function _get_outline(): string + { + $color = $this->__get("outline_color"); + + return $this->__get("outline_width") . " " . + $this->__get("outline_style") . " " . + (\is_array($color) ? $color["hex"] : $color); + } + + /** + * Returns the list style image URI, or "none" + * + * @param string $computed + * @return string + * + * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-image + */ + protected function _get_list_style_image($computed): string + { + return $this->_stylesheet->resolve_url($computed, true); + } + + /** + * @param array|string $computed + * @return array|string + * + * @link https://www.w3.org/TR/css-content-3/#quotes + */ + protected function _get_quotes($computed) + { + if ($computed === "auto") { + // TODO: Use typographically appropriate quotes for the current + // language here + return [['"', '"'], ["'", "'"]]; + } + + return $computed; + } + + /*==============================*/ + + /** + * Parse a property value into its components. + * + * @param string $value + * + * @return string[] + */ + protected function parse_property_value(string $value): array + { + $string = self::CSS_STRING; + $ident = self::CSS_IDENTIFIER; + $number = self::CSS_NUMBER; + + $pattern = "/\n" . + "\s* (?$string) |\n" . // String + "\s* (url \( (?> (\\\\[\"'()] | [^\"'()])* ) (? \g | [^\"'()]+ ) | (?-2))* \)) ) |\n" . // Function (with balanced parentheses) + "\s* ($ident) |\n" . // Keyword + "\s* (\#[0-9a-fA-F]*) |\n" . // Hex value + "\s* ($number [a-zA-Z%]*) |\n" . // Number (+ unit/percentage) + "\s* ([\/,;]) \n" . // Delimiter + "/iSx"; + + if (!preg_match_all($pattern, $value, $matches)) { + return []; + } + + return array_map("trim", $matches[0]); + } + + protected function is_color_value(string $val): bool + { + return $val === "currentcolor" + || $val === "transparent" + || isset(Color::$cssColorNames[$val]) + || preg_match("/^#|rgb\(|rgba\(|cmyk\(/", $val); + } + + /** + * @param string $val + * @return string|null + */ + protected function compute_color_value(string $val): ?string + { + // https://www.w3.org/TR/css-color-4/#resolving-other-colors + $val = strtolower($val); + $munged_color = $val !== "currentcolor" + ? $this->munge_color($val) + : $val; + + if ($munged_color === null) { + return null; + } + + return \is_array($munged_color) ? $munged_color["hex"] : $munged_color; + } + + /** + * @param string $val + * @return int|null + */ + protected function compute_integer(string $val): ?int + { + $integer = self::CSS_INTEGER; + return preg_match("/^$integer$/", $val) + ? (int) $val + : null; + } + + /** + * @param string $val + * @return float|null + */ + protected function compute_number(string $val): ?float + { + $number = self::CSS_NUMBER; + return preg_match("/^$number$/", $val) + ? (float) $val + : null; + } + + /** + * @param string $val + * @return float|null + */ + protected function compute_length(string $val): ?float + { + return strpos($val, "%") === false + ? $this->single_length_in_pt($val) + : null; + } + + /** + * @param string $val + * @return float|null + */ + protected function compute_length_positive(string $val): ?float + { + $computed = $this->compute_length($val); + + // Negative non-`calc` values are invalid + if ($computed === null + || ($computed < 0 && !preg_match("/^-?[_a-zA-Z]/", $val)) + ) { + return null; + } + + return $computed; + } + + /** + * @param string $val + * @return float|string|null + */ + protected function compute_length_percentage(string $val) + { + // Compute with a fixed ref size to decide whether percentage values + // are valid + $computed = $this->single_length_in_pt($val, 12); + + if ($computed === null) { + return null; + } + + // Retain valid percentage declarations + return strpos($val, "%") === false ? $computed : $val; + } + + /** + * @param string $val + * @return float|string|null + */ + protected function compute_length_percentage_positive(string $val) + { + // Compute with a fixed ref size to decide whether percentage values + // are valid + $computed = $this->single_length_in_pt($val, 12); + + // Negative non-`calc` values are invalid + if ($computed === null + || ($computed < 0 && !preg_match("/^-?[_a-zA-Z]/", $val)) + ) { + return null; + } + + // Retain valid percentage declarations + return strpos($val, "%") === false ? $computed : $val; + } + + /** + * @param string $val + * @param string $style_prop The corresponding border-/outline-style property. + * + * @return float|null + * + * @link https://www.w3.org/TR/css-backgrounds-3/#typedef-line-width + */ + protected function compute_line_width(string $val, string $style_prop): ?float + { + $val = strtolower($val); + + // Border-width keywords + if ($val === "thin") { + $computed = 0.5; + } elseif ($val === "medium") { + $computed = 1.5; + } elseif ($val === "thick") { + $computed = 2.5; + } else { + $computed = $this->compute_length_positive($val); + } + + if ($computed === null) { + return null; + } + + // Computed width is 0 if the line style is `none` or `hidden` + // https://www.w3.org/TR/css-backgrounds-3/#border-width + // https://www.w3.org/TR/css-ui-4/#outline-width + $lineStyle = $this->__get($style_prop); + $hasLineStyle = $lineStyle !== "none" && $lineStyle !== "hidden"; + + return $hasLineStyle ? $computed : 0.0; + } + + /** + * @param string $val + * @return string|null + */ + protected function compute_border_style(string $val): ?string + { + $val = strtolower($val); + return \in_array($val, self::BORDER_STYLES, true) ? $val : null; + } + + /** + * @param string $val + * @return float|null + * + * @link https://www.w3.org/TR/css3-values/#angles + */ + protected function compute_angle_or_zero(string $val): ?float + { + $number = self::CSS_NUMBER; + $pattern = "/^($number)(deg|grad|rad|turn)?$/i"; + + if (!preg_match($pattern, $val, $matches)) { + return null; + } + + $v = (float) $matches[1]; + $unit = strtolower($matches[2] ?? ""); + + switch ($unit) { + case "deg": + return $v; + case "grad": + return $v * 0.9; + case "rad": + return rad2deg($v); + case "turn": + return $v * 360; + default: + return $v === 0.0 ? $v : null; + } + } + + /** + * Common computation logic for `background-position` and `transform-origin`. + * + * @param string $v1 + * @param string $v2 + * + * @return (float|string|null)[] + */ + protected function computeBackgroundPositionTransformOrigin(string $v1, string $v2): array + { + $x = null; + $y = null; + + switch ($v1) { + case "left": + $x = 0.0; + break; + case "right": + $x = "100%"; + break; + case "top": + $y = 0.0; + break; + case "bottom": + $y = "100%"; + break; + case "center": + if ($v2 === "left" || $v2 === "right") { + $y = "50%"; + } else { + $x = "50%"; + } + break; + default: + $x = $this->compute_length_percentage($v1); + break; + } + + switch ($v2) { + case "left": + $x = 0.0; + break; + case "right": + $x = "100%"; + break; + case "top": + $y = 0.0; + break; + case "bottom": + $y = "100%"; + break; + case "center": + if ($v1 === "top" || $v1 === "bottom") { + $x = "50%"; + } else { + $y = "50%"; + } + break; + default: + $y = $this->compute_length_percentage($v2); + break; + } + + return [$x, $y]; + } + + /** + * @link https://www.w3.org/TR/css-lists-3/#typedef-counter-name + */ + protected function isValidCounterName(string $name): bool + { + return $name !== "none" + && !in_array($name, self::CUSTOM_IDENT_FORBIDDEN, true); + } + + /** + * @link https://www.w3.org/TR/css-counter-styles-3/#typedef-counter-style-name + */ + protected function isValidCounterStyleName(string $name): bool + { + return $name !== "none" + && !in_array($name, self::CUSTOM_IDENT_FORBIDDEN, true); + } + + /** + * Parse a property value with 1 to 4 components into 4 values, as required + * by shorthand properties such as `margin`, `padding`, and `border-radius`. + * + * @param string $prop The shorthand property with exactly 4 sub-properties to handle. + * @param string $value The property value to parse. + * + * @return string[] + */ + protected function set_quad_shorthand(string $prop, string $value): array + { + $v = $this->parse_property_value($value); + + switch (\count($v)) { + case 1: + $values = [$v[0], $v[0], $v[0], $v[0]]; + break; + case 2: + $values = [$v[0], $v[1], $v[0], $v[1]]; + break; + case 3: + $values = [$v[0], $v[1], $v[2], $v[1]]; + break; + case 4: + $values = [$v[0], $v[1], $v[2], $v[3]]; + break; + default: + return []; + } + + return array_combine(self::$_props_shorthand[$prop], $values); + } + + /*======================*/ + + /** + * @link https://www.w3.org/TR/CSS21/visuren.html#display-prop + */ + protected function _compute_display(string $val) + { + $val = strtolower($val); + + // Make sure that common valid, but unsupported display types have an + // appropriate fallback display type + switch ($val) { + case "flow-root": + case "flex": + case "grid": + case "table-caption": + $val = "block"; + break; + case "inline-flex": + case "inline-grid": + $val = "inline-block"; + break; + } + + if (!isset(self::$valid_display_types[$val])) { + return null; + } + + // https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo + if ($this->is_in_flow()) { + return $val; + } else { + switch ($val) { + case "inline": + case "inline-block": + // case "table-row-group": + // case "table-header-group": + // case "table-footer-group": + // case "table-row": + // case "table-cell": + // case "table-column-group": + // case "table-column": + // case "table-caption": + return "block"; + case "inline-table": + return "table"; + default: + return $val; + } + } + } + + /** + * @link https://www.w3.org/TR/CSS21/colors.html#propdef-color + */ + protected function _compute_color(string $color) + { + return $this->compute_color_value($color); + } + + /** + * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-color + */ + protected function _compute_background_color(string $color) + { + return $this->compute_color_value($color); + } + + /** + * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-image + */ + protected function _compute_background_image(string $val) + { + $parsed_val = $this->_stylesheet->resolve_url($val); + + if ($parsed_val === "none") { + return "none"; + } else { + return "url(\"" . str_replace("\"", "\\\"", $parsed_val) . "\")"; + } + } + + /** + * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-repeat + */ + protected function _compute_background_repeat(string $val) + { + $keywords = ["repeat", "repeat-x", "repeat-y", "no-repeat"]; + $val = strtolower($val); + return \in_array($val, $keywords, true) ? $val : null; + } + + /** + * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-attachment + */ + protected function _compute_background_attachment(string $val) + { + $keywords = ["scroll", "fixed"]; + $val = strtolower($val); + return \in_array($val, $keywords, true) ? $val : null; + } + + /** + * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-position + */ + protected function _compute_background_position(string $val) + { + $val = strtolower($val); + $parts = $this->parse_property_value($val); + $count = \count($parts); + + if ($count === 0 || $count > 2) { + return null; + } + + $v1 = $parts[0]; + $v2 = $parts[1] ?? "center"; + [$x, $y] = $this->computeBackgroundPositionTransformOrigin($v1, $v2); + + if ($x === null || $y === null) { + return null; + } + + return [$x, $y]; + } + + /** + * Compute `background-size`. + * + * Computes to one of the following values: + * * `cover` + * * `contain` + * * `[width, height]`, each being a length, percentage, or `auto` + * + * @link https://www.w3.org/TR/css-backgrounds-3/#background-size + */ + protected function _compute_background_size(string $val) + { + $val = strtolower($val); + + if ($val === "cover" || $val === "contain") { + return $val; + } + + $parts = $this->parse_property_value($val); + $count = \count($parts); + + if ($count === 0 || $count > 2) { + return null; + } + + $width = $parts[0]; + if ($width !== "auto") { + $width = $this->compute_length_percentage_positive($width); + } + + $height = $parts[1] ?? "auto"; + if ($height !== "auto") { + $height = $this->compute_length_percentage_positive($height); + } + + if ($width === null || $height === null) { + return null; + } + + return [$width, $height]; + } + + /** + * @link https://www.w3.org/TR/css-backgrounds-3/#propdef-background + */ + protected function _set_background(string $value): array + { + $components = $this->parse_property_value($value); + $props = []; + $pos_size = []; + + foreach ($components as $val) { + $lower = strtolower($val); + + if ($lower === "none") { + $props["background_image"] = $lower; + } elseif (strncmp($lower, "url(", 4) === 0) { + $props["background_image"] = $val; + } elseif ($lower === "scroll" || $lower === "fixed") { + $props["background_attachment"] = $lower; + } elseif ($lower === "repeat" || $lower === "repeat-x" || $lower === "repeat-y" || $lower === "no-repeat") { + $props["background_repeat"] = $lower; + } elseif ($this->is_color_value($lower)) { + $props["background_color"] = $lower; + } else { + $pos_size[] = $lower; + } + } + + if (\count($pos_size)) { + // Split value list at "/" + $index = array_search("/", $pos_size, true); + + if ($index !== false) { + $pos = \array_slice($pos_size, 0, $index); + $size = \array_slice($pos_size, $index + 1); + } else { + $pos = $pos_size; + $size = []; + } + + $props["background_position"] = implode(" ", $pos); + + if (\count($size)) { + $props["background_size"] = implode(" ", $size); + } + } + + return $props; + } + + /** + * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-family + */ + protected function _compute_font_family(string $val) + { + return array_map( + function ($name) { + return trim($name, " '\""); + }, + preg_split("/\s*,\s*/", $val) + ); + } + + /** + * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-size + */ + protected function _compute_font_size(string $val) + { + $val = strtolower($val); + $parentFontSize = isset($this->parent_style) + ? $this->parent_style->__get("font_size") + : self::$default_font_size; + + switch ($val) { + case "xx-small": + case "x-small": + case "small": + case "medium": + case "large": + case "x-large": + case "xx-large": + $computed = self::$default_font_size * self::$font_size_keywords[$val]; + break; + + case "smaller": + $computed = 8 / 9 * $parentFontSize; + break; + + case "larger": + $computed = 6 / 5 * $parentFontSize; + break; + + default: + $computed = $this->single_length_in_pt($val, $parentFontSize, $parentFontSize); + + // Negative non-`calc` values are invalid + if ($computed === null + || ($computed < 0 && !preg_match("/^-?[_a-zA-Z]/", $val)) + ) { + return null; + } + break; + } + + return $computed; + } + + /** + * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-style + */ + protected function _compute_font_style(string $val) + { + $val = strtolower($val); + return $val === "normal" || $val === "italic" || $val === "oblique" + ? $val + : null; + } + + /** + * @link https://www.w3.org/TR/css-fonts-4/#propdef-font-weight + */ + protected function _compute_font_weight(string $val) + { + $val = strtolower($val); + + switch ($val) { + case "normal": + return 400; + + case "bold": + return 700; + + case "bolder": + // https://www.w3.org/TR/css-fonts-4/#relative-weights + $w = isset($this->parent_style) + ? $this->parent_style->__get("font_weight") + : 400; + + if ($w < 350) { + return 400; + } elseif ($w < 550) { + return 700; + } elseif ($w < 900) { + return 900; + } else { + return $w; + } + + case "lighter": + // https://www.w3.org/TR/css-fonts-4/#relative-weights + $w = isset($this->parent_style) + ? $this->parent_style->__get("font_weight") + : 400; + + if ($w < 100) { + return $w; + } elseif ($w < 550) { + return 100; + } elseif ($w < 750) { + return 400; + } else { + return 700; + } + + default: + $number = self::CSS_NUMBER; + $weight = preg_match("/^$number$/", $val) + ? (int) $val + : null; + return $weight !== null && $weight >= 1 && $weight <= 1000 + ? $weight + : null; + } + } + + /** + * @link https://www.w3.org/TR/css-fonts-4/#src-desc + */ + protected function _compute_src(string $val) + { + return $val; + } + + /** + * Handle the `font` shorthand property. + * + * `[ font-style || font-variant || font-weight ] font-size [ / line-height ] font-family` + * + * @link https://www.w3.org/TR/CSS21/fonts.html#font-shorthand + */ + protected function _set_font(string $value): array + { + $value = strtolower($value); + $components = $this->parse_property_value($value); + $props = []; + + $number = self::CSS_NUMBER; + $unit = "pt|px|pc|rem|em|ex|in|cm|mm|%"; + $sizePattern = "/^(xx-small|x-small|small|medium|large|x-large|xx-large|smaller|larger|$number(?:$unit)|0)$/"; + $sizeIndex = null; + + // Find index of font-size to split the component list + foreach ($components as $i => $val) { + if (preg_match($sizePattern, $val)) { + $sizeIndex = $i; + $props["font_size"] = $val; + break; + } + } + + // `font-size` is mandatory + if ($sizeIndex === null) { + return []; + } + + // `font-style`, `font-variant`, `font-weight` in any order + $styleVariantWeight = \array_slice($components, 0, $sizeIndex); + $stylePattern = "/^(italic|oblique)$/"; + $variantPattern = "/^(small-caps)$/"; + $weightPattern = "/^(bold|bolder|lighter|$number)$/"; + + if (\count($styleVariantWeight) > 3) { + return []; + } + + foreach ($styleVariantWeight as $val) { + if ($val === "normal") { + // Ignore any `normal` value, as it is valid and the initial + // value for all three properties + } elseif (!isset($props["font_style"]) && preg_match($stylePattern, $val)) { + $props["font_style"] = $val; + } elseif (!isset($props["font_variant"]) && preg_match($variantPattern, $val)) { + $props["font_variant"] = $val; + } elseif (!isset($props["font_weight"]) && preg_match($weightPattern, $val)) { + $props["font_weight"] = $val; + } else { + // Duplicates and other values disallowed here + return []; + } + } + + // Optional slash + `line-height` followed by mandatory `font-family` + $lineFamily = \array_slice($components, $sizeIndex + 1); + $hasLineHeight = $lineFamily !== [] && $lineFamily[0] === "/"; + $lineHeight = $hasLineHeight ? \array_slice($lineFamily, 1, 1) : []; + $fontFamily = $hasLineHeight ? \array_slice($lineFamily, 2) : $lineFamily; + $lineHeightPattern = "/^(normal|$number(?:$unit)?)$/"; + + // Missing `font-family` or `line-height` after slash + if ($fontFamily === [] + || ($hasLineHeight && !preg_match($lineHeightPattern, $lineHeight[0])) + ) { + return []; + } + + if ($hasLineHeight) { + $props["line_height"] = $lineHeight[0]; + } + + $props["font_family"] = implode("", $fontFamily); + + return $props; + } + + /** + * Compute `text-align`. + * + * If no alignment is set on the element and the direction is rtl then + * the property is set to "right", otherwise it is set to "left". + * + * @link https://www.w3.org/TR/CSS21/text.html#propdef-text-align + */ + protected function _compute_text_align(string $val) + { + $alignment = strtolower($val); + + if ($alignment === "") { + $alignment = $this->__get("direction") === "rtl" + ? "right" + : "left"; + } + + if (!\in_array($alignment, self::TEXT_ALIGN_KEYWORDS, true)) { + return null; + } + + return $alignment; + } + + /** + * @link https://www.w3.org/TR/css-text-4/#word-spacing-property + */ + protected function _compute_word_spacing(string $val) + { + $val = strtolower($val); + + if ($val === "normal") { + return 0.0; + } + + return $this->compute_length_percentage($val); + } + + /** + * @link https://www.w3.org/TR/css-text-4/#letter-spacing-property + */ + protected function _compute_letter_spacing(string $val) + { + $val = strtolower($val); + + if ($val === "normal") { + return 0.0; + } + + return $this->compute_length_percentage($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-line-height + */ + protected function _compute_line_height(string $val) + { + $val = strtolower($val); + + if ($val === "normal") { + return $val; + } + + // Compute number values to string and lengths to float (in pt) + if (is_numeric($val)) { + return (string) $val; + } + + $font_size = $this->__get("font_size"); + $computed = $this->single_length_in_pt($val, $font_size); + + // Negative non-`calc` values are invalid + if ($computed === null + || ($computed < 0 && !preg_match("/^-?[_a-zA-Z]/", $val)) + ) { + return null; + } + + return $computed; + } + + /** + * @link https://www.w3.org/TR/css-text-3/#text-indent-property + */ + protected function _compute_text_indent(string $val) + { + return $this->compute_length_percentage($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/page.html#propdef-page-break-before + */ + protected function _compute_page_break_before(string $val) + { + $break = strtolower($val); + + if ($break === "left" || $break === "right") { + $break = "always"; + } + + return $break; + } + + /** + * @link https://www.w3.org/TR/CSS21/page.html#propdef-page-break-after + */ + protected function _compute_page_break_after(string $val) + { + $break = strtolower($val); + + if ($break === "left" || $break === "right") { + $break = "always"; + } + + return $break; + } + + /** + * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-width + */ + protected function _compute_width(string $val) + { + $val = strtolower($val); + + if ($val === "auto") { + return $val; + } + + return $this->compute_length_percentage_positive($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-height + */ + protected function _compute_height(string $val) + { + $val = strtolower($val); + + if ($val === "auto") { + return $val; + } + + return $this->compute_length_percentage_positive($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-min-width + */ + protected function _compute_min_width(string $val) + { + $val = strtolower($val); + + // Legacy support for `none`, not covered by spec + if ($val === "auto" || $val === "none") { + return "auto"; + } + + return $this->compute_length_percentage_positive($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-min-height + */ + protected function _compute_min_height(string $val) + { + $val = strtolower($val); + + // Legacy support for `none`, not covered by spec + if ($val === "auto" || $val === "none") { + return "auto"; + } + + return $this->compute_length_percentage_positive($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-max-width + */ + protected function _compute_max_width(string $val) + { + $val = strtolower($val); + + // Legacy support for `auto`, not covered by spec + if ($val === "none" || $val === "auto") { + return "none"; + } + + return $this->compute_length_percentage_positive($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-max-height + */ + protected function _compute_max_height(string $val) + { + $val = strtolower($val); + + // Legacy support for `auto`, not covered by spec + if ($val === "none" || $val === "auto") { + return "none"; + } + + return $this->compute_length_percentage_positive($val); + } + + /** + * @link https://www.w3.org/TR/css-position-3/#inset-properties + * @link https://www.w3.org/TR/css-position-3/#propdef-inset + */ + protected function _set_inset(string $val): array + { + return $this->set_quad_shorthand("inset", $val); + } + + /** + * @param string $val + * @return float|string|null + */ + protected function compute_box_inset(string $val) + { + $val = strtolower($val); + + if ($val === "auto") { + return $val; + } + + return $this->compute_length_percentage($val); + } + + protected function _compute_top(string $val) + { + return $this->compute_box_inset($val); + } + + protected function _compute_right(string $val) + { + return $this->compute_box_inset($val); + } + + protected function _compute_bottom(string $val) + { + return $this->compute_box_inset($val); + } + + protected function _compute_left(string $val) + { + return $this->compute_box_inset($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/box.html#margin-properties + * @link https://www.w3.org/TR/CSS21/box.html#propdef-margin + */ + protected function _set_margin(string $val): array + { + return $this->set_quad_shorthand("margin", $val); + } + + /** + * @param string $val + * @return float|string|null + */ + protected function compute_margin(string $val) + { + $val = strtolower($val); + + // Legacy support for `none` keyword, not covered by spec + if ($val === "none") { + return 0.0; + } + + if ($val === "auto") { + return $val; + } + + return $this->compute_length_percentage($val); + } + + protected function _compute_margin_top(string $val) + { + return $this->compute_margin($val); + } + + protected function _compute_margin_right(string $val) + { + return $this->compute_margin($val); + } + + protected function _compute_margin_bottom(string $val) + { + return $this->compute_margin($val); + } + + protected function _compute_margin_left(string $val) + { + return $this->compute_margin($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/box.html#padding-properties + * @link https://www.w3.org/TR/CSS21/box.html#propdef-padding + */ + protected function _set_padding(string $val): array + { + return $this->set_quad_shorthand("padding", $val); + } + + /** + * @param string $val + * @return float|string|null + */ + protected function compute_padding(string $val) + { + $val = strtolower($val); + + // Legacy support for `none` keyword, not covered by spec + if ($val === "none") { + return 0.0; + } + + return $this->compute_length_percentage_positive($val); + } + + protected function _compute_padding_top(string $val) + { + return $this->compute_padding($val); + } + + protected function _compute_padding_right(string $val) + { + return $this->compute_padding($val); + } + + protected function _compute_padding_bottom(string $val) + { + return $this->compute_padding($val); + } + + protected function _compute_padding_left(string $val) + { + return $this->compute_padding($val); + } + + /** + * @param string $value `width || style || color` + * @param string[] $styles The list of border styles to accept. + * + * @return string[]|null Array of `[width, style, color]`, or `null` if the declaration is invalid. + */ + protected function parse_border_side(string $value, array $styles = self::BORDER_STYLES): ?array + { + $value = strtolower($value); + $components = $this->parse_property_value($value); + $width = null; + $style = null; + $color = null; + + foreach ($components as $val) { + if ($style === null && \in_array($val, $styles, true)) { + $style = $val; + } elseif ($color === null && $this->is_color_value($val)) { + $color = $val; + } elseif ($width === null) { + // Assume width + $width = $val; + } else { + // Duplicates are not allowed + return null; + } + } + + return [$width, $style, $color]; + } + + /** + * @link https://www.w3.org/TR/CSS21/box.html#border-properties + * @link https://www.w3.org/TR/CSS21/box.html#propdef-border + */ + protected function _set_border(string $value): array + { + $values = $this->parse_border_side($value); + + if ($values === null) { + return []; + } + + return array_merge( + array_combine(self::$_props_shorthand["border_top"], $values), + array_combine(self::$_props_shorthand["border_right"], $values), + array_combine(self::$_props_shorthand["border_bottom"], $values), + array_combine(self::$_props_shorthand["border_left"], $values) + ); + } + + /** + * @param string $prop + * @param string $value + * @return array + */ + protected function set_border_side(string $prop, string $value): array + { + $values = $this->parse_border_side($value); + + if ($values === null) { + return []; + } + + return array_combine(self::$_props_shorthand[$prop], $values); + } + + protected function _set_border_top(string $val): array + { + return $this->set_border_side("border_top", $val); + } + + protected function _set_border_right(string $val): array + { + return $this->set_border_side("border_right", $val); + } + + protected function _set_border_bottom(string $val): array + { + return $this->set_border_side("border_bottom", $val); + } + + protected function _set_border_left(string $val): array + { + return $this->set_border_side("border_left", $val); + } + + /** + * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-color + */ + protected function _set_border_color(string $val): array + { + return $this->set_quad_shorthand("border_color", $val); + } + + protected function _compute_border_top_color(string $val) + { + return $this->compute_color_value($val); + } + + protected function _compute_border_right_color(string $val) + { + return $this->compute_color_value($val); + } + + protected function _compute_border_bottom_color(string $val) + { + return $this->compute_color_value($val); + } + + protected function _compute_border_left_color(string $val) + { + return $this->compute_color_value($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-style + */ + protected function _set_border_style(string $val): array + { + return $this->set_quad_shorthand("border_style", $val); + } + + protected function _compute_border_top_style(string $val) + { + return $this->compute_border_style($val); + } + + protected function _compute_border_right_style(string $val) + { + return $this->compute_border_style($val); + } + + protected function _compute_border_bottom_style(string $val) + { + return $this->compute_border_style($val); + } + + protected function _compute_border_left_style(string $val) + { + return $this->compute_border_style($val); + } + + /** + * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-width + */ + protected function _set_border_width(string $val): array + { + return $this->set_quad_shorthand("border_width", $val); + } + + protected function _compute_border_top_width(string $val) + { + return $this->compute_line_width($val, "border_top_style"); + } + + protected function _compute_border_right_width(string $val) + { + return $this->compute_line_width($val, "border_right_style"); + } + + protected function _compute_border_bottom_width(string $val) + { + return $this->compute_line_width($val, "border_bottom_style"); + } + + protected function _compute_border_left_width(string $val) + { + return $this->compute_line_width($val, "border_left_style"); + } + + /** + * @link https://www.w3.org/TR/css-backgrounds-3/#corners + * @link https://www.w3.org/TR/css-backgrounds-3/#propdef-border-radius + */ + protected function _set_border_radius(string $val): array + { + return $this->set_quad_shorthand("border_radius", $val); + } + + protected function _compute_border_top_left_radius(string $val) + { + return $this->compute_length_percentage_positive($val); + } + + protected function _compute_border_top_right_radius(string $val) + { + return $this->compute_length_percentage_positive($val); + } + + protected function _compute_border_bottom_right_radius(string $val) + { + return $this->compute_length_percentage_positive($val); + } + + protected function _compute_border_bottom_left_radius(string $val) + { + return $this->compute_length_percentage_positive($val); + } + + /** + * @link https://www.w3.org/TR/css-ui-4/#outline-props + * @link https://www.w3.org/TR/css-ui-4/#propdef-outline + */ + protected function _set_outline(string $value): array + { + $values = $this->parse_border_side($value, self::OUTLINE_STYLES); + + if ($values === null) { + return []; + } + + return array_combine(self::$_props_shorthand["outline"], $values); + } + + protected function _compute_outline_color(string $val) + { + return $this->compute_color_value($val); + } + + protected function _compute_outline_style(string $val) + { + $val = strtolower($val); + return \in_array($val, self::OUTLINE_STYLES, true) ? $val : null; + } + + protected function _compute_outline_width(string $val) + { + return $this->compute_line_width($val, "outline_style"); + } + + /** + * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-offset + */ + protected function _compute_outline_offset(string $val) + { + return $this->compute_length($val); + } + + /** + * Compute `border-spacing` to two lengths of the form + * `[horizontal, vertical]`. + * + * @link https://www.w3.org/TR/CSS21/tables.html#propdef-border-spacing + */ + protected function _compute_border_spacing(string $val) + { + $val = strtolower($val); + $parts = $this->parse_property_value($val); + $count = \count($parts); + + if ($count === 0 || $count > 2) { + return null; + } + + $h = $this->compute_length_positive($parts[0]); + $v = isset($parts[1]) + ? $this->compute_length_positive($parts[1]) + : $h; + + if ($h === null || $v === null) { + return null; + } + + return [$h, $v]; + } + + /** + * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-image + */ + protected function _compute_list_style_image(string $val) + { + $parsed_val = $this->_stylesheet->resolve_url($val); + + if ($parsed_val === "none") { + return "none"; + } else { + return "url(\"" . str_replace("\"", "\\\"", $parsed_val) . "\")"; + } + } + + /** + * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-type + */ + protected function _compute_list_style_position(string $val) + { + $val = strtolower($val); + return $val === "inside" || $val === "outside" ? $val : null; + } + + /** + * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-type + */ + protected function _compute_list_style_type(string $val) + { + $val = strtolower($val); + + if ($val === "none") { + return $val; + } + + $ident = self::CSS_IDENTIFIER; + return $val !== "default" && preg_match("/^$ident$/", $val) + ? $val + : null; + } + + /** + * Handle the `list-style` shorthand property. + * + * `[ list-style-position || list-style-image || list-style-type ]` + * + * @link https://www.w3.org/TR/css-lists-3/#list-style-property + */ + protected function _set_list_style(string $value): array + { + $components = $this->parse_property_value($value); + $none = 0; + $position = null; + $image = null; + $type = null; + + foreach ($components as $val) { + $lower = strtolower($val); + + // `none` can occur max 2 times (for image and type each) + if ($none < 2 && $lower === "none") { + $none++; + } elseif ($position === null && ($lower === "inside" || $lower === "outside")) { + $position = $lower; + } elseif ($image === null && strncmp($lower, "url(", 4) === 0) { + $image = $val; + } elseif ($type === null) { + $type = $val; + } else { + // Duplicates are not allowed + return []; + } + } + + // From the spec: + // Using a value of `none` in the shorthand is potentially ambiguous, as + // `none` is a valid value for both `list-style-image` and `list-style-type`. + // To resolve this ambiguity, a value of `none` in the shorthand must be + // applied to whichever of the two properties aren’t otherwise set by + // the shorthand. + if ($none === 2) { + if ($image !== null || $type !== null) { + return []; + } + + $image = "none"; + $type = "none"; + } elseif ($none === 1) { + if ($image !== null && $type !== null) { + return []; + } + + $image = $image ?? "none"; + $type = $type ?? "none"; + } + + return [ + "list_style_position" => $position, + "list_style_image" => $image, + "list_style_type" => $type + ]; + } + + /** + * @param string $value + * @param int $default + * @param bool $sumDuplicates + * + * @return array|string|null + */ + protected function compute_counter_prop(string $value, int $default, bool $sumDuplicates = false) + { + $lower = strtolower($value); + + if ($lower === "none") { + return $lower; + } + + $ident = self::CSS_IDENTIFIER; + $integer = self::CSS_INTEGER; + $counterDef = "($ident)(?:\s+($integer))?"; + $validationPattern = "/^$counterDef(\s+$counterDef)*$/"; + + if (!preg_match($validationPattern, $value)) { + return null; + } + + preg_match_all("/$counterDef/", $value, $matches, PREG_SET_ORDER); + $counters = []; + + foreach ($matches as $match) { + $name = $match[1]; + + if (!$this->isValidCounterName($name)) { + return null; + } + + $value = isset($match[2]) ? (int) $match[2] : $default; + $counters[$name] = $sumDuplicates + ? ($counters[$name] ?? 0) + $value + : $value; + } + + return $counters; + } + + /** + * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-increment + */ + protected function _compute_counter_increment(string $val) + { + return $this->compute_counter_prop($val, 1, true); + } + + /** + * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-reset + */ + protected function _compute_counter_reset(string $val) + { + return $this->compute_counter_prop($val, 0); + } + + /** + * @link https://www.w3.org/TR/css-content-3/#quotes + */ + protected function _compute_quotes(string $val) + { + $lower = strtolower($val); + + // `auto` is resolved in the getter, so it can inherit as is + if ($lower === "none" || $lower === "auto") { + return $lower; + } + + $components = $this->parse_property_value($val); + $quotes = []; + + foreach ($components as $value) { + if (strncmp($value, '"', 1) !== 0 + && strncmp($value, "'", 1) !== 0 + ) { + return null; + } + + $quotes[] = $this->_stylesheet->parse_string($value); + } + + if ($quotes === [] || \count($quotes) % 2 !== 0) { + return null; + } + + return array_chunk($quotes, 2); + } + + /** + * @link https://www.w3.org/TR/CSS21/generate.html#propdef-content + * @link https://www.w3.org/TR/css-content-3/#propdef-content + */ + protected function _compute_content(string $val) + { + $lower = strtolower($val); + + if ($lower === "normal" || $lower === "none") { + return $lower; + } + + $components = $this->parse_property_value($val); + $parts = []; + + if ($components === []) { + return null; + } + + foreach ($components as $value) { + // String + if (strncmp($value, '"', 1) === 0 || strncmp($value, "'", 1) === 0) { + $parts[] = new StringPart($this->_stylesheet->parse_string($value)); + continue; + } + + $lower = strtolower($value); + + // Keywords + if ($lower === "open-quote") { + $parts[] = new OpenQuote; + continue; + } elseif ($lower === "close-quote") { + $parts[] = new CloseQuote; + continue; + } elseif ($lower === "no-open-quote") { + $parts[] = new NoOpenQuote; + continue; + } elseif ($lower === "no-close-quote") { + $parts[] = new NoCloseQuote; + continue; + } + + // Functional components + $pos = strpos($lower, "("); + + if ($pos === false) { + return null; + } + + // `parse_property_value` ensures that the value is of the form + // `function(arguments)` at this point + $function = substr($lower, 0, $pos); + $arguments = trim(substr($value, $pos + 1, -1)); + + // attr() + if ($function === "attr") { + $attr = strtolower($arguments); + + if ($attr === "") { + return null; + } + + $parts[] = new Attr($attr); + } + + // counter(name [, style]) + elseif ($function === "counter") { + $ident = self::CSS_IDENTIFIER; + + if (!preg_match("/^($ident)(?:\s*,\s*($ident))?$/", $arguments, $matches)) { + return null; + } + + $name = $matches[1]; + $type = isset($matches[2]) ? strtolower($matches[2]) : "decimal"; + + if (!$this->isValidCounterName($name) + || !$this->isValidCounterStyleName($type) + ) { + return null; + } + + $parts[] = new Counter($name, $type); + } + + // counters(name, string [, style]) + elseif ($function === "counters") { + $ident = self::CSS_IDENTIFIER; + $string = self::CSS_STRING; + + if (!preg_match("/^($ident)\s*,\s*($string)(?:\s*,\s*($ident))?$/", $arguments, $matches)) { + return null; + } + + $name = $matches[1]; + $string = $this->_stylesheet->parse_string($matches[2]); + $type = isset($matches[3]) ? strtolower($matches[3]) : "decimal"; + + if (!$this->isValidCounterName($name) + || !$this->isValidCounterStyleName($type) + ) { + return null; + } + + $parts[] = new Counters($name, $string, $type); + } + + // url() + elseif ($function === "url") { + $url = $this->_stylesheet->parse_string($arguments); + $parts[] = new Url($url); + } + + else { + return null; + } + } + + return $parts; + } + + /** + * @link https://www.w3.org/TR/css-page-3/#page-size-prop + */ + protected function _compute_size(string $val) + { + $val = strtolower($val); + + if ($val === "auto") { + return $val; + } + + $parts = $this->parse_property_value($val); + $count = \count($parts); + + if ($count === 0 || $count > 3) { + return null; + } + + $size = null; + $orientation = null; + $lengths = []; + + foreach ($parts as $part) { + if ($size === null && isset(CPDF::$PAPER_SIZES[$part])) { + $size = $part; + } elseif ($orientation === null && ($part === "portrait" || $part === "landscape")) { + $orientation = $part; + } else { + $lengths[] = $part; + } + } + + if ($size !== null && $lengths !== []) { + return null; + } + + if ($size !== null) { + // Standard paper size + [$l1, $l2] = \array_slice(CPDF::$PAPER_SIZES[$size], 2, 2); + } elseif ($lengths === []) { + // Orientation only, use default paper size + $dims = $this->_stylesheet->get_dompdf()->getPaperSize(); + [$l1, $l2] = \array_slice($dims, 2, 2); + } else { + // Custom paper size + $l1 = $this->compute_length_positive($lengths[0]); + $l2 = isset($lengths[1]) ? $this->compute_length_positive($lengths[1]) : $l1; + + if ($l1 === null || $l2 === null) { + return null; + } + } + + if (($orientation === "portrait" && $l1 > $l2) + || ($orientation === "landscape" && $l2 > $l1) + ) { + return [$l2, $l1]; + } + + return [$l1, $l2]; + } + + /** + * @link https://www.w3.org/TR/css-transforms-1/#transform-property + */ + protected function _compute_transform(string $val) + { + $val = strtolower($val); + + if ($val === "none") { + return []; + } + + $parts = $this->parse_property_value($val); + $transforms = []; + + if ($parts === []) { + return null; + } + + foreach ($parts as $part) { + if (!preg_match("/^([a-z]+)\((.+)\)$/s", $part, $matches)) { + return null; + } + + $name = $matches[1]; + $arguments = trim($matches[2]); + $values = $this->parse_property_value($arguments); + $values = array_values(array_filter($values, function ($v) { + return $v !== ","; + })); + $count = \count($values); + + if ($count === 0) { + return null; + } + + switch ($name) { + // case "matrix": + // if ($count !== 6) { + // return null; + // } + + // $values = array_map([$this, "compute_number"], $values); + // break; + + // units + case "translate": + if ($count > 2) { + return null; + } + + $values = [ + $this->compute_length_percentage($values[0]), + isset($values[1]) ? $this->compute_length_percentage($values[1]) : 0.0 + ]; + break; + + case "translatex": + if ($count > 1) { + return null; + } + + $name = "translate"; + $values = [$this->compute_length_percentage($values[0]), 0.0]; + break; + + case "translatey": + if ($count > 1) { + return null; + } + + $name = "translate"; + $values = [0.0, $this->compute_length_percentage($values[0])]; + break; + + // units + case "scale": + if ($count > 2) { + return null; + } + + $v0 = $this->compute_number($values[0]); + $v1 = isset($values[1]) ? $this->compute_number($values[1]) : $v0; + $values = [$v0, $v1]; + break; + + case "scalex": + if ($count > 1) { + return null; + } + + $name = "scale"; + $values = [$this->compute_number($values[0]), 1.0]; + break; + + case "scaley": + if ($count > 1) { + return null; + } + + $name = "scale"; + $values = [1.0, $this->compute_number($values[0])]; + break; + + // units + case "rotate": + if ($count > 1) { + return null; + } + + $values = [$this->compute_angle_or_zero($values[0])]; + break; + + case "skew": + if ($count > 2) { + return null; + } + + $values = [ + $this->compute_angle_or_zero($values[0]), + isset($values[1]) ? $this->compute_angle_or_zero($values[1]) : 0.0 + ]; + break; + + case "skewx": + if ($count > 1) { + return null; + } + + $name = "skew"; + $values = [$this->compute_angle_or_zero($values[0]), 0.0]; + break; + + case "skewy": + if ($count > 1) { + return null; + } + + $name = "skew"; + $values = [0.0, $this->compute_angle_or_zero($values[0])]; + break; + + default: + return null; + } + + foreach ($values as $v) { + if ($v === null) { + return null; + } + } + + $transforms[] = [$name, $values]; + } + + return $transforms; + } + + /** + * @link https://www.w3.org/TR/css-transforms-1/#transform-origin-property + */ + protected function _compute_transform_origin(string $val) + { + $val = strtolower($val); + $parts = $this->parse_property_value($val); + $count = \count($parts); + + if ($count === 0 || $count > 3) { + return null; + } + + $v1 = $parts[0]; + $v2 = $parts[1] ?? "center"; + [$x, $y] = $this->computeBackgroundPositionTransformOrigin($v1, $v2); + $z = $count === 3 ? $this->compute_length($parts[2]) : 0.0; + + if ($x === null || $y === null || $z === null) { + return null; + } + + return [$x, $y, $z]; + } + + /** + * @param string $val + * @return string|null + */ + protected function parse_image_resolution(string $val): ?string + { + // If exif data could be get: + // $re = '/^\s*(\d+|normal|auto)(?:\s*,\s*(\d+|normal))?\s*$/'; + + $val = strtolower($val); + $re = '/^\s*(\d+|normal|auto)\s*$/'; + + if (!preg_match($re, $val, $matches)) { + return null; + } + + return $matches[1]; + } + + /** + * auto | normal | dpi + */ + protected function _compute_background_image_resolution(string $val) + { + return $this->parse_image_resolution($val); + } + + /** + * auto | normal | dpi + */ + protected function _compute_image_resolution(string $val) + { + return $this->parse_image_resolution($val); + } + + /** + * @link https://www.w3.org/TR/css-break-3/#propdef-orphans + */ + protected function _compute_orphans(string $val) + { + return $this->compute_integer($val); + } + + /** + * @link https://www.w3.org/TR/css-break-3/#propdef-widows + */ + protected function _compute_widows(string $val) + { + return $this->compute_integer($val); + } + + /** + * @link https://www.w3.org/TR/css-color-4/#propdef-opacity + */ + protected function _compute_opacity(string $val) + { + $number = self::CSS_NUMBER; + $pattern = "/^($number)(%?)$/"; + + if (!preg_match($pattern, $val, $matches)) { + return null; + } + + $v = (float) $matches[1]; + $percent = $matches[2] === "%"; + $opacity = $percent ? ($v / 100) : $v; + + return max(0.0, min($opacity, 1.0)); + } + + /** + * @link https://www.w3.org/TR/CSS21//visuren.html#propdef-z-index + */ + protected function _compute_z_index(string $val) + { + $val = strtolower($val); + + if ($val === "auto") { + return $val; + } + + return $this->compute_integer($val); + } + + /** + * @param FontMetrics $fontMetrics + * @return $this + */ + public function setFontMetrics(FontMetrics $fontMetrics) + { + $this->fontMetrics = $fontMetrics; + return $this; + } + + /** + * @return FontMetrics + */ + public function getFontMetrics() + { + return $this->fontMetrics; + } + + /** + * Generate a string representation of the Style + * + * This dumps the entire property array into a string via print_r. Useful + * for debugging. + * + * @return string + */ + /*DEBUGCSS print: see below additional debugging util*/ + public function __toString(): string + { + $parent_font_size = $this->parent_style + ? $this->parent_style->font_size + : self::$default_font_size; + + return print_r(array_merge(["parent_font_size" => $parent_font_size], + $this->_props), true); + } + + /*DEBUGCSS*/ + public function debug_print(): void + { + $parent_font_size = $this->parent_style + ? $this->parent_style->font_size + : self::$default_font_size; + + print " parent_font_size:" . $parent_font_size . ";\n"; + print " Props [\n"; + print " specified [\n"; + foreach ($this->_props as $prop => $val) { + print ' ' . $prop . ': ' . preg_replace("/\r\n/", ' ', print_r($val, true)); + if (isset($this->_important_props[$prop])) { + print ' !important'; + } + print ";\n"; + } + print " ]\n"; + print " computed [\n"; + foreach ($this->_props_computed as $prop => $val) { + print ' ' . $prop . ': ' . preg_replace("/\r\n/", ' ', print_r($val, true)); + print ";\n"; + } + print " ]\n"; + print " cached [\n"; + foreach ($this->_props_used as $prop => $val) { + print ' ' . $prop . ': ' . preg_replace("/\r\n/", ' ', print_r($val, true)); + print ";\n"; + } + print " ]\n"; + print " ]\n"; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Stylesheet.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Stylesheet.php new file mode 100644 index 0000000..001a729 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Css/Stylesheet.php @@ -0,0 +1,1894 @@ +(?[\'"])(?.*?)(?url\(\s*(?[\'"]?)(?.*?)(?(CSS_URL_FN_QUOTE)(?local\(\s*(?[\'"]?)(?.*?)(?(CSS_LOCAL_FN_QUOTE)(?(?:(?:(?:(?only|not)\s+)?(?all|aural|bitmap|braille|dompdf|embossed|handheld|paged|print|projection|screen|speech|static|tty|tv|visual))|(?:\(\s*(?(?:(?:(?:min|max)-)?(?:width|height))|orientation|[^:]*?)\s*(?:\:\s*(?.*?)\s*)?\))))'; + + /* + * The highest possible specificity is 0x01000000 (and that is only for author + * stylesheets, as it is for inline styles). Origin precedence can be achieved by + * adding multiples of 0x10000000 to the actual specificity. Important + * declarations are handled in Style; though technically they should be handled + * here so that user important declarations can be made to take precedence over + * user important declarations, this doesn't matter in practice as Dompdf does + * not support user stylesheets, and user agent stylesheets can not include + * important declarations. + */ + private static $_stylesheet_origins = [ + self::ORIG_UA => 0x00000000, // user agent declarations + self::ORIG_USER => 0x10000000, // user normal declarations + self::ORIG_AUTHOR => 0x30000000, // author normal declarations + ]; + + /** + * Non-CSS presentational hints (i.e. HTML 4 attributes) are handled as if added + * to the beginning of an author stylesheet, i.e. anything in author stylesheets + * should override them. + */ + const SPEC_NON_CSS = 0x20000000; + + /** + * Current dompdf instance + * + * @var Dompdf + */ + private $_dompdf; + + /** + * Array of currently defined styles + * + * @var array + */ + private $_styles; + + /** + * Array of embedded files (dataURIs) found in the parsed CSS + * + * @var array + */ + private $_blobs; + + /** + * Base protocol of the document being parsed + * Used to handle relative urls. + * + * @var string + */ + private $_protocol = ""; + + /** + * Base hostname of the document being parsed + * Used to handle relative urls. + * + * @var string + */ + private $_base_host = ""; + + /** + * Base path of the document being parsed + * Used to handle relative urls. + * + * @var string + */ + private $_base_path = ""; + + /** + * The styles defined by @page rules + * + * @var array + $child = $child->nextSibling; + } + } else { + $css = $tag->nodeValue; + } + + // Set the base path of the Stylesheet to that of the file being processed + $this->css->set_protocol($this->protocol); + $this->css->set_host($this->baseHost); + $this->css->set_base_path($this->basePath); + + $this->css->load_css($css, Stylesheet::ORIG_AUTHOR); + break; + } + + // Set the base path of the Stylesheet to that of the file being processed + $this->css->set_protocol($this->protocol); + $this->css->set_host($this->baseHost); + $this->css->set_base_path($this->basePath); + } + } + + /** + * @param string $cacheId + * @deprecated + */ + public function enable_caching($cacheId) + { + $this->enableCaching($cacheId); + } + + /** + * Enable experimental caching capability + * + * @param string $cacheId + */ + public function enableCaching($cacheId) + { + $this->cacheId = $cacheId; + } + + /** + * @param string $value + * @return bool + * @deprecated + */ + public function parse_default_view($value) + { + return $this->parseDefaultView($value); + } + + /** + * @param string $value + * @return bool + */ + public function parseDefaultView($value) + { + $valid = ["XYZ", "Fit", "FitH", "FitV", "FitR", "FitB", "FitBH", "FitBV"]; + + $options = preg_split("/\s*,\s*/", trim($value)); + $defaultView = array_shift($options); + + if (!in_array($defaultView, $valid)) { + return false; + } + + $this->setDefaultView($defaultView, $options); + return true; + } + + /** + * Renders the HTML to PDF + */ + public function render() + { + $this->setPhpConfig(); + + $logOutputFile = $this->options->getLogOutputFile(); + if ($logOutputFile) { + if (!file_exists($logOutputFile) && is_writable(dirname($logOutputFile))) { + touch($logOutputFile); + } + + $startTime = microtime(true); + if (is_writable($logOutputFile)) { + ob_start(); + } + } + + $this->processHtml(); + + $this->css->apply_styles($this->tree); + + // @page style rules : size, margins + $pageStyles = $this->css->get_page_styles(); + $basePageStyle = $pageStyles["base"]; + unset($pageStyles["base"]); + + foreach ($pageStyles as $pageStyle) { + $pageStyle->inherit($basePageStyle); + } + + // Set paper size if defined via CSS + if (is_array($basePageStyle->size)) { + // Orientation is already applied when reading the computed CSS + // `size` value. The `Canvas` back ends, however, unconditionally + // swap with an orientation of `landscape` and leave the defined + // size as-is with `portrait`; so passing `portrait` as orientation + // here (via the default value) is correct + [$width, $height] = $basePageStyle->size; + $this->setPaper([0, 0, $width, $height]); + } + + // Create a new canvas instance if the current one does not match the + // desired paper size + $canvasWidth = $this->canvas->get_width(); + $canvasHeight = $this->canvas->get_height(); + $size = $this->getPaperSize(); + + if ( + \Dompdf\Helpers::lengthEqual($canvasWidth, $size[2]) === false || + \Dompdf\Helpers::lengthEqual($canvasHeight, $size[3]) === false + ) { + $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation); + $this->fontMetrics->setCanvas($this->canvas); + } + + $canvas = $this->canvas; + + $root_frame = $this->tree->get_root(); + $root = Factory::decorate_root($root_frame, $this); + foreach ($this->tree as $frame) { + if ($frame === $root_frame) { + continue; + } + Factory::decorate_frame($frame, $this, $root); + } + + // Add meta information + $title = $this->dom->getElementsByTagName("title"); + if ($title->length) { + $canvas->add_info("Title", trim($title->item(0)->nodeValue)); + } + + $metas = $this->dom->getElementsByTagName("meta"); + $labels = [ + "author" => "Author", + "keywords" => "Keywords", + "description" => "Subject", + ]; + /** @var \DOMElement $meta */ + foreach ($metas as $meta) { + $name = mb_strtolower($meta->getAttribute("name")); + $value = trim($meta->getAttribute("content")); + + if (isset($labels[$name])) { + $canvas->add_info($labels[$name], $value); + continue; + } + + if ($name === "dompdf.view" && $this->parseDefaultView($value)) { + $canvas->set_default_view($this->defaultView, $this->defaultViewOptions); + } + } + + $root->set_containing_block(0, 0, $canvas->get_width(), $canvas->get_height()); + $root->set_renderer(new Renderer($this)); + + // This is where the magic happens: + $root->reflow(); + + if (isset($this->callbacks["end_document"])) { + $fs = $this->callbacks["end_document"]; + + foreach ($fs as $f) { + $canvas->page_script($f); + } + } + + // Clean up cached images + if (!$this->options->getDebugKeepTemp()) { + Cache::clear($this->options->getDebugPng()); + } + + global $_dompdf_warnings, $_dompdf_show_warnings; + if ($_dompdf_show_warnings && isset($_dompdf_warnings)) { + echo 'Dompdf Warnings
';
+            foreach ($_dompdf_warnings as $msg) {
+                echo $msg . "\n";
+            }
+
+            if ($canvas instanceof CPDF) {
+                echo $canvas->get_cpdf()->messages;
+            }
+            echo '
'; + flush(); + } + + if ($logOutputFile && is_writable($logOutputFile)) { + $this->writeLog($logOutputFile, $startTime); + ob_end_clean(); + } + + $this->restorePhpConfig(); + } + + /** + * Writes the output buffer in the log file + * + * @param string $logOutputFile + * @param float $startTime + */ + private function writeLog(string $logOutputFile, float $startTime): void + { + $frames = Frame::$ID_COUNTER; + $memory = memory_get_peak_usage(true) / 1024; + $time = (microtime(true) - $startTime) * 1000; + + $out = sprintf( + "%6d" . + "%10.2f KB" . + "%10.2f ms" . + " " . + ($this->quirksmode ? " ON" : "OFF") . + "
", $frames, $memory, $time); + + $out .= ob_get_contents(); + ob_clean(); + + file_put_contents($logOutputFile, $out); + } + + /** + * Add meta information to the PDF after rendering. + * + * @deprecated + */ + public function add_info($label, $value) + { + $this->addInfo($label, $value); + } + + /** + * Add meta information to the PDF after rendering. + * + * @param string $label Label of the value (Creator, Producer, etc.) + * @param string $value The text to set + */ + public function addInfo(string $label, string $value): void + { + $this->canvas->add_info($label, $value); + } + + /** + * Streams the PDF to the client. + * + * The file will open a download dialog by default. The options + * parameter controls the output. Accepted options (array keys) are: + * + * 'compress' = > 1 (=default) or 0: + * Apply content stream compression + * + * 'Attachment' => 1 (=default) or 0: + * Set the 'Content-Disposition:' HTTP header to 'attachment' + * (thereby causing the browser to open a download dialog) + * + * @param string $filename the name of the streamed file + * @param array $options header options (see above) + */ + public function stream($filename = "document.pdf", $options = []) + { + $this->setPhpConfig(); + + $this->canvas->stream($filename, $options); + + $this->restorePhpConfig(); + } + + /** + * Returns the PDF as a string. + * + * The options parameter controls the output. Accepted options are: + * + * 'compress' = > 1 or 0 - apply content stream compression, this is + * on (1) by default + * + * @param array $options options (see above) + * + * @return string + */ + public function output($options = []) + { + $this->setPhpConfig(); + + $output = $this->canvas->output($options); + + $this->restorePhpConfig(); + + return $output; + } + + /** + * @return string + * @deprecated + */ + public function output_html() + { + return $this->outputHtml(); + } + + /** + * Returns the underlying HTML document as a string + * + * @return string + */ + public function outputHtml() + { + return $this->dom->saveHTML(); + } + + /** + * Get the dompdf option value + * + * @param string $key + * @return mixed + * @deprecated + */ + public function get_option($key) + { + return $this->options->get($key); + } + + /** + * @param string $key + * @param mixed $value + * @return $this + * @deprecated + */ + public function set_option($key, $value) + { + $new_options = clone $this->options; + $new_options->set($key, $value); + $this->setOptions($new_options); + return $this; + } + + /** + * @param array $options + * @return $this + * @deprecated + */ + public function set_options(array $options) + { + $new_options = clone $this->options; + $new_options->set($options); + $this->setOptions($new_options); + return $this; + } + + /** + * @param string $size + * @param string $orientation + * @deprecated + */ + public function set_paper($size, $orientation = "portrait") + { + $this->setPaper($size, $orientation); + } + + /** + * Sets the paper size & orientation + * + * @param string|float[] $size 'letter', 'legal', 'A4', etc. {@link Dompdf\Adapter\CPDF::$PAPER_SIZES} + * @param string $orientation 'portrait' or 'landscape' + * @return $this + */ + public function setPaper($size, string $orientation = "portrait"): self + { + $current_size = $this->getPaperSize(); + $this->paperSize = $size; + $this->paperOrientation = $orientation; + $new_size = $this->getPaperSize(); + if ( + \Dompdf\Helpers::lengthEqual($current_size[2], $new_size[2]) === false || + \Dompdf\Helpers::lengthEqual($current_size[3], $new_size[3]) === false + ) { + $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation); + } + return $this; + } + + /** + * Gets the paper size + * + * @return float[] A four-element float array + */ + public function getPaperSize(): array + { + $paper = $this->paperSize; + $orientation = $this->paperOrientation; + + 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]]; + } + + return $size; + } + + /** + * Gets the paper orientation + * + * @return string Either "portrait" or "landscape" + */ + public function getPaperOrientation(): string + { + return $this->paperOrientation; + } + + /** + * @param FrameTree $tree + * @return $this + */ + public function setTree(FrameTree $tree) + { + $this->tree = $tree; + return $this; + } + + /** + * @return FrameTree + * @deprecated + */ + public function get_tree() + { + return $this->getTree(); + } + + /** + * Returns the underlying {@link FrameTree} object + * + * @return FrameTree + */ + public function getTree() + { + return $this->tree; + } + + /** + * @param string $protocol + * @return $this + * @deprecated + */ + public function set_protocol($protocol) + { + return $this->setProtocol($protocol); + } + + /** + * Sets the protocol to use + * FIXME validate these + * + * @param string $protocol + * @return $this + */ + public function setProtocol(string $protocol) + { + $this->protocol = $protocol; + return $this; + } + + /** + * @return string + * @deprecated + */ + public function get_protocol() + { + return $this->getProtocol(); + } + + /** + * Returns the protocol in use + * + * @return string + */ + public function getProtocol() + { + return $this->protocol; + } + + /** + * @param string $host + * @deprecated + */ + public function set_host($host) + { + $this->setBaseHost($host); + } + + /** + * Sets the base hostname + * + * @param string $baseHost + * @return $this + */ + public function setBaseHost(string $baseHost) + { + $this->baseHost = $baseHost; + return $this; + } + + /** + * @return string + * @deprecated + */ + public function get_host() + { + return $this->getBaseHost(); + } + + /** + * Returns the base hostname + * + * @return string + */ + public function getBaseHost() + { + return $this->baseHost; + } + + /** + * Sets the base path + * + * @param string $path + * @deprecated + */ + public function set_base_path($path) + { + $this->setBasePath($path); + } + + /** + * Sets the base path + * + * @param string $basePath + * @return $this + */ + public function setBasePath(string $basePath) + { + $this->basePath = $basePath; + return $this; + } + + /** + * @return string + * @deprecated + */ + public function get_base_path() + { + return $this->getBasePath(); + } + + /** + * Returns the base path + * + * @return string + */ + public function getBasePath() + { + return $this->basePath; + } + + /** + * @param string $default_view The default document view + * @param array $options The view's options + * @return $this + * @deprecated + */ + public function set_default_view($default_view, $options) + { + return $this->setDefaultView($default_view, $options); + } + + /** + * Sets the default view + * + * @param string $defaultView The default document view + * @param array $options The view's options + * @return $this + */ + public function setDefaultView($defaultView, $options) + { + $this->defaultView = $defaultView; + $this->defaultViewOptions = $options; + return $this; + } + + /** + * @param resource $http_context + * @return $this + * @deprecated + */ + public function set_http_context($http_context) + { + return $this->setHttpContext($http_context); + } + + /** + * Sets the HTTP context + * + * @param resource|array $httpContext + * @return $this + */ + public function setHttpContext($httpContext) + { + $this->options->setHttpContext($httpContext); + return $this; + } + + /** + * @return resource + * @deprecated + */ + public function get_http_context() + { + return $this->getHttpContext(); + } + + /** + * Returns the HTTP context + * + * @return resource + */ + public function getHttpContext() + { + return $this->options->getHttpContext(); + } + + /** + * Set a custom `Canvas` instance to render the document to. + * + * Be aware that the instance will be replaced on render if the document + * defines a paper size different from the canvas. + * + * @param Canvas $canvas + * @return $this + */ + public function setCanvas(Canvas $canvas) + { + $this->canvas = $canvas; + $canvasWidth = $this->canvas->get_width(); + $canvasHeight = $this->canvas->get_height(); + $this->paperSize = [0, 0, $canvasWidth, $canvasHeight]; + $this->paperOrientation = "portrait"; + return $this; + } + + /** + * @return Canvas + * @deprecated + */ + public function get_canvas() + { + return $this->getCanvas(); + } + + /** + * Return the underlying Canvas instance (e.g. Dompdf\Adapter\CPDF, Dompdf\Adapter\GD) + * + * @return Canvas + */ + public function getCanvas() + { + return $this->canvas; + } + + /** + * @param Stylesheet $css + * @return $this + */ + public function setCss(Stylesheet $css) + { + $this->css = $css; + return $this; + } + + /** + * @return Stylesheet + * @deprecated + */ + public function get_css() + { + return $this->getCss(); + } + + /** + * Returns the stylesheet + * + * @return Stylesheet + */ + public function getCss() + { + return $this->css; + } + + /** + * @param DOMDocument $dom + * @return $this + */ + public function setDom(DOMDocument $dom) + { + $this->dom = $dom; + return $this; + } + + /** + * @return DOMDocument + * @deprecated + */ + public function get_dom() + { + return $this->getDom(); + } + + /** + * @return DOMDocument + */ + public function getDom() + { + return $this->dom; + } + + /** + * @param Options $options + * @return $this + */ + public function setOptions(Options $options) + { + // For backwards compatibility + if ($this->options && $this->options->getHttpContext() && !$options->getHttpContext()) { + $options->setHttpContext($this->options->getHttpContext()); + } + + $this->options = $options; + + $fontMetrics = $this->fontMetrics; + if (isset($fontMetrics)) { + $fontMetrics->setOptions($options); + } + + if (isset($this->canvas)) { + $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation); + if (isset($fontMetrics)) { + $this->fontMetrics = new FontMetrics($this->canvas, $this->options); + } + } + + return $this; + } + + /** + * @return Options + */ + public function getOptions() + { + return $this->options; + } + + /** + * @return array + * @deprecated + */ + public function get_callbacks() + { + return $this->getCallbacks(); + } + + /** + * Returns the callbacks array + * + * @return array + */ + public function getCallbacks() + { + return $this->callbacks; + } + + /** + * @param array $callbacks the set of callbacks to set + * @return $this + * @deprecated + */ + public function set_callbacks($callbacks) + { + return $this->setCallbacks($callbacks); + } + + /** + * Define callbacks that allow modifying the document during render. + * + * The callbacks array should contain arrays with `event` set to a callback + * event name and `f` set to a function or any other callable. + * + * The available callback events are: + * * `begin_page_reflow`: called before page reflow + * * `begin_frame`: called before a frame is rendered + * * `end_frame`: called after frame rendering is complete + * * `begin_page_render`: called before a page is rendered + * * `end_page_render`: called after page rendering is complete + * * `end_document`: called for every page after rendering is complete + * + * The function `f` receives three arguments `Frame $frame`, `Canvas $canvas`, + * and `FontMetrics $fontMetrics` for all events but `end_document`. For + * `end_document`, the function receives four arguments `int $pageNumber`, + * `int $pageCount`, `Canvas $canvas`, and `FontMetrics $fontMetrics` instead. + * + * @param array $callbacks The set of callbacks to set. + * @return $this + */ + public function setCallbacks(array $callbacks): self + { + $this->callbacks = []; + + foreach ($callbacks as $c) { + if (is_array($c) && isset($c["event"]) && isset($c["f"])) { + $event = $c["event"]; + $f = $c["f"]; + if (is_string($event) && is_callable($f)) { + $this->callbacks[$event][] = $f; + } + } + } + + return $this; + } + + /** + * @return boolean + * @deprecated + */ + public function get_quirksmode() + { + return $this->getQuirksmode(); + } + + /** + * Get the quirks mode + * + * @return boolean true if quirks mode is active + */ + public function getQuirksmode() + { + return $this->quirksmode; + } + + /** + * @param FontMetrics $fontMetrics + * @return $this + */ + public function setFontMetrics(FontMetrics $fontMetrics) + { + $this->fontMetrics = $fontMetrics; + return $this; + } + + /** + * @return FontMetrics + */ + public function getFontMetrics() + { + return $this->fontMetrics; + } + + /** + * PHP5 overloaded getter + * Along with {@link Dompdf::__set()} __get() provides access to all + * properties directly. Typically __get() is not called directly outside + * of this class. + * + * @param string $prop + * + * @throws Exception + * @return mixed + */ + function __get($prop) + { + switch ($prop) { + case 'version': + return $this->version; + default: + throw new Exception('Invalid property: ' . $prop); + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Exception.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Exception.php new file mode 100644 index 0000000..3a90e47 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Exception.php @@ -0,0 +1,27 @@ +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(); + + $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"], "UTF-8"); + $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; + } + } + + [$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; + } + + /** + * Maps substrings of text against the provided font list. This is achieved by + * parsing each character of the string against the supported glyphs for each + * font. Fonts preference is based on the order of the font list. + * + * Returns an array containing substring information that indicates the + * matched font (if any), start index, substring length, and (optionally) + * the actual text of the substring. + * + * @param string $text The text to map + * @param array $fontFamilies List of font families to map against + * @param string $subtype The font subtype (italic, bold, etc.) + * @param int $count The number of matches to return + * @param bool $returnSubstring Should the actual matched text be returned + * @return array + */ + public function mapTextToFonts(string $text, array $fontFamilies, string $subtype = "normal", int $count = -1, bool $returnSubstring = false): array + { + $char_mapping = []; + $fonts = []; + + foreach ($fontFamilies as $family) { + $font = $this->getFont($family, $subtype); + if ($font !== null) { + $fonts[] = $font; + } + } + + if (function_exists("mb_str_split")) { + $char_array = mb_str_split($text, 1, "UTF-8"); + } else { + $char_array = preg_split("//u", $text, -1, PREG_SPLIT_NO_EMPTY); + } + $start_index = 0; + $char_index = -1; + while (isset($char_array[++$char_index])) { + $char = $char_array[$char_index]; + if (preg_match('/[\x00-\x1F\x7F]/u', $char)) { + //non-printable, moving on + continue; + } + $mapped_font = null; + foreach ($fonts as $font) { + if ($this->canvas->font_supports_char($font, $char)) { + $mapped_font = $font; + break; + } + } + + if (!isset($char_mapping[$start_index])) { + $char_mapping[$start_index] = ["font" => $mapped_font, "length" => 0, "text" => null]; + } + + if ($mapped_font !== $char_mapping[$start_index]["font"]) { + $char_mapping[$start_index]["length"] = $char_index - $start_index; + if ($count > 0 && count($char_mapping) === $count) { + break; + } + $start_index = $char_index; + $char_mapping[$start_index] = ["font" => $mapped_font, "length" => 0, "text" => null]; + } + } + + if ($returnSubstring) { + // build the string for each mapping + foreach ($char_mapping as $start_index => &$info) { + $info["text"] = mb_substr($text, $start_index, $info["length"], "UTF-8"); + } + } + + return $char_mapping; + } + + /** + * @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 (!$familyRaw) { + $familyRaw = $familyRaw === null ? 0 : $this->options->getDefaultFont(); + } + if (!$subtypeRaw) { + $subtypeRaw = "normal"; + } + + 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, "UTF-8")); + $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, "UTF-8")] = $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; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame.php new file mode 100644 index 0000000..678fb65 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame.php @@ -0,0 +1,1238 @@ +_node = $node; + + $this->_parent = null; + $this->_first_child = null; + $this->_last_child = null; + $this->_prev_sibling = $this->_next_sibling = null; + + $this->_style = null; + + $this->_containing_block = [ + "x" => null, + "y" => null, + "w" => null, + "h" => null, + ]; + + $this->_containing_block[0] =& $this->_containing_block["x"]; + $this->_containing_block[1] =& $this->_containing_block["y"]; + $this->_containing_block[2] =& $this->_containing_block["w"]; + $this->_containing_block[3] =& $this->_containing_block["h"]; + + $this->_position = [ + "x" => null, + "y" => null, + ]; + + $this->_position[0] =& $this->_position["x"]; + $this->_position[1] =& $this->_position["y"]; + + $this->_opacity = 1.0; + $this->_decorator = null; + + $this->set_id(self::$ID_COUNTER++); + } + + /** + * WIP : preprocessing to remove all the unused whitespace + */ + protected function ws_trim() + { + if ($this->ws_keep()) { + return; + } + + if (self::$_ws_state === self::WS_SPACE) { + $node = $this->_node; + + if ($node->nodeName === "#text" && !empty($node->nodeValue)) { + $node->nodeValue = preg_replace("/[ \t\r\n\f]+/u", " ", trim($node->nodeValue)); + self::$_ws_state = self::WS_TEXT; + } + } + } + + /** + * @return bool + */ + protected function ws_keep() + { + $whitespace = $this->get_style()->white_space; + + return in_array($whitespace, ["pre", "pre-wrap", "pre-line"]); + } + + /** + * @return bool + */ + protected function ws_is_text() + { + $node = $this->get_node(); + + if ($node->nodeName === "img") { + return true; + } + + if (!$this->is_in_flow()) { + return false; + } + + if ($this->is_text_node()) { + return trim($node->nodeValue) !== ""; + } + + return true; + } + + /** + * "Destructor": forcibly free all references held by this frame + * + * @param bool $recursive if true, call dispose on all children + */ + public function dispose($recursive = false) + { + if ($recursive) { + while ($child = $this->_first_child) { + $child->dispose(true); + } + } + + // Remove this frame from the tree + if ($this->_prev_sibling) { + $this->_prev_sibling->_next_sibling = $this->_next_sibling; + } + + if ($this->_next_sibling) { + $this->_next_sibling->_prev_sibling = $this->_prev_sibling; + } + + if ($this->_parent && $this->_parent->_first_child === $this) { + $this->_parent->_first_child = $this->_next_sibling; + } + + if ($this->_parent && $this->_parent->_last_child === $this) { + $this->_parent->_last_child = $this->_prev_sibling; + } + + if ($this->_parent) { + $this->_parent->get_node()->removeChild($this->_node); + } + + $this->_style = null; + unset($this->_style); + } + + /** + * Re-initialize the frame + */ + public function reset() + { + $this->_position["x"] = null; + $this->_position["y"] = null; + + $this->_containing_block["x"] = null; + $this->_containing_block["y"] = null; + $this->_containing_block["w"] = null; + $this->_containing_block["h"] = null; + + $this->_style->reset(); + } + + /** + * @return \DOMElement|\DOMText + */ + public function get_node() + { + return $this->_node; + } + + /** + * @return int + */ + public function get_id() + { + return $this->_id; + } + + /** + * @return Style + */ + public function get_style() + { + return $this->_style; + } + + /** + * @deprecated + * @return Style + */ + public function get_original_style() + { + return $this->_style; + } + + /** + * @return Frame + */ + public function get_parent() + { + return $this->_parent; + } + + /** + * @return FrameDecorator\AbstractFrameDecorator + */ + public function get_decorator() + { + return $this->_decorator; + } + + /** + * @return Frame + */ + public function get_first_child() + { + return $this->_first_child; + } + + /** + * @return Frame + */ + public function get_last_child() + { + return $this->_last_child; + } + + /** + * @return Frame + */ + public function get_prev_sibling() + { + return $this->_prev_sibling; + } + + /** + * @return Frame + */ + public function get_next_sibling() + { + return $this->_next_sibling; + } + + /** + * @return FrameListIterator + */ + public function get_children(): FrameListIterator + { + return new FrameListIterator($this); + } + + // Layout property accessors + + /** + * Containing block dimensions + * + * @param string|null $i The key of the wanted containing block's dimension (x, y, w, h) + * + * @return float[]|float + */ + public function get_containing_block($i = null) + { + if (isset($i)) { + return $this->_containing_block[$i]; + } + + return $this->_containing_block; + } + + /** + * Block position + * + * @param string|null $i The key of the wanted position value (x, y) + * + * @return float[]|float + */ + public function get_position($i = null) + { + if (isset($i)) { + return $this->_position[$i]; + } + + return $this->_position; + } + + //........................................................................ + + /** + * Return the width of the margin box of the frame, in pt. Meaningless + * unless the width has been calculated properly. + * + * @return float + */ + public function get_margin_width(): float + { + $style = $this->_style; + + return (float)$style->length_in_pt([ + $style->width, + $style->margin_left, + $style->margin_right, + $style->border_left_width, + $style->border_right_width, + $style->padding_left, + $style->padding_right + ], $this->_containing_block["w"]); + } + + /** + * Return the height of the margin box of the frame, in pt. Meaningless + * unless the height has been calculated properly. + * + * @return float + */ + public function get_margin_height(): float + { + $style = $this->_style; + + return (float)$style->length_in_pt( + [ + $style->height, + (float)$style->length_in_pt( + [ + $style->border_top_width, + $style->border_bottom_width, + $style->margin_top, + $style->margin_bottom, + $style->padding_top, + $style->padding_bottom + ], $this->_containing_block["w"] + ) + ], + $this->_containing_block["h"] + ); + } + + /** + * Return the content box (x,y,w,h) of the frame. + * + * Width and height might be reported as 0 if they have not been resolved + * yet. + * + * @return float[] + */ + public function get_content_box(): array + { + $style = $this->_style; + $cb = $this->_containing_block; + + $x = $this->_position["x"] + + (float)$style->length_in_pt( + [ + $style->margin_left, + $style->border_left_width, + $style->padding_left + ], + $cb["w"] + ); + + $y = $this->_position["y"] + + (float)$style->length_in_pt( + [ + $style->margin_top, + $style->border_top_width, + $style->padding_top + ], $cb["w"] + ); + + $w = (float)$style->length_in_pt($style->width, $cb["w"]); + + $h = (float)$style->length_in_pt($style->height, $cb["h"]); + + return [0 => $x, "x" => $x, + 1 => $y, "y" => $y, + 2 => $w, "w" => $w, + 3 => $h, "h" => $h]; + } + + /** + * Return the padding box (x,y,w,h) of the frame. + * + * Width and height might be reported as 0 if they have not been resolved + * yet. + * + * @return float[] + */ + public function get_padding_box(): array + { + $style = $this->_style; + $cb = $this->_containing_block; + + $x = $this->_position["x"] + + (float)$style->length_in_pt( + [ + $style->margin_left, + $style->border_left_width + ], + $cb["w"] + ); + + $y = $this->_position["y"] + + (float)$style->length_in_pt( + [ + $style->margin_top, + $style->border_top_width + ], + $cb["h"] + ); + + $w = (float)$style->length_in_pt( + [ + $style->padding_left, + $style->width, + $style->padding_right + ], + $cb["w"] + ); + + $h = (float)$style->length_in_pt( + [ + $style->padding_top, + $style->padding_bottom, + $style->length_in_pt($style->height, $cb["h"]) + ], + $cb["w"] + ); + + return [0 => $x, "x" => $x, + 1 => $y, "y" => $y, + 2 => $w, "w" => $w, + 3 => $h, "h" => $h]; + } + + /** + * Return the border box of the frame. + * + * Width and height might be reported as 0 if they have not been resolved + * yet. + * + * @return float[] + */ + public function get_border_box(): array + { + $style = $this->_style; + $cb = $this->_containing_block; + + $x = $this->_position["x"] + (float)$style->length_in_pt($style->margin_left, $cb["w"]); + + $y = $this->_position["y"] + (float)$style->length_in_pt($style->margin_top, $cb["w"]); + + $w = (float)$style->length_in_pt( + [ + $style->border_left_width, + $style->padding_left, + $style->width, + $style->padding_right, + $style->border_right_width + ], + $cb["w"] + ); + + $h = (float)$style->length_in_pt( + [ + $style->border_top_width, + $style->padding_top, + $style->padding_bottom, + $style->border_bottom_width, + $style->length_in_pt($style->height, $cb["h"]) + ], + $cb["w"] + ); + + return [0 => $x, "x" => $x, + 1 => $y, "y" => $y, + 2 => $w, "w" => $w, + 3 => $h, "h" => $h]; + } + + /** + * @param float|null $opacity + * + * @return float + */ + public function get_opacity(?float $opacity = null): float + { + if ($opacity !== null) { + $this->set_opacity($opacity); + } + + return $this->_opacity; + } + + /** + * @return LineBox|null + */ + public function &get_containing_line() + { + return $this->_containing_line; + } + + //........................................................................ + // Set methods + + /** + * @param int $id + */ + public function set_id($id) + { + $this->_id = $id; + + // We can only set attributes of DOMElement objects (nodeType == 1). + // Since these are the only objects that we can assign CSS rules to, + // this shortcoming is okay. + if ($this->_node->nodeType == XML_ELEMENT_NODE) { + $this->_node->setAttribute("frame_id", $id); + } + } + + /** + * @param Style $style + */ + public function set_style(Style $style): void + { + // $style->set_frame($this); + $this->_style = $style; + } + + /** + * @param FrameDecorator\AbstractFrameDecorator $decorator + */ + public function set_decorator(FrameDecorator\AbstractFrameDecorator $decorator) + { + $this->_decorator = $decorator; + } + + /** + * @param float|float[]|null $x + * @param float|null $y + * @param float|null $w + * @param float|null $h + */ + public function set_containing_block($x = null, $y = null, $w = null, $h = null) + { + if (is_array($x)) { + foreach ($x as $key => $val) { + $$key = $val; + } + } + + if (is_numeric($x)) { + $this->_containing_block["x"] = $x; + } + + if (is_numeric($y)) { + $this->_containing_block["y"] = $y; + } + + if (is_numeric($w)) { + $this->_containing_block["w"] = $w; + } + + if (is_numeric($h)) { + $this->_containing_block["h"] = $h; + } + } + + /** + * @param float|float[]|null $x + * @param float|null $y + */ + public function set_position($x = null, $y = null) + { + if (is_array($x)) { + list($x, $y) = [$x["x"], $x["y"]]; + } + + if (is_numeric($x)) { + $this->_position["x"] = $x; + } + + if (is_numeric($y)) { + $this->_position["y"] = $y; + } + } + + /** + * @param float $opacity + */ + public function set_opacity(float $opacity): void + { + $parent = $this->get_parent(); + $base_opacity = $parent && $parent->_opacity !== null ? $parent->_opacity : 1.0; + $this->_opacity = $base_opacity * $opacity; + } + + /** + * @param LineBox $line + */ + public function set_containing_line(LineBox $line) + { + $this->_containing_line = $line; + } + + /** + * Indicates if the margin height is auto sized + * + * @return bool + */ + public function is_auto_height() + { + $style = $this->_style; + + return in_array( + "auto", + [ + $style->height, + $style->margin_top, + $style->margin_bottom, + $style->border_top_width, + $style->border_bottom_width, + $style->padding_top, + $style->padding_bottom, + $this->_containing_block["h"] + ], + true + ); + } + + /** + * Indicates if the margin width is auto sized + * + * @return bool + */ + public function is_auto_width() + { + $style = $this->_style; + + return in_array( + "auto", + [ + $style->width, + $style->margin_left, + $style->margin_right, + $style->border_left_width, + $style->border_right_width, + $style->padding_left, + $style->padding_right, + $this->_containing_block["w"] + ], + true + ); + } + + /** + * Tells if the frame is a text node + * + * @return bool + */ + public function is_text_node(): bool + { + if (isset($this->_is_cache["text_node"])) { + return $this->_is_cache["text_node"]; + } + + return $this->_is_cache["text_node"] = ($this->get_node()->nodeName === "#text"); + } + + /** + * @return bool + */ + public function is_positioned(): bool + { + if (isset($this->_is_cache["positioned"])) { + return $this->_is_cache["positioned"]; + } + + $position = $this->get_style()->position; + + return $this->_is_cache["positioned"] = in_array($position, Style::POSITIONED_TYPES, true); + } + + /** + * @return bool + */ + public function is_absolute(): bool + { + if (isset($this->_is_cache["absolute"])) { + return $this->_is_cache["absolute"]; + } + + return $this->_is_cache["absolute"] = $this->get_style()->is_absolute(); + } + + /** + * Whether the frame is a block container. + * + * @return bool + */ + public function is_block(): bool + { + if (isset($this->_is_cache["block"])) { + return $this->_is_cache["block"]; + } + + return $this->_is_cache["block"] = in_array($this->get_style()->display, Style::BLOCK_TYPES, true); + } + + /** + * Whether the frame has a block-level display type. + * + * @return bool + */ + public function is_block_level(): bool + { + if (isset($this->_is_cache["block_level"])) { + return $this->_is_cache["block_level"]; + } + + $display = $this->get_style()->display; + + return $this->_is_cache["block_level"] = in_array($display, Style::BLOCK_LEVEL_TYPES, true); + } + + /** + * Whether the frame has an inline-level display type. + * + * @return bool + */ + public function is_inline_level(): bool + { + if (isset($this->_is_cache["inline_level"])) { + return $this->_is_cache["inline_level"]; + } + + $display = $this->get_style()->display; + + return $this->_is_cache["inline_level"] = in_array($display, Style::INLINE_LEVEL_TYPES, true); + } + + /** + * @return bool + */ + public function is_in_flow(): bool + { + if (isset($this->_is_cache["in_flow"])) { + return $this->_is_cache["in_flow"]; + } + + return $this->_is_cache["in_flow"] = $this->get_style()->is_in_flow(); + } + + /** + * @return bool + */ + public function is_pre(): bool + { + if (isset($this->_is_cache["pre"])) { + return $this->_is_cache["pre"]; + } + + $white_space = $this->get_style()->white_space; + + return $this->_is_cache["pre"] = in_array($white_space, ["pre", "pre-wrap"], true); + } + + /** + * @return bool + */ + public function is_table(): bool + { + if (isset($this->_is_cache["table"])) { + return $this->_is_cache["table"]; + } + + $display = $this->get_style()->display; + + return $this->_is_cache["table"] = in_array($display, Style::TABLE_TYPES, true); + } + + + /** + * Inserts a new child at the beginning of the Frame + * + * @param Frame $child The new Frame to insert + * @param bool $update_node Whether or not to update the DOM + */ + public function prepend_child(Frame $child, $update_node = true) + { + if ($update_node) { + $this->_node->insertBefore($child->_node, $this->_first_child ? $this->_first_child->_node : null); + } + + // Remove the child from its parent + if ($child->_parent) { + $child->_parent->remove_child($child, false); + } + + $child->_parent = $this; + $decorator = $child->get_decorator(); + // force an update to the cached parent + if ($decorator !== null) { + $decorator->get_parent(false); + } + $child->_prev_sibling = null; + + // Handle the first child + if (!$this->_first_child) { + $this->_first_child = $child; + $this->_last_child = $child; + $child->_next_sibling = null; + } else { + $this->_first_child->_prev_sibling = $child; + $child->_next_sibling = $this->_first_child; + $this->_first_child = $child; + } + } + + /** + * Inserts a new child at the end of the Frame + * + * @param Frame $child The new Frame to insert + * @param bool $update_node Whether or not to update the DOM + */ + public function append_child(Frame $child, $update_node = true) + { + if ($update_node) { + $this->_node->appendChild($child->_node); + } + + // Remove the child from its parent + if ($child->_parent) { + $child->_parent->remove_child($child, false); + } + + $child->_parent = $this; + $decorator = $child->get_decorator(); + // force an update to the cached parent + if ($decorator !== null) { + $decorator->get_parent(false); + } + $child->_next_sibling = null; + + // Handle the first child + if (!$this->_last_child) { + $this->_first_child = $child; + $this->_last_child = $child; + $child->_prev_sibling = null; + } else { + $this->_last_child->_next_sibling = $child; + $child->_prev_sibling = $this->_last_child; + $this->_last_child = $child; + } + } + + /** + * Inserts a new child immediately before the specified frame + * + * @param Frame $new_child The new Frame to insert + * @param Frame $ref The Frame after the new Frame + * @param bool $update_node Whether or not to update the DOM + * + * @throws Exception + */ + public function insert_child_before(Frame $new_child, Frame $ref, $update_node = true) + { + if ($ref === $this->_first_child) { + $this->prepend_child($new_child, $update_node); + + return; + } + + if (is_null($ref)) { + $this->append_child($new_child, $update_node); + + return; + } + + if ($ref->_parent !== $this) { + throw new Exception("Reference child is not a child of this node."); + } + + // Update the node + if ($update_node) { + $this->_node->insertBefore($new_child->_node, $ref->_node); + } + + // Remove the child from its parent + if ($new_child->_parent) { + $new_child->_parent->remove_child($new_child, false); + } + + $new_child->_parent = $this; + $decorator = $new_child->get_decorator(); + // force an update to the cached parent + if ($decorator !== null) { + $decorator->get_parent(false); + } + $new_child->_next_sibling = $ref; + $new_child->_prev_sibling = $ref->_prev_sibling; + + if ($ref->_prev_sibling) { + $ref->_prev_sibling->_next_sibling = $new_child; + } + + $ref->_prev_sibling = $new_child; + } + + /** + * Inserts a new child immediately after the specified frame + * + * @param Frame $new_child The new Frame to insert + * @param Frame $ref The Frame before the new Frame + * @param bool $update_node Whether or not to update the DOM + * + * @throws Exception + */ + public function insert_child_after(Frame $new_child, Frame $ref, $update_node = true) + { + if ($ref === $this->_last_child) { + $this->append_child($new_child, $update_node); + + return; + } + + if (is_null($ref)) { + $this->prepend_child($new_child, $update_node); + + return; + } + + if ($ref->_parent !== $this) { + throw new Exception("Reference child is not a child of this node."); + } + + // Update the node + if ($update_node) { + if ($ref->_next_sibling) { + $next_node = $ref->_next_sibling->_node; + $this->_node->insertBefore($new_child->_node, $next_node); + } else { + $new_child->_node = $this->_node->appendChild($new_child->_node); + } + } + + // Remove the child from its parent + if ($new_child->_parent) { + $new_child->_parent->remove_child($new_child, false); + } + + $new_child->_parent = $this; + $decorator = $new_child->get_decorator(); + // force an update to the cached parent + if ($decorator !== null) { + $decorator->get_parent(false); + } + $new_child->_prev_sibling = $ref; + $new_child->_next_sibling = $ref->_next_sibling; + + if ($ref->_next_sibling) { + $ref->_next_sibling->_prev_sibling = $new_child; + } + + $ref->_next_sibling = $new_child; + } + + /** + * Remove a child frame + * + * @param Frame $child + * @param bool $update_node Whether or not to remove the DOM node + * + * @throws Exception + * @return Frame The removed child frame + */ + public function remove_child(Frame $child, $update_node = true) + { + if ($child->_parent !== $this) { + throw new Exception("Child not found in this frame"); + } + + if ($update_node) { + $this->_node->removeChild($child->_node); + } + + if ($child === $this->_first_child) { + $this->_first_child = $child->_next_sibling; + } + + if ($child === $this->_last_child) { + $this->_last_child = $child->_prev_sibling; + } + + if ($child->_prev_sibling) { + $child->_prev_sibling->_next_sibling = $child->_next_sibling; + } + + if ($child->_next_sibling) { + $child->_next_sibling->_prev_sibling = $child->_prev_sibling; + } + + $child->_next_sibling = null; + $child->_prev_sibling = null; + $child->_parent = null; + + // Force an update to the cached decorator parent + $decorator = $child->get_decorator(); + if ($decorator !== null) { + $decorator->get_parent(false); + } + + return $child; + } + + //........................................................................ + + // Debugging function: + /** + * @return string + */ + public function __toString() + { + // Skip empty text frames +// if ( $this->is_text_node() && +// preg_replace("/\s/", "", $this->_node->data) === "" ) +// return ""; + + + $str = "" . $this->_node->nodeName . ":
"; + //$str .= spl_object_hash($this->_node) . "
"; + $str .= "Id: " . $this->get_id() . "
"; + $str .= "Class: " . get_class($this) . "
"; + + if ($this->is_text_node()) { + $tmp = htmlspecialchars($this->_node->nodeValue); + $str .= "
'" . mb_substr($tmp, 0, 70, "UTF-8") .
+                (mb_strlen($tmp, "UTF-8") > 70 ? "..." : "") . "'
"; + } elseif ($css_class = $this->_node->getAttribute("class")) { + $str .= "CSS class: '$css_class'
"; + } + + if ($this->_parent) { + $str .= "\nParent:" . $this->_parent->_node->nodeName . + " (" . spl_object_hash($this->_parent->_node) . ") " . + "
"; + } + + if ($this->_prev_sibling) { + $str .= "Prev: " . $this->_prev_sibling->_node->nodeName . + " (" . spl_object_hash($this->_prev_sibling->_node) . ") " . + "
"; + } + + if ($this->_next_sibling) { + $str .= "Next: " . $this->_next_sibling->_node->nodeName . + " (" . spl_object_hash($this->_next_sibling->_node) . ") " . + "
"; + } + + $d = $this->get_decorator(); + while ($d && $d != $d->get_decorator()) { + $str .= "Decorator: " . get_class($d) . "
"; + $d = $d->get_decorator(); + } + + $str .= "Position: " . Helpers::pre_r($this->_position, true); + $str .= "\nContaining block: " . Helpers::pre_r($this->_containing_block, true); + $str .= "\nMargin width: " . Helpers::pre_r($this->get_margin_width(), true); + $str .= "\nMargin height: " . Helpers::pre_r($this->get_margin_height(), true); + + $str .= "\nStyle:
" . $this->_style->__toString() . "
"; + + if ($this->_decorator instanceof FrameDecorator\Block) { + $str .= "Lines:
";
+            foreach ($this->_decorator->get_line_boxes() as $line) {
+                foreach ($line->get_frames() as $frame) {
+                    if ($frame instanceof FrameDecorator\Text) {
+                        $str .= "\ntext: ";
+                        $str .= "'" . htmlspecialchars($frame->get_text()) . "'";
+                    } else {
+                        $str .= "\nBlock: " . $frame->get_node()->nodeName . " (" . spl_object_hash($frame->get_node()) . ")";
+                    }
+                }
+
+                $str .=
+                    "\ny => " . $line->y . "\n" .
+                    "w => " . $line->w . "\n" .
+                    "h => " . $line->h . "\n" .
+                    "left => " . $line->left . "\n" .
+                    "right => " . $line->right . "\n";
+            }
+            $str .= "
"; + } + + $str .= "\n"; + if (php_sapi_name() === "cli") { + $str = strip_tags(str_replace(["
", "", ""], + ["\n", "", ""], + $str)); + } + + return $str; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/Factory.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/Factory.php new file mode 100644 index 0000000..be39ff3 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/Factory.php @@ -0,0 +1,262 @@ +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]; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/FrameListIterator.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/FrameListIterator.php new file mode 100644 index 0000000..0157550 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/FrameListIterator.php @@ -0,0 +1,100 @@ +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(); + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/FrameTree.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/FrameTree.php new file mode 100644 index 0000000..6d012d8 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/FrameTree.php @@ -0,0 +1,324 @@ +_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; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/FrameTreeIterator.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/FrameTreeIterator.php new file mode 100644 index 0000000..4da8da1 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Frame/FrameTreeIterator.php @@ -0,0 +1,88 @@ +_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; + } + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/AbstractFrameDecorator.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/AbstractFrameDecorator.php new file mode 100644 index 0000000..4ab7e7f --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/AbstractFrameDecorator.php @@ -0,0 +1,925 @@ + 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"); + } + + $deco = Factory::decorate_frame($frame, $this->_dompdf, $this->_root); + + if ($this instanceof Text) { + $deco->trailingWs = $this->trailingWs; + } + + return $deco; + } + + /** + * 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); + + if ($this instanceof Text) { + $deco->trailingWs = $this->trailingWs; + } + + 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 + */ + public function get_children(): FrameListIterator + { + return new FrameListIterator($this); + } + + /** + * @return FrameTreeIterator + */ + 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, true); + $counter_frame->_counters[$id] += $increment; + } + + /** + * @param string $id + * @param bool $auto_reset Instantiate a new counter if none with the given name is in scope. + * + * @return AbstractFrameDecorator|null + */ + public function lookup_counter_frame( + string $id = self::DEFAULT_COUNTER, + bool $auto_reset = false + ): ?AbstractFrameDecorator { + $f = $this->get_parent(); + + while ($f) { + if (isset($f->_counters[$id])) { + return $f; + } + $f = $f->get_parent(); + } + + if ($auto_reset) { + $f = $this->get_parent(); + $f->_counters[$id] = 0; + return $f; + } + + return null; + } + + /** + * @param string $id + * @param string $type + * + * @return string + * + * TODO: What version is the best : this one or the one in ListBullet ? + */ + public function counter_value(string $id = self::DEFAULT_COUNTER, string $type = "decimal"): string + { + $value = $this->_counters[$id] ?? 0; + + 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 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(); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Block.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Block.php new file mode 100644 index 0000000..dd95209 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Block.php @@ -0,0 +1,256 @@ +_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 = []; + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Image.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Image.php new file mode 100644 index 0000000..bbfb130 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Image.php @@ -0,0 +1,120 @@ +get_node(); + $url = $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 = $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 = $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; + } + +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Inline.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Inline.php new file mode 100644 index 0000000..668d795 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Inline.php @@ -0,0 +1,121 @@ +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); + } + } + +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/ListBullet.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/ListBullet.php new file mode 100644 index 0000000..703f467 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/ListBullet.php @@ -0,0 +1,117 @@ +_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; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/ListBulletImage.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/ListBulletImage.php new file mode 100644 index 0000000..df6c105 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/ListBulletImage.php @@ -0,0 +1,111 @@ +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(); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/NullFrameDecorator.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/NullFrameDecorator.php new file mode 100644 index 0000000..f083816 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/NullFrameDecorator.php @@ -0,0 +1,33 @@ +_frame->get_style(); + $style->width = 0; + $style->height = 0; + $style->margin = 0; + $style->padding = 0; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Page.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Page.php new file mode 100644 index 0000000..374cc97 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Page.php @@ -0,0 +1,767 @@ +_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; + } + + // If the frame is fixed-position or has a fixed-position parent + // ignore the forced page break + if ($frame->get_style()->is_absolute()) { + return false; + } + $p = $frame; + while ($p = $p->get_parent()) { + if ($p->get_style()->position === "fixed") { + 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") && !$prev->get_style()->is_absolute()) { + 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); + if ($table === null) { + throw new Exception("Parent table not found for table row"); + } + + $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 + */ + 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; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Table.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Table.php new file mode 100644 index 0000000..5ba8dda --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Table.php @@ -0,0 +1,344 @@ +_cellmap = new Cellmap($this); + + $style = $frame->get_style(); + if ($style->table_layout === "fixed" && $style->width !== "auto") { + $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); + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/TableCell.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/TableCell.php new file mode 100644 index 0000000..7d06b55 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/TableCell.php @@ -0,0 +1,111 @@ +content_height = 0.0; + } + + function reset() + { + parent::reset(); + $this->content_height = 0.0; + } + + /** + * @return float + */ + public function get_content_height(): float + { + return $this->content_height; + } + + /** + * @param float $height + */ + public function set_content_height(float $height): void + { + $this->content_height = $height; + } + + /** + * @param float $height + */ + public function set_cell_height(float $height): void + { + $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); + } + } + } + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/TableRow.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/TableRow.php new file mode 100644 index 0000000..ba985c9 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/TableRow.php @@ -0,0 +1,28 @@ +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); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Text.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Text.php new file mode 100644 index 0000000..894be3f --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Text.php @@ -0,0 +1,279 @@ +is_text_node()) { + throw new Exception("Text_Decorator can only be applied to #text nodes."); + } + + parent::__construct($frame, $dompdf); + $this->text_spacing = 0.0; + } + + /** + * Trim trailing white space from the frame text. + */ + public function trim_trailing_ws(): void + { + $frame = $this->_frame; + $text = $this->get_text(); + $trailing = mb_substr($text, -1, null, "UTF-8"); + + // White space is always collapsed to the standard space character + // currently, so only handle that for now + if ($trailing === " ") { + $this->trailingWs = $trailing; + $this->set_text(mb_substr($text, 0, -1, "UTF-8")); + $this->recalculate_width(); + } + } + + function reset() + { + parent::reset(); + $this->text_spacing = 0.0; + $this->mapped_font = null; + + // 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->get_text(); + $this->set_text($text . $this->trailingWs); + $this->trailingWs = null; + } + } + + // 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 + * @param bool $split_parent Whether to split parent inline frames. + * + * @return Text|null + */ + function split_text(int $offset, bool $split_parent = true): ?self + { + if ($offset === 0) { + return null; + } + + $split = $this->_frame->get_node()->splitText($offset); + if ($split === false) { + return null; + } + + /** @var Text */ + $deco = $this->copy($split); + $style = $this->_frame->get_style(); + $split_style = $deco->get_style(); + + if ($this->mapped_font !== null) { + $split_style->set_used("font_family", $this->mapped_font); + $deco->mapped_font = $this->mapped_font; + } + + // Clear decoration widths at the split point. They might have been + // copied from the parent frame during inline reflow + $style->margin_right = 0.0; + $style->padding_right = 0.0; + $style->border_right_width = 0.0; + + $split_style->margin_left = 0.0; + $split_style->padding_left = 0.0; + $split_style->border_left_width = 0.0; + + $p = $this->get_parent(); + $p->insert_child_after($deco, $this, false); + + if ($split_parent && $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; + } + + /** + * Determines the optimal font that applies to the frame and splits + * the frame where the optimal font changes. + */ + function apply_font_mapping(): void + { + if ($this->mapped_font !== null) { + return; + } + + $fontMetrics = $this->_dompdf->getFontMetrics(); + $style = $this->get_style(); + $families = $style->get_font_family_computed(); + $subtype = $fontMetrics->getType($style->font_weight . ' ' . $style->font_style); + $charMapping = $fontMetrics->mapTextToFonts($this->get_text(), $families, $subtype, 1); + + if (isset($charMapping[0])) { + if ($charMapping[0]["length"] !== 0) { + $this->split_text($charMapping[0]["length"], false); + } + $mapped_font = $charMapping[0]["font"]; + if ($mapped_font !== null) { + $style->set_used("font_family", $mapped_font); + $this->mapped_font = $mapped_font; + } + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/AbstractFrameReflower.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/AbstractFrameReflower.php new file mode 100644 index 0000000..7f0cb51 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/AbstractFrameReflower.php @@ -0,0 +1,607 @@ +_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": + $root = $frame->get_root(); + $parent = $frame->get_parent(); + do { + $parents_parent = $parent->get_parent(); + if ($parents_parent == $root) { + break; + } + $parent = $parents_parent; + } while ($parent); + $initial_cb = $parent->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()) { + /** @var AbstractFrameDecorator */ + $child = $iter->current(); + $child->get_reflower()->_set_content(); + if (!$iter->current()->is_absolute()) { + 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]; + } + + /** + * Resolves the `content` property to string. + * + * https://www.w3.org/TR/CSS21/generate.html#content + * + * @return string The resulting string + */ + protected function resolve_content(): ?string + { + $frame = $this->_frame; + $style = $frame->get_style(); + $content = $style->content; + + if ($content === "normal" || $content === "none") { + return null; + } + + $quotes = $style->quotes; + $text = ""; + + foreach ($content as $val) { + if ($val instanceof StringPart) { + $text .= $val->string; + } + + elseif ($val instanceof OpenQuote) { + // FIXME: Take quotation depth into account + if ($quotes !== "none" && isset($quotes[0][0])) { + $text .= $quotes[0][0]; + } + } + + elseif ($val instanceof CloseQuote) { + // FIXME: Take quotation depth into account + if ($quotes !== "none" && isset($quotes[0][1])) { + $text .= $quotes[0][1]; + } + } + + elseif ($val instanceof NoOpenQuote) { + // FIXME: Increment quotation depth + } + + elseif ($val instanceof NoCloseQuote) { + // FIXME: Decrement quotation depth + } + + elseif ($val instanceof Attr) { + $text .= $frame->get_parent()->get_node()->getAttribute($val->attribute); + } + + elseif ($val instanceof Counter) { + $p = $frame->lookup_counter_frame($val->name, true); + $text .= $p->counter_value($val->name, $val->style); + } + + elseif ($val instanceof Counters) { + $p = $frame->lookup_counter_frame($val->name, true); + $tmp = []; + while ($p) { + array_unshift($tmp, $p->counter_value($val->name, $val->style)); + $p = $p->lookup_counter_frame($val->name); + } + $text .= implode($val->string, $tmp); + } + } + + 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->resolve_content(); + + if ($content !== null) { + $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; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Block.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Block.php new file mode 100644 index 0000000..45db9fd --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Block.php @@ -0,0 +1,948 @@ +_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(): float + { + $height = 0.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|null $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]; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Image.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Image.php new file mode 100644 index 0000000..42618f6 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Image.php @@ -0,0 +1,213 @@ +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); + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Inline.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Inline.php new file mode 100644 index 0000000..d31fa45 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Inline.php @@ -0,0 +1,191 @@ +_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 margin, padding & border width to the first and last children, + // so they are accounted for during text layout + 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; + } + + 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; + } + + $frame->position(); + + $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; + } + } + + // Assume the position of the first in-flow child, otherwise use the + // fallback position that was set before child reflow + $child = $frame->get_first_child(); + while ($child && !$child->is_in_flow()) { + $child = $child->get_next_sibling(); + } + + if ($child) { + [$x, $y] = $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); + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/ListBullet.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/ListBullet.php new file mode 100644 index 0000000..c7141ab --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/ListBullet.php @@ -0,0 +1,51 @@ +_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); + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/NullFrameReflower.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/NullFrameReflower.php new file mode 100644 index 0000000..0159b1e --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/NullFrameReflower.php @@ -0,0 +1,37 @@ +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; + + // Only if it's the first page, we save the nodes with a fixed position + if ($child) { + foreach ($child->get_children() as $onechild) { + if ($onechild->get_style()->position === "fixed") { + $fixed_children[] = $onechild->deep_copy(); + $child->remove_child($onechild); + } + } + $fixed_children = array_reverse($fixed_children); + } + + 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; + + $child->set_containing_block($content_x, $content_y, $content_width, $content_height); + + //Insert a copy of each node which have a fixed position + foreach ($fixed_children as $fixed_child) { + $child->prepend_child($fixed_child->deep_copy()); + } + + // Check for begin reflow callback + $this->_check_callbacks("begin_page_reflow", $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); + } + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Table.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Table.php new file mode 100644 index 0000000..707d18f --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Table.php @@ -0,0 +1,523 @@ +_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 + ]; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableCell.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableCell.php new file mode 100644 index 0000000..e63029f --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableCell.php @@ -0,0 +1,165 @@ +_frame; + $table = TableFrameDecorator::find_parent_table($frame); + if ($table === null) { + throw new Exception("Parent table not found for table cell"); + } + + // Counters and generated content + $this->_set_content(); + + $style = $frame->get_style(); + $cellmap = $table->get_cellmap(); + + [$x, $y] = $cellmap->get_frame_position($frame); + $frame->set_position($x, $y); + + $cells = $cellmap->get_spanned_cells($frame); + + $w = 0; + foreach ($cells["columns"] as $i) { + $col = $cellmap->get_column($i); + $w += $col["used-width"]; + } + + //FIXME? + $h = $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); + $frame->increase_line_width($indent); + + $page = $frame->get_root(); + + // Set the y position of the first line in the cell + $line_box = $frame->get_current_line_box(); + $line_box->y = $line_y; + + // Set the containing blocks and reflow each child + foreach ($frame->get_children() as $child) { + $child->set_containing_block($content_x, $content_y, $cb_w, $h); + $this->process_clear($child); + $child->reflow($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); + $content_height = $this->_calculate_content_height(); + $height = max($style_height, $content_height); + + $frame->set_content_height($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 ($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]; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableRow.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableRow.php new file mode 100644 index 0000000..5115a24 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableRow.php @@ -0,0 +1,86 @@ +_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(); + + $frame->position(); + $style = $frame->get_style(); + $cb = $frame->get_containing_block(); + + foreach ($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($frame); + if ($table === null) { + throw new Exception("Parent table not found for table row"); + } + $cellmap = $table->get_cellmap(); + + $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)); + } + + /** + * @throws Exception + */ + public function get_min_max_width(): array + { + throw new Exception("Min/max width is undefined for table rows"); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableRowGroup.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableRowGroup.php new file mode 100644 index 0000000..20f5ee6 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableRowGroup.php @@ -0,0 +1,81 @@ +_frame; + $page = $frame->get_root(); + $parent = $frame->get_parent(); + $dompdf_generated = $parent->get_frame()->get_node()->nodeName === "dompdf_generated"; + + // 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; + } + } + + if ($page->is_full() && $dompdf_generated && $frame->get_parent() === null) { + return; + } + + $table = TableFrameDecorator::find_parent_table($frame); + if ($table === null) { + throw new Exception("Parent table not found for table row group"); + } + $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)); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Text.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Text.php new file mode 100644 index 0000000..7564910 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Text.php @@ -0,0 +1,601 @@ + + */ + 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; + + /** + * @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, "UTF-8"); + break; + case "lowercase": + $text = mb_convert_case($text, MB_CASE_LOWER, "UTF-8"); + 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 int|false + */ + 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; + } + + $force_first = $current_line->left == 0 + && $current_line->right == 0 + && $current_line->is_empty(); + + if ($nowrap) { + return $force_first ? 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 + 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 ($used_width > 0 && 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 ($force_first && $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 = ""; + $len = mb_strlen($word, "UTF-8"); + + for ($j = 0; $j < $len; $j++) { + $c = mb_substr($word, $j, 1, "UTF-8"); + $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, "UTF-8"); + return $offset; + } + + /** + * @param string $text + * @return int|false + */ + protected function newline_break(string $text) + { + if (($i = mb_strpos($text, "\n", 0, "UTF-8")) === 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->is_empty() && !$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, "UTF-8") + : $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, "UTF-8")) { + // Split the line + $frame->set_text($text); + $frame->split_text($split, true); + $add_line = true; + + // Remove inner soft hyphens + $t = $frame->get_text(); + $shyPosition = mb_strpos($t, self::SOFT_HYPHEN, 0, "UTF-8"); + if (false !== $shyPosition && $shyPosition < mb_strlen($t, "UTF-8") - 1) { + $t = str_replace(self::SOFT_HYPHEN, "", mb_substr($t, 0, -1, "UTF-8")) . mb_substr($t, -1, null, "UTF-8"); + $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 + * @throws Exception + */ + function reflow(?BlockFrameDecorator $block = null) + { + $frame = $this->_frame; + $page = $frame->get_root(); + $page->check_forced_page_break($frame); + + if ($page->is_full()) { + return; + } + + $style = $frame->get_style(); + + // Handle text transform and white space + $frame->set_text($this->pre_process_text($frame->get_text())); + + // map text to fonts based on supported Unicode range + $frame->apply_font_mapping(); + $text = $frame->get_text(); + + // Determine the text height + $size = $style->font_size; + $font = $style->font_family; + $font_height = $this->getFontMetrics()->getFontHeight($font, $size); + $style->set_used("height", $font_height); + + 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 + $text = $frame->get_text(); + if ($text === "" && $frame->get_margin_width() === 0.0) { + return; + } + + $line = $block->add_frame_to_line($frame); + $trimmed = trim($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 + { + $this->_frame->trim_trailing_ws(); + } + + public function reset(): void + { + parent::reset(); + } + + //........................................................................ + + public function get_min_max_width(): array + { + $fontMetrics = $this->getFontMetrics(); + $frame = $this->_frame; + $style = $frame->get_style(); + + // Handle text transform and white space + $frame->set_text($this->pre_process_text($frame->get_text())); + + // map text to fonts based on supported Unicode range + $frame->apply_font_mapping(); + $text = $frame->get_text(); + + $font = $style->font_family; + $size = $style->font_size; + $word_spacing = $style->word_spacing; + $letter_spacing = $style->letter_spacing; + + 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, "UTF-8"); + $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; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Helpers.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Helpers.php new file mode 100644 index 0000000..e8435d2 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Helpers.php @@ -0,0 +1,1253 @@ + tags if the current sapi is not 'cli'. + * Returns the output string instead of displaying it if $return is true. + * + * @param mixed $mixed variable or expression to display + * @param bool $return + * + * @return string|null + */ + public static function pre_r($mixed, $return = false) + { + if ($return) { + return "
" . print_r($mixed, true) . "
"; + } + + if (php_sapi_name() !== "cli") { + echo "
";
+        }
+
+        print_r($mixed);
+
+        if (php_sapi_name() !== "cli") {
+            echo "
"; + } else { + echo "\n"; + } + + flush(); + + return null; + } + + /** + * Builds a full url given a protocol, hostname, base path and URL. + * + * When the URL provided is a local file reference from the root of the filesystem + * (i.e., beginning with a "/") and the file does not resolve to a valid path, + * the path is validated against the chroot paths (if provided). + * + * @param string $protocol + * @param string $host + * @param string $base_path + * @param string $url + * @param array $chrootDirs array of strings representing the chroot paths + * @return string + */ + public static function build_url($protocol, $host, $base_path, $url, $chrootDirs = []) + { + $protocol = mb_strtolower($protocol, "UTF-8"); + if (empty($protocol)) { + $protocol = "file://"; + } + if ($url === "") { + return null; + } + + $url_lc = mb_strtolower($url, "UTF-8"); + + // Is the url already fully qualified, a Data URI, or a reference to a named anchor? + // File-protocol URLs may require additional processing (e.g. for URLs with a relative path) + if ( + ( + mb_strpos($url_lc, "://") !== false + && !in_array(substr($url_lc, 0, 7), ["file://", "phar://"], true) + ) + || mb_substr($url_lc, 0, 1) === "#" + || mb_strpos($url_lc, "data:") === 0 + || mb_strpos($url_lc, "mailto:") === 0 + || mb_strpos($url_lc, "tel:") === 0 + ) { + return $url; + } + + $res = ""; + if (strpos($url_lc, "file://") === 0) { + $url = substr($url, 7); + $protocol = "file://"; + } elseif (strpos($url_lc, "phar://") === 0) { + $res = substr($url, strpos($url_lc, ".phar")+5); + $url = substr($url, 7, strpos($url_lc, ".phar")-2); + $protocol = "phar://"; + } + + $ret = ""; + + $is_local_path = in_array($protocol, ["file://", "phar://"], true); + + if ($is_local_path) { + //On Windows local file, an abs path can begin also with a '\' or a drive letter and colon + //drive: followed by a relative path would be a drive specific default folder. + //not known in php app code, treat as abs path + //($url[1] !== ':' || ($url[2]!=='\\' && $url[2]!=='/')) + if ($url[0] !== '/' && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN' || (mb_strlen($url) > 1 && $url[0] !== '\\' && $url[1] !== ':'))) { + // For rel path and local access we ignore the host, and run the path through realpath() + $ret .= realpath($base_path) . '/'; + } + $ret .= $url; + $ret = preg_replace('/\?(.*)$/', "", $ret); + + $filepath = realpath($ret); + if ($filepath !== false) { + $ret = "$protocol$filepath$res"; + + return $ret; + } + + if ($url[0] == '/' && !empty($chrootDirs)) { + foreach ($chrootDirs as $dir) { + $ret = realpath($dir) . $url; + $ret = preg_replace('/\?(.*)$/', "", $ret); + + if ($filepath = realpath($ret)) { + $ret = "$protocol$filepath$res"; + + return $ret; + } + } + } + + return null; + } + + $ret = $protocol; + // Protocol relative urls (e.g. "//example.org/style.css") + if (strpos($url, '//') === 0) { + $ret .= substr($url, 2); + //remote urls with backslash in html/css are not really correct, but lets be genereous + } elseif ($url[0] === '/' || $url[0] === '\\') { + // Absolute path + $ret .= $host . $url; + } else { + // Relative path + //$base_path = $base_path !== "" ? rtrim($base_path, "/\\") . "/" : ""; + $ret .= $host . $base_path . $url; + } + + // URL should now be complete, final cleanup + $parsed_url = parse_url($ret); + + // reproduced from https://www.php.net/manual/en/function.parse-url.php#106731 + $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; + $host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; + $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; + $user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; + $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; + $pass = ($user || $pass) ? "$pass@" : ''; + $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; + $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; + $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; + + // partially reproduced from https://stackoverflow.com/a/1243431/264628 + /* replace '//' or '/./' or '/foo/../' with '/' */ + $re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#'); + for ($n=1; $n>0; $path=preg_replace($re, '/', $path, -1, $n)) {} + + $ret = "$scheme$user$pass$host$port$path$query$fragment"; + + return $ret; + } + + /** + * Builds a HTTP Content-Disposition header string using `$dispositionType` + * and `$filename`. + * + * If the filename contains any characters not in the ISO-8859-1 character + * set, a fallback filename will be included for clients not supporting the + * `filename*` parameter. + * + * @param string $dispositionType + * @param string $filename + * @return string + */ + public static function buildContentDispositionHeader($dispositionType, $filename) + { + $encoding = mb_detect_encoding($filename); + $fallbackfilename = mb_convert_encoding($filename, "ISO-8859-1", $encoding); + $fallbackfilename = str_replace("\"", "", $fallbackfilename); + $encodedfilename = rawurlencode($filename); + + $contentDisposition = "Content-Disposition: $dispositionType; filename=\"$fallbackfilename\""; + if ($fallbackfilename !== $filename) { + $contentDisposition .= "; filename*=UTF-8''$encodedfilename"; + } + + return $contentDisposition; + } + + /** + * Converts decimal numbers to roman numerals. + * + * As numbers larger than 3999 (and smaller than 1) cannot be represented in + * the standard form of roman numerals, those are left in decimal form. + * + * See https://en.wikipedia.org/wiki/Roman_numerals#Standard_form + * + * @param int|string $num + * + * @throws Exception + * @return string + */ + public static function dec2roman($num): string + { + + static $ones = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"]; + static $tens = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"]; + static $hund = ["", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm"]; + static $thou = ["", "m", "mm", "mmm"]; + + if (!is_numeric($num)) { + throw new Exception("dec2roman() requires a numeric argument."); + } + + if ($num >= 4000 || $num <= 0) { + return (string) $num; + } + + $num = strrev((string)$num); + + $ret = ""; + switch (mb_strlen($num)) { + /** @noinspection PhpMissingBreakStatementInspection */ + case 4: + $ret .= $thou[$num[3]]; + /** @noinspection PhpMissingBreakStatementInspection */ + case 3: + $ret .= $hund[$num[2]]; + /** @noinspection PhpMissingBreakStatementInspection */ + case 2: + $ret .= $tens[$num[1]]; + /** @noinspection PhpMissingBreakStatementInspection */ + case 1: + $ret .= $ones[$num[0]]; + default: + break; + } + + return $ret; + } + + /** + * Converts decimal numbers to base26 (hexavigesimal) + * represented in lower case letters. + * + * @param int|string $num + * + * @throws Exception + * @return string + */ + public static function dec2base26($num): string + { + if (!is_numeric($num)) { + throw new Exception("dec2base26() requires a numeric argument."); + } + + $num = intval($num); + + if ($num <= 0) { + return (string) $num; + } + + $ret = ''; + while ($num > 0) { + $remainder = ($num - 1) % 26; + $ret = chr(97 + $remainder) . $ret; + $num = intval(($num - 1) / 26); + } + return $ret; + } + + /** + * Restrict a length to the given range. + * + * If min > max, the result is min. + * + * @param float $length + * @param float $min + * @param float $max + * + * @return float + */ + public static function clamp(float $length, float $min, float $max): float + { + return max($min, min($length, $max)); + } + + /** + * Determines whether $value is a percentage or not + * + * @param string|float|int $value + * + * @return bool + */ + public static function is_percent($value): bool + { + return is_string($value) && false !== mb_strpos($value, "%"); + } + + /** + * Parses a data URI scheme + * http://en.wikipedia.org/wiki/Data_URI_scheme + * + * @param string $data_uri The data URI to parse + * + * @return array|bool The result with charset, mime type and decoded data + */ + public static function parse_data_uri($data_uri) + { + $expression = '/^data:(?P[a-z0-9\/+-.]+)(;charset=(?P[a-z0-9-])+)?(?P;base64)?\,(?P.*)?/is'; + if (!preg_match($expression, $data_uri, $match)) { + $parts = explode(",", $data_uri); + $parts[0] = preg_replace('/\\s/', '', $parts[0]); + if (preg_match('/\\s/', $data_uri) && !preg_match($expression, implode(",", $parts), $match)) { + return false; + } + } + + $match['data'] = rawurldecode($match['data']); + $result = [ + 'charset' => $match['charset'] ? $match['charset'] : 'US-ASCII', + 'mime' => $match['mime'] ? $match['mime'] : 'text/plain', + 'data' => $match['base64'] ? base64_decode($match['data']) : $match['data'], + ]; + + return $result; + } + + /** + * Encodes a Uniform Resource Identifier (URI) by replacing non-alphanumeric + * characters with a percent (%) sign followed by two hex digits, excepting + * characters in the URI reserved character set. + * + * Assumes that the URI is a complete URI, so does not encode reserved + * characters that have special meaning in the URI. + * + * Simulates the encodeURI function available in JavaScript + * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURI + * + * Source: http://stackoverflow.com/q/4929584/264628 + * + * @param string $uri The URI to encode + * @return string The original URL with special characters encoded + */ + public static function encodeURI($uri) { + $unescaped = [ + '%2D'=>'-','%5F'=>'_','%2E'=>'.','%21'=>'!', '%7E'=>'~', + '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')' + ]; + $reserved = [ + '%3B'=>';','%2C'=>',','%2F'=>'/','%3F'=>'?','%3A'=>':', + '%40'=>'@','%26'=>'&','%3D'=>'=','%2B'=>'+','%24'=>'$' + ]; + $score = [ + '%23'=>'#' + ]; + return preg_replace( + '/%25([a-fA-F0-9]{2,2})/', + '%$1', + strtr(rawurlencode($uri), array_merge($reserved, $unescaped, $score)) + ); + } + + /** + * Decoder for RLE8 compression in windows bitmaps + * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp + * + * @param string $str Data to decode + * @param int $width Image width + * + * @return string + */ + public static function rle8_decode($str, $width) + { + $lineWidth = $width + (3 - ($width - 1) % 4); + $out = ''; + $cnt = strlen($str); + + for ($i = 0; $i < $cnt; $i++) { + $o = ord($str[$i]); + switch ($o) { + case 0: # ESCAPE + $i++; + switch (ord($str[$i])) { + case 0: # NEW LINE + $padCnt = $lineWidth - strlen($out) % $lineWidth; + if ($padCnt < $lineWidth) { + $out .= str_repeat(chr(0), $padCnt); # pad line + } + break; + case 1: # END OF FILE + $padCnt = $lineWidth - strlen($out) % $lineWidth; + if ($padCnt < $lineWidth) { + $out .= str_repeat(chr(0), $padCnt); # pad line + } + break 3; + case 2: # DELTA + $i += 2; + break; + default: # ABSOLUTE MODE + $num = ord($str[$i]); + for ($j = 0; $j < $num; $j++) { + $out .= $str[++$i]; + } + if ($num % 2) { + $i++; + } + } + break; + default: + $out .= str_repeat($str[++$i], $o); + } + } + return $out; + } + + /** + * Decoder for RLE4 compression in windows bitmaps + * see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp + * + * @param string $str Data to decode + * @param int $width Image width + * + * @return string + */ + public static function rle4_decode($str, $width) + { + $w = floor($width / 2) + ($width % 2); + $lineWidth = $w + (3 - (($width - 1) / 2) % 4); + $pixels = []; + $cnt = strlen($str); + $c = 0; + + for ($i = 0; $i < $cnt; $i++) { + $o = ord($str[$i]); + switch ($o) { + case 0: # ESCAPE + $i++; + switch (ord($str[$i])) { + case 0: # NEW LINE + while (count($pixels) % $lineWidth != 0) { + $pixels[] = 0; + } + break; + case 1: # END OF FILE + while (count($pixels) % $lineWidth != 0) { + $pixels[] = 0; + } + break 3; + case 2: # DELTA + $i += 2; + break; + default: # ABSOLUTE MODE + $num = ord($str[$i]); + for ($j = 0; $j < $num; $j++) { + if ($j % 2 == 0) { + $c = ord($str[++$i]); + $pixels[] = ($c & 240) >> 4; + } else { + $pixels[] = $c & 15; + } + } + + if ($num % 2 == 0) { + $i++; + } + } + break; + default: + $c = ord($str[++$i]); + for ($j = 0; $j < $o; $j++) { + $pixels[] = ($j % 2 == 0 ? ($c & 240) >> 4 : $c & 15); + } + } + } + + $out = ''; + if (count($pixels) % 2) { + $pixels[] = 0; + } + + $cnt = count($pixels) / 2; + + for ($i = 0; $i < $cnt; $i++) { + $out .= chr(16 * $pixels[2 * $i] + $pixels[2 * $i + 1]); + } + + return $out; + } + + /** + * parse a full url or pathname and return an array(protocol, host, path, + * file + query + fragment) + * + * @param string $url + * @return array + */ + public static function explode_url($url) + { + $protocol = ""; + $host = ""; + $path = ""; + $file = ""; + $res = ""; + + $arr = parse_url($url); + if ( isset($arr["scheme"]) ) { + $arr["scheme"] = mb_strtolower($arr["scheme"], "UTF-8"); + } + + if (isset($arr["scheme"]) && $arr["scheme"] !== "file" && $arr["scheme"] !== "phar" && strlen($arr["scheme"]) > 1) { + $protocol = $arr["scheme"] . "://"; + + if (isset($arr["user"])) { + $host .= $arr["user"]; + + if (isset($arr["pass"])) { + $host .= ":" . $arr["pass"]; + } + + $host .= "@"; + } + + if (isset($arr["host"])) { + $host .= $arr["host"]; + } + + if (isset($arr["port"])) { + $host .= ":" . $arr["port"]; + } + + if (isset($arr["path"]) && $arr["path"] !== "") { + // Do we have a trailing slash? + if ($arr["path"][mb_strlen($arr["path"], "8bit") - 1] === "/") { + $path = $arr["path"]; + $file = ""; + } else { + $path = rtrim(dirname($arr["path"]), '/\\') . "/"; + $file = basename($arr["path"]); + } + } + + if (isset($arr["query"])) { + $file .= "?" . $arr["query"]; + } + + if (isset($arr["fragment"])) { + $file .= "#" . $arr["fragment"]; + } + + } else { + + $protocol = ""; + $host = ""; // localhost, really + + $i = mb_stripos($url, "://", 0, "UTF-8"); + if ($i !== false) { + $protocol = mb_strtolower(mb_substr($url, 0, $i + 3, "UTF-8"), "UTF-8"); + $url = mb_substr($url, $i + 3, null, "UTF-8"); + } else { + $protocol = "file://"; + } + + if ($protocol === "phar://") { + $res = substr($url, stripos($url, ".phar")+5); + $url = substr($url, 7, stripos($url, ".phar")-2); + } + + $file = basename($url); + $path = dirname($url) . "/"; + } + + $ret = [$protocol, $host, $path, $file, + "protocol" => $protocol, + "host" => $host, + "path" => $path, + "file" => $file, + "resource" => $res]; + return $ret; + } + + /** + * Print debug messages + * + * @param string $type The type of debug messages to print + * @param string $msg The message to show + */ + public static function dompdf_debug($type, $msg) + { + global $_DOMPDF_DEBUG_TYPES, $_dompdf_show_warnings, $_dompdf_debug; + if (isset($_DOMPDF_DEBUG_TYPES[$type]) && ($_dompdf_show_warnings || $_dompdf_debug)) { + $arr = debug_backtrace(); + + echo basename($arr[0]["file"]) . " (" . $arr[0]["line"] . "): " . $arr[1]["function"] . ": "; + Helpers::pre_r($msg); + } + } + + /** + * Stores warnings in an array for display later + * This function allows warnings generated by the DomDocument parser + * and CSS loader ({@link Stylesheet}) to be captured and displayed + * later. Without this function, errors are displayed immediately and + * PDF streaming is impossible. + * @see http://www.php.net/manual/en/function.set-error_handler.php + * + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param string $errline + * + * @throws Exception + */ + public static function record_warnings($errno, $errstr, $errfile, $errline) + { + // Not a warning or notice + if (!($errno & (E_WARNING | E_NOTICE | E_USER_NOTICE | E_USER_WARNING | E_DEPRECATED | E_USER_DEPRECATED))) { + throw new Exception($errstr . " $errno"); + } + + global $_dompdf_warnings; + global $_dompdf_show_warnings; + + if ($_dompdf_show_warnings) { + echo $errstr . "\n"; + } + + $_dompdf_warnings[] = $errstr; + } + + /** + * Get Unicode code point of character + * + * Shim for use on systems running PHP < 7.2 + * + * @param string $c + * @param string|null $encoding + * @return int|false + */ + public static function uniord(string $c, ?string $encoding = null) + { + if (function_exists("mb_ord")) { + if (PHP_VERSION_ID < 80000 && $encoding === null) { + // in PHP < 8 the encoding argument, if supplied, must be a valid encoding + $encoding = "UTF-8"; + } + return mb_ord($c, $encoding); + } + + if ($encoding != "UTF-8" && $encoding !== null) { + $c = mb_convert_encoding($c, "UTF-8", $encoding); + } + + $length = mb_strlen(mb_substr($c, 0, 1, "UTF-8"), "8bit"); + $ord = false; + $bytes = []; + $numbytes = 1; + for ($i = 0; $i < $length; $i++) { + $o = ord($c[$i]); // get one string character at time + if (count($bytes) === 0) { // get starting octect + if ($o <= 0x7F) { + $ord = $o; + $numbytes = 1; + } elseif (($o >> 0x05) === 0x06) { // 2 bytes character (0x06 = 110 BIN) + $bytes[] = ($o - 0xC0) << 0x06; + $numbytes = 2; + } elseif (($o >> 0x04) === 0x0E) { // 3 bytes character (0x0E = 1110 BIN) + $bytes[] = ($o - 0xE0) << 0x0C; + $numbytes = 3; + } elseif (($o >> 0x03) === 0x1E) { // 4 bytes character (0x1E = 11110 BIN) + $bytes[] = ($o - 0xF0) << 0x12; + $numbytes = 4; + } else { + $ord = false; + break; + } + } elseif (($o >> 0x06) === 0x02) { // bytes 2, 3 and 4 must start with 0x02 = 10 BIN + $bytes[] = $o - 0x80; + if (count($bytes) === $numbytes) { + // compose UTF-8 bytes to a single unicode value + $o = $bytes[0]; + for ($j = 1; $j < $numbytes; $j++) { + $o += ($bytes[$j] << (($numbytes - $j - 1) * 0x06)); + } + if ((($o >= 0xD800) and ($o <= 0xDFFF)) or ($o >= 0x10FFFF)) { + // The definition of UTF-8 prohibits encoding character numbers between + // U+D800 and U+DFFF, which are reserved for use with the UTF-16 + // encoding form (as surrogate pairs) and do not directly represent + // characters. + return false; + } else { + $ord = $o; // add char to array + } + // reset data for next char + $bytes = []; + $numbytes = 1; + } + } else { + $ord = false; + break; + } + } + + return $ord; + } + + /** + * Return character by Unicode code point value + * + * Shim for use on systems running PHP < 7.2 + * + * @param int $c + * @param string|null $encoding + * @return string|false + */ + public static function unichr(int $c, ?string $encoding = null) + { + if (function_exists("mb_chr")) { + if (PHP_VERSION_ID < 80000 && $encoding === null) { + // in PHP < 8 the encoding argument, if supplied, must be a valid encoding + $encoding = "UTF-8"; + } + return mb_chr($c, $encoding); + } + + $chr = false; + if ($c <= 0x7F) { + $chr = chr($c); + } elseif ($c <= 0x7FF) { + $chr = chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F); + } elseif ($c <= 0xFFFF) { + $chr = chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F) + . chr(0x80 | $c & 0x3F); + } elseif ($c <= 0x10FFFF) { + $chr = chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F) + . chr(0x80 | $c >> 6 & 0x3F) + . chr(0x80 | $c & 0x3F); + } + + return $chr; + } + + /** + * Converts a CMYK color to RGB + * + * @param float|float[] $c + * @param float $m + * @param float $y + * @param float $k + * + * @return float[] + */ + public static function cmyk_to_rgb($c, $m = null, $y = null, $k = null) + { + if (is_array($c)) { + [$c, $m, $y, $k] = $c; + } + + $c *= 255; + $m *= 255; + $y *= 255; + $k *= 255; + + $r = (1 - round(2.55 * ($c + $k))); + $g = (1 - round(2.55 * ($m + $k))); + $b = (1 - round(2.55 * ($y + $k))); + + if ($r < 0) { + $r = 0; + } + if ($g < 0) { + $g = 0; + } + if ($b < 0) { + $b = 0; + } + + return [ + $r, $g, $b, + "r" => $r, "g" => $g, "b" => $b + ]; + } + + /** + * getimagesize doesn't give a good size for 32bit BMP image v5 + * + * @param string $filename + * @param resource $context + * @return array An array of three elements: width and height as + * `float|int`, and image type as `string|null`. + */ + public static function dompdf_getimagesize($filename, $context = null) + { + static $cache = []; + + // Custom types + $types = [ + IMAGETYPE_JPEG => "jpeg", + IMAGETYPE_GIF => "gif", + IMAGETYPE_BMP => "bmp", + IMAGETYPE_PNG => "png", + IMAGETYPE_WEBP => "webp" + ]; + if (defined('IMAGETYPE_SVG')) { + $types[IMAGETYPE_SVG] = "svg"; + } + + if (isset($cache[$filename])) { + return $cache[$filename]; + } + + $parse_result = @getimagesize($filename); + $width = $height = $type = null; + if ($parse_result !== false) { + [$width, $height, $type] = $parse_result; + $type = $types[$type] ?? null; + } + + if ($width == null || $height == null) { + [$data] = Helpers::getFileContent($filename, $context); + + if ($data !== null) { + if (substr($data, 0, 2) === "BM") { + $meta = unpack("vtype/Vfilesize/Vreserved/Voffset/Vheadersize/Vwidth/Vheight", $data); + $width = (int) $meta["width"]; + $height = (int) $meta["height"]; + $type = "bmp"; + } elseif (strpos($data, "loadFile($filename); + + [$width, $height] = $doc->getDimensions(); + $width = (float) $width; + $height = (float) $height; + $type = "svg"; + } + } + } + + return $cache[$filename] = [$width ?? 0, $height ?? 0, $type]; + } + + /** + * Credit goes to mgutt + * http://www.programmierer-forum.de/function-imagecreatefrombmp-welche-variante-laeuft-t143137.htm + * Modified by Fabien Menager to support RGB555 BMP format + */ + public static function imagecreatefrombmp($filename) + { + if (!function_exists("imagecreatetruecolor")) { + trigger_error("The PHP GD extension is required, but is not installed.", E_ERROR); + return false; + } + + if (function_exists("imagecreatefrombmp") && ($im = imagecreatefrombmp($filename)) !== false) { + return $im; + } + + // version 1.00 + if (!($fh = fopen($filename, 'rb'))) { + trigger_error('imagecreatefrombmp: Can not open ' . $filename, E_USER_WARNING); + return false; + } + + $bytes_read = 0; + + // read file header + $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14)); + + // check for bitmap + if ($meta['type'] != 19778) { + trigger_error('imagecreatefrombmp: ' . $filename . ' is not a bitmap!', E_USER_WARNING); + return false; + } + + // read image header + $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40)); + $bytes_read += 40; + + // read additional bitfield header + if ($meta['compression'] == 3) { + $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12)); + $bytes_read += 12; + } + + // set bytes and padding + $meta['bytes'] = $meta['bits'] / 8; + $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4))); + if ($meta['decal'] == 4) { + $meta['decal'] = 0; + } + + // obtain imagesize + if ($meta['imagesize'] < 1) { + $meta['imagesize'] = $meta['filesize'] - $meta['offset']; + // in rare cases filesize is equal to offset so we need to read physical size + if ($meta['imagesize'] < 1) { + $meta['imagesize'] = @filesize($filename) - $meta['offset']; + if ($meta['imagesize'] < 1) { + trigger_error('imagecreatefrombmp: Can not obtain filesize of ' . $filename . '!', E_USER_WARNING); + return false; + } + } + } + + // calculate colors + $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors']; + + // read color palette + $palette = []; + if ($meta['bits'] < 16) { + $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4)); + // in rare cases the color value is signed + if ($palette[1] < 0) { + foreach ($palette as $i => $color) { + $palette[$i] = $color + 16777216; + } + } + } + + // ignore extra bitmap headers + if ($meta['headersize'] > $bytes_read) { + fread($fh, $meta['headersize'] - $bytes_read); + } + + // create gd image + $im = imagecreatetruecolor($meta['width'], $meta['height']); + $data = fread($fh, $meta['imagesize']); + + // uncompress data + switch ($meta['compression']) { + case 1: + $data = Helpers::rle8_decode($data, $meta['width']); + break; + case 2: + $data = Helpers::rle4_decode($data, $meta['width']); + break; + } + + $p = 0; + $vide = chr(0); + $y = $meta['height'] - 1; + $error = 'imagecreatefrombmp: ' . $filename . ' has not enough data!'; + + // loop through the image data beginning with the lower left corner + while ($y >= 0) { + $x = 0; + while ($x < $meta['width']) { + switch ($meta['bits']) { + case 32: + case 24: + if (!($part = substr($data, $p, 3 /*$meta['bytes']*/))) { + trigger_error($error, E_USER_WARNING); + return $im; + } + $color = unpack('V', $part . $vide); + break; + case 16: + if (!($part = substr($data, $p, 2 /*$meta['bytes']*/))) { + trigger_error($error, E_USER_WARNING); + return $im; + } + $color = unpack('v', $part); + + if (empty($meta['rMask']) || $meta['rMask'] != 0xf800) { + $color[1] = (($color[1] & 0x7c00) >> 7) * 65536 + (($color[1] & 0x03e0) >> 2) * 256 + (($color[1] & 0x001f) << 3); // 555 + } else { + $color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3); // 565 + } + break; + case 8: + $color = unpack('n', $vide . substr($data, $p, 1)); + $color[1] = $palette[$color[1] + 1]; + break; + case 4: + $color = unpack('n', $vide . substr($data, floor($p), 1)); + $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F; + $color[1] = $palette[$color[1] + 1]; + break; + case 1: + $color = unpack('n', $vide . substr($data, floor($p), 1)); + switch (($p * 8) % 8) { + case 0: + $color[1] = $color[1] >> 7; + break; + case 1: + $color[1] = ($color[1] & 0x40) >> 6; + break; + case 2: + $color[1] = ($color[1] & 0x20) >> 5; + break; + case 3: + $color[1] = ($color[1] & 0x10) >> 4; + break; + case 4: + $color[1] = ($color[1] & 0x8) >> 3; + break; + case 5: + $color[1] = ($color[1] & 0x4) >> 2; + break; + case 6: + $color[1] = ($color[1] & 0x2) >> 1; + break; + case 7: + $color[1] = ($color[1] & 0x1); + break; + } + $color[1] = $palette[$color[1] + 1]; + break; + default: + trigger_error('imagecreatefrombmp: ' . $filename . ' has ' . $meta['bits'] . ' bits and this is not supported!', E_USER_WARNING); + return false; + } + imagesetpixel($im, $x, $y, $color[1]); + $x++; + $p += $meta['bytes']; + } + $y--; + $p += $meta['decal']; + } + fclose($fh); + return $im; + } + + /** + * Gets the content of the file at the specified path using one of + * the following methods, in preferential order: + * - file_get_contents: if allow_url_fopen is true or the file is local + * - curl: if allow_url_fopen is false and curl is available + * + * @param string $uri + * @param resource $context + * @param int $offset + * @param int $maxlen + * @return string[] + */ + public static function getFileContent($uri, $context = null, $offset = 0, $maxlen = null) + { + $content = null; + $headers = null; + [$protocol] = Helpers::explode_url($uri); + $is_local_path = in_array(strtolower($protocol), ["", "file://", "phar://"], true); + $can_use_curl = in_array(strtolower($protocol), ["http://", "https://"], true) && function_exists('curl_exec'); + + set_error_handler([self::class, 'record_warnings']); + + try { + if ($is_local_path || ini_get('allow_url_fopen') && !$can_use_curl) { + $http_response_header = null; + if (version_compare(PHP_VERSION, "8.4.0", ">=")) { + \http_clear_last_response_headers(); + } + if ($is_local_path === false) { + $uri = Helpers::encodeURI($uri); + } + if (isset($maxlen)) { + $result = file_get_contents($uri, false, $context, $offset, $maxlen); + } else { + $result = file_get_contents($uri, false, $context, $offset); + } + if ($result !== false) { + $content = $result; + } + if (version_compare(PHP_VERSION, "8.4.0", ">=")) { + $headers = \http_get_last_response_headers(); + \http_clear_last_response_headers(); + } elseif (isset($http_response_header)) { + $headers = $http_response_header; + } + + } elseif ($can_use_curl) { + $curl = curl_init($uri); + + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HEADER, true); + if ($offset > 0) { + curl_setopt($curl, CURLOPT_RESUME_FROM, $offset); + } + + if ($maxlen > 0) { + curl_setopt($curl, CURLOPT_BUFFERSIZE, 128); + curl_setopt($curl, CURLOPT_NOPROGRESS, false); + curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, function ($res, $download_size_total, $download_size, $upload_size_total, $upload_size) use ($maxlen) { + return ($download_size > $maxlen) ? 1 : 0; + }); + } + + $context_options = []; + if (!is_null($context)) { + $context_options = stream_context_get_options($context); + } + foreach ($context_options as $stream => $options) { + foreach ($options as $option => $value) { + $key = strtolower($stream) . ":" . strtolower($option); + switch ($key) { + case "curl:curl_verify_ssl_host": + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, !$value ? 0 : 2); + break; + case "curl:max_redirects": + curl_setopt($curl, CURLOPT_MAXREDIRS, $value); + break; + case "http:follow_location": + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, $value); + break; + case "http:header": + if (is_string($value)) { + curl_setopt($curl, CURLOPT_HTTPHEADER, [$value]); + } else { + curl_setopt($curl, CURLOPT_HTTPHEADER, $value); + } + break; + case "http:timeout": + curl_setopt($curl, CURLOPT_TIMEOUT, $value); + break; + case "http:user_agent": + curl_setopt($curl, CURLOPT_USERAGENT, $value); + break; + case "curl:curl_verify_ssl_peer": + case "ssl:verify_peer": + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $value); + break; + } + } + } + + $data = curl_exec($curl); + + if ($data !== false && !curl_errno($curl)) { + switch ($http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE)) { + case 200: + $raw_headers = substr($data, 0, curl_getinfo($curl, CURLINFO_HEADER_SIZE)); + $headers = preg_split("/[\n\r]+/", trim($raw_headers)); + $content = substr($data, curl_getinfo($curl, CURLINFO_HEADER_SIZE)); + break; + } + } + + if (PHP_MAJOR_VERSION < 8) { + curl_close($curl); + } + } + } finally { + restore_error_handler(); + } + + return [$content, $headers]; + } + + /** + * @param string $str + * @return string + */ + public static function mb_ucwords(string $str): string + { + $max_len = mb_strlen($str, "UTF-8"); + if ($max_len === 1) { + return mb_strtoupper($str, "UTF-8"); + } + + $str = mb_strtoupper(mb_substr($str, 0, 1, "UTF-8"), "UTF-8") . mb_substr($str, 1, null, "UTF-8"); + + foreach ([' ', '.', ',', '!', '?', '-', '+'] as $s) { + $pos = 0; + while (($pos = mb_strpos($str, $s, $pos, "UTF-8")) !== false) { + $pos++; + // Nothing to do if the separator is the last char of the string + if ($pos !== false && $pos < $max_len) { + // If the char we want to upper is the last char there is nothing to append behind + if ($pos + 1 < $max_len) { + $str = mb_substr($str, 0, $pos, "UTF-8") . mb_strtoupper(mb_substr($str, $pos, 1, "UTF-8"), "UTF-8") . mb_substr($str, $pos + 1, null, "UTF-8"); + } else { + $str = mb_substr($str, 0, $pos, "UTF-8") . mb_strtoupper(mb_substr($str, $pos, 1, "UTF-8"), "UTF-8"); + } + } + } + } + + return $str; + } + + /** + * Check whether two lengths should be considered equal, accounting for + * inaccuracies in float computation. + * + * The implementation relies on the fact that we are neither dealing with + * very large, nor with very small numbers in layout. Adapted from + * https://floating-point-gui.de/errors/comparison/. + * + * @param float $a + * @param float $b + * + * @return bool + */ + public static function lengthEqual(float $a, float $b): bool + { + // The epsilon results in a precision of at least: + // * 7 decimal digits at around 1 + // * 4 decimal digits at around 1000 (around the size of common paper formats) + // * 2 decimal digits at around 100,000 (100,000pt ~ 35.28m) + static $epsilon = 1e-8; + static $almostZero = 1e-12; + + $diff = abs($a - $b); + + if ($a === $b || $diff < $almostZero) { + return true; + } + + return $diff < $epsilon * max(abs($a), abs($b)); + } + + /** + * Check `$a < $b`, accounting for inaccuracies in float computation. + */ + public static function lengthLess(float $a, float $b): bool + { + return $a < $b && !self::lengthEqual($a, $b); + } + + /** + * Check `$a <= $b`, accounting for inaccuracies in float computation. + */ + public static function lengthLessOrEqual(float $a, float $b): bool + { + return $a <= $b || self::lengthEqual($a, $b); + } + + /** + * Check `$a > $b`, accounting for inaccuracies in float computation. + */ + public static function lengthGreater(float $a, float $b): bool + { + return $a > $b && !self::lengthEqual($a, $b); + } + + /** + * Check `$a >= $b`, accounting for inaccuracies in float computation. + */ + public static function lengthGreaterOrEqual(float $a, float $b): bool + { + return $a >= $b || self::lengthEqual($a, $b); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Image/Cache.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Image/Cache.php new file mode 100644 index 0000000..5a06f3f --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Image/Cache.php @@ -0,0 +1,292 @@ +getChroot()); + + 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; + + $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 (strtolower($name) === "image") { + if (!\array_key_exists($full_url, self::$svgRefs)) { + self::$svgRefs[$full_url] = []; + } + $attributes = array_change_key_case($attributes, CASE_LOWER); + $urls = []; + $urls[] = $attributes["xlink:href"] ?? ""; + $urls[] = $attributes["href"] ?? ""; + foreach ($urls as $url) { + if (empty($url)) { + continue; + } + + $inner_full_url = Helpers::build_url($parsed_url["protocol"], $parsed_url["host"], $parsed_url["path"], $url, $options->getChroot()); + if (empty($inner_full_url)) { + continue; + } + + self::detectCircularRef($full_url, $inner_full_url); + self::$svgRefs[$full_url][] = $inner_full_url; + [$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); + } + } + } + }, + null + ); + + if (($fp = fopen($resolved_url, "r")) !== false) { + while ($line = fread($fp, 8192)) { + xml_parse($parser, $line, false); + } + fclose($fp); + xml_parse($parser, "", true); + } + if (PHP_MAJOR_VERSION < 8) { + 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()); + if ($full_url !== null) { + self::$_cache[$full_url] = $resolved_url; + } + } + + return [$resolved_url, $type, $message]; + } + + static function detectCircularRef(string $src, string $target) + { + if (!\array_key_exists($target, self::$svgRefs)) { + return; + } + foreach (self::$svgRefs[$target] as $ref) { + if ($ref === $src) { + throw new ImageException("Circular external SVG image reference detected.", E_WARNING); + } + self::detectCircularRef($src, $ref); + } + } + + /** + * 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 = []; + self::$svgRefs = []; + } + + 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"); +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/JavascriptEmbedder.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/JavascriptEmbedder.php new file mode 100644 index 0000000..f4b9bc2 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/JavascriptEmbedder.php @@ -0,0 +1,51 @@ +_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); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/LineBox.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/LineBox.php new file mode 100644 index 0000000..85ea8cc --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/LineBox.php @@ -0,0 +1,407 @@ +_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): array + { + $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(): void + { + 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(): float + { + return $this->left + $this->w + $this->right; + } + + /** + * @return Block + */ + public function get_block_frame(): Block + { + return $this->_block_frame; + } + + /** + * @return AbstractFrameDecorator[] + */ + public function &get_frames(): array + { + return $this->_frames; + } + + /** + * @return bool + */ + public function is_empty(): bool + { + 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 + */ + 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; + } + + 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; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Options.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Options.php new file mode 100644 index 0000000..d7b9453 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Options.php @@ -0,0 +1,1231 @@ + ["rules" => []], + "file://" => ["rules" => []], + "http://" => ["rules" => []], + "https://" => ["rules" => []] + ]; + + /** + * Operational artifact (log files, temporary files) path validation + * + * @var callable + */ + private $artifactPathValidation = null; + + /** + * @var string + */ + private $logOutputFile = ''; + + /** + * Styles targeted to this media type are applied to the document. + * This is on top of the media types that are always applied: + * all, static, visual, bitmap, paged, dompdf + * + * @var string + */ + private $defaultMediaType = "screen"; + + /** + * The default paper size. + * + * North America standard is "letter"; other countries generally "a4" + * @see \Dompdf\Adapter\CPDF::PAPER_SIZES for valid sizes + * + * @var string|float[] + */ + private $defaultPaperSize = "letter"; + + /** + * The default paper orientation. + * + * The orientation of the page (portrait or landscape). + * + * @var string + */ + private $defaultPaperOrientation = "portrait"; + + /** + * The default font family + * + * Used if no suitable fonts can be found. This must exist in the font folder. + * + * @var string + */ + private $defaultFont = "serif"; + + /** + * Image DPI setting + * + * This setting determines the default DPI setting for images and fonts. The + * DPI may be overridden for inline images by explicitly setting the + * image's width & height style attributes (i.e. if the image's native + * width is 600 pixels and you specify the image's width as 72 points, + * the image will have a DPI of 600 in the rendered PDF. The DPI of + * background images can not be overridden and is controlled entirely + * via this parameter. + * + * For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI). + * If a size in html is given as px (or without unit as image size), + * this tells the corresponding size in pt at 72 DPI. + * This adjusts the relative sizes to be similar to the rendering of the + * html page in a reference browser. + * + * In pdf, always 1 pt = 1/72 inch + * + * @var int + */ + private $dpi = 96; + + /** + * A ratio applied to the fonts height to be more like browsers' line height + * + * @var float + */ + private $fontHeightRatio = 1.1; + + /** + * Enable embedded PHP + * + * If this setting is set to true then DOMPDF will automatically evaluate + * embedded PHP contained within tags. + * + * ==== IMPORTANT ==== + * Enabling this for documents you do not trust (e.g. arbitrary remote html + * pages) is a security risk. Embedded scripts are run with the same level of + * system access available to dompdf. Set this option to false (recommended) + * if you wish to process untrusted documents. + * + * This setting may increase the risk of system exploit. Do not change + * this settings without understanding the consequences. Additional + * documentation is available on the dompdf wiki at: + * https://github.com/dompdf/dompdf/wiki + * + * @var bool + */ + private $isPhpEnabled = false; + + /** + * Enable remote file access + * + * If this setting is set to true, DOMPDF will access remote sites for + * images and CSS files as required. + * + * ==== IMPORTANT ==== + * This can be a security risk, in particular in combination with isPhpEnabled and + * allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...); + * This allows anonymous users to download legally doubtful internet content which on + * tracing back appears to being downloaded by your server, or allows malicious php code + * in remote html pages to be executed by your server with your account privileges. + * + * This setting may increase the risk of system exploit. Do not change + * this settings without understanding the consequences. Additional + * documentation is available on the dompdf wiki at: + * https://github.com/dompdf/dompdf/wiki + * + * @var bool + */ + private $isRemoteEnabled = false; + + /** + * List of allowed remote hosts + * + * Each value of the array must be a valid hostname. + * + * This will be used to filter which resources can be loaded in combination with + * isRemoteEnabled. If isRemoteEnabled is FALSE, then this will have no effect. + * + * Leave to NULL to allow any remote host. + * + * @var array|null + */ + private $allowedRemoteHosts = null; + + /** + * Enable PDF/A-3 compliance mode + * + * ==== EXPERIMENTAL ==== + * This feature is currently only supported with the CPDF backend and will + * have no effect if used with any other. + * + * Currently this mode only takes care of adding the necessary metadata, output intents, etc. + * It does not enforce font embedding, it's up to you to embed the fonts you plan on using. + * + * @var bool + */ + private $isPdfAEnabled = false; + + /** + * Enable inline JavaScript + * + * If this setting is set to true then DOMPDF will automatically insert + * JavaScript code contained within + * tags as written into the PDF. + * + * NOTE: This is PDF-based JavaScript to be executed by the PDF viewer, + * not browser-based JavaScript executed by Dompdf. + * + * @var bool + */ + private $isJavascriptEnabled = true; + + /** + * Use the HTML5 Lib parser + * + * @deprecated + * @var bool + */ + private $isHtml5ParserEnabled = true; + + /** + * Whether to enable font subsetting or not. + * + * @var bool + */ + private $isFontSubsettingEnabled = true; + + /** + * @var bool + */ + private $debugPng = false; + + /** + * @var bool + */ + private $debugKeepTemp = false; + + /** + * @var bool + */ + private $debugCss = false; + + /** + * @var bool + */ + private $debugLayout = false; + + /** + * @var bool + */ + private $debugLayoutLines = true; + + /** + * @var bool + */ + private $debugLayoutBlocks = true; + + /** + * @var bool + */ + private $debugLayoutInline = true; + + /** + * @var bool + */ + private $debugLayoutPaddingBox = true; + + /** + * The PDF rendering backend to use + * + * Valid settings are 'PDFLib', 'CPDF', 'GD', and 'auto'. 'auto' will + * look for PDFLib and use it if found, or if not it will fall back on + * CPDF. 'GD' renders PDFs to graphic files. {@link Dompdf\CanvasFactory} + * ultimately determines which rendering class to instantiate + * based on this setting. + * + * @var string + */ + private $pdfBackend = "CPDF"; + + /** + * PDFlib license key + * + * If you are using a licensed, commercial version of PDFlib, specify + * your license key here. If you are using PDFlib-Lite or are evaluating + * the commercial version of PDFlib, comment out this setting. + * + * @link http://www.pdflib.com + * + * If pdflib present in web server and auto or selected explicitly above, + * a real license code must exist! + * + * @var string + */ + private $pdflibLicense = ""; + + /** + * HTTP context created with stream_context_create() + * Will be used for file_get_contents + * + * @link https://www.php.net/manual/context.php + * + * @var resource + */ + private $httpContext; + + /** + * @param array $attributes + */ + public function __construct(?array $attributes = null) + { + $rootDir = realpath(__DIR__ . "/../"); + $this->setChroot(array($rootDir)); + $this->setRootDir($rootDir); + $this->setTempDir(sys_get_temp_dir()); + $this->setFontDir($rootDir . "/lib/fonts"); + $this->setFontCache($this->getFontDir()); + + $ver = ""; + $versionFile = realpath(__DIR__ . '/../VERSION'); + if (($version = file_get_contents($versionFile)) !== false) { + $version = trim($version); + if ($version !== '$Format:<%h>$') { + $ver = "/$version"; + } + } + $this->setHttpContext([ + "http" => [ + "follow_location" => false, + "user_agent" => "Dompdf$ver https://github.com/dompdf/dompdf" + ] + ]); + + $this->setAllowedProtocols(["data://", "file://", "http://", "https://"]); + + $this->setArtifactPathValidation([$this, "validateArtifactPath"]); + + if (null !== $attributes) { + $this->set($attributes); + } + } + + /** + * @param array|string $attributes + * @param null|mixed $value + * @return $this + */ + public function set($attributes, $value = null) + { + if (!is_array($attributes)) { + $attributes = [$attributes => $value]; + } + + foreach ($attributes as $key => $value) { + $methodForMatch = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))); + $methodForCall = "set" . ucfirst($methodForMatch); + + if ($methodForMatch === 'enablePhp') { + $methodForCall = "setIsPhpEnabled"; + } elseif ($methodForMatch === 'enableRemote') { + $methodForCall = "setIsRemoteEnabled"; + } elseif ($methodForMatch === 'enablePdfA') { + $methodForCall = "setIsPdfAEnabled"; + } elseif ($methodForMatch === 'enableJavascript') { + $methodForCall = "setIsJavascriptEnabled"; + } elseif ($methodForMatch === 'enableHtml5Parser') { + $methodForCall = "setIsHtml5ParserEnabled"; + } elseif ($methodForMatch === 'enableFontSubsetting') { + $methodForCall = "setIsFontSubsettingEnabled"; + } + + if (method_exists($this, $methodForCall)) { + $this->{$methodForCall}($value); + } + } + + return $this; + } + + /** + * @param string $key + * @return mixed + */ + public function get($key) + { + $methodForMatch = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))); + $methodForCall = "get" . ucfirst($methodForMatch); + + if ($methodForMatch === 'enablePhp') { + $methodForCall = "getIsPhpEnabled"; + } elseif ($methodForMatch === 'enableRemote') { + $methodForCall = "getIsRemoteEnabled"; + } elseif ($methodForMatch === 'enablePdfA') { + $methodForCall = "getIsPdfAEnabled"; + } elseif ($methodForMatch === 'enableJavascript') { + $methodForCall = "getIsJavascriptEnabled"; + } elseif ($methodForMatch === 'enableHtml5Parser') { + $methodForCall = "getIsHtml5ParserEnabled"; + } elseif ($methodForMatch === 'enableFontSubsetting') { + $methodForCall = "getIsFontSubsettingEnabled"; + } + + if (method_exists($this, $methodForCall)) { + return $this->{$methodForCall}(); + } + + return null; + } + + /** + * @param string $pdfBackend + * @return $this + */ + public function setPdfBackend($pdfBackend) + { + $this->pdfBackend = $pdfBackend; + return $this; + } + + /** + * @return string + */ + public function getPdfBackend() + { + return $this->pdfBackend; + } + + /** + * @param string $pdflibLicense + * @return $this + */ + public function setPdflibLicense($pdflibLicense) + { + $this->pdflibLicense = $pdflibLicense; + return $this; + } + + /** + * @return string + */ + public function getPdflibLicense() + { + return $this->pdflibLicense; + } + + /** + * @param array|string $chroot + * @return $this + */ + public function setChroot($chroot, $delimiter = ',') + { + if (is_string($chroot)) { + $this->chroot = explode($delimiter, $chroot); + } elseif (is_array($chroot)) { + $this->chroot = $chroot; + } + return $this; + } + + /** + * @return array + */ + public function getAllowedProtocols() + { + return $this->allowedProtocols; + } + + /** + * @param array $allowedProtocols The protocols to allow, as an array + * formatted as ["protocol://" => ["rules" => [callable]], ...] + * or ["protocol://", ...] + * + * @return $this + */ + public function setAllowedProtocols(array $allowedProtocols) + { + $protocols = []; + foreach ($allowedProtocols as $protocol => $config) { + if (is_string($protocol)) { + $protocols[$protocol] = []; + if (is_array($config)) { + $protocols[$protocol] = $config; + } + } elseif (is_string($config)) { + $protocols[$config] = []; + } + } + $this->allowedProtocols = []; + foreach ($protocols as $protocol => $config) { + $this->addAllowedProtocol($protocol, ...($config["rules"] ?? [])); + } + return $this; + } + + /** + * Adds a new protocol to the allowed protocols collection + * + * @param string $protocol The scheme to add (e.g. "http://") + * @param callable $rule A callable that validates the protocol + * @return $this + */ + public function addAllowedProtocol(string $protocol, callable ...$rules) + { + $protocol = strtolower($protocol); + if (empty($rules)) { + $rules = []; + switch ($protocol) { + case "data://": + break; + case "file://": + $rules[] = [$this, "validateLocalUri"]; + break; + case "http://": + case "https://": + $rules[] = [$this, "validateRemoteUri"]; + break; + case "phar://": + $rules[] = [$this, "validatePharUri"]; + break; + } + } + $this->allowedProtocols[$protocol] = ["rules" => $rules]; + return $this; + } + + /** + * @return array + */ + public function getArtifactPathValidation() + { + return $this->artifactPathValidation; + } + + /** + * @param callable $validator + * @return $this + */ + public function setArtifactPathValidation($validator) + { + $this->artifactPathValidation = $validator; + return $this; + } + + /** + * @return array + */ + public function getChroot() + { + $chroot = []; + if (is_array($this->chroot)) { + $chroot = $this->chroot; + } + return $chroot; + } + + /** + * @param boolean $debugCss + * @return $this + */ + public function setDebugCss($debugCss) + { + $this->debugCss = $debugCss; + return $this; + } + + /** + * @return boolean + */ + public function getDebugCss() + { + return $this->debugCss; + } + + /** + * @param boolean $debugKeepTemp + * @return $this + */ + public function setDebugKeepTemp($debugKeepTemp) + { + $this->debugKeepTemp = $debugKeepTemp; + return $this; + } + + /** + * @return boolean + */ + public function getDebugKeepTemp() + { + return $this->debugKeepTemp; + } + + /** + * @param boolean $debugLayout + * @return $this + */ + public function setDebugLayout($debugLayout) + { + $this->debugLayout = $debugLayout; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayout() + { + return $this->debugLayout; + } + + /** + * @param boolean $debugLayoutBlocks + * @return $this + */ + public function setDebugLayoutBlocks($debugLayoutBlocks) + { + $this->debugLayoutBlocks = $debugLayoutBlocks; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayoutBlocks() + { + return $this->debugLayoutBlocks; + } + + /** + * @param boolean $debugLayoutInline + * @return $this + */ + public function setDebugLayoutInline($debugLayoutInline) + { + $this->debugLayoutInline = $debugLayoutInline; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayoutInline() + { + return $this->debugLayoutInline; + } + + /** + * @param boolean $debugLayoutLines + * @return $this + */ + public function setDebugLayoutLines($debugLayoutLines) + { + $this->debugLayoutLines = $debugLayoutLines; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayoutLines() + { + return $this->debugLayoutLines; + } + + /** + * @param boolean $debugLayoutPaddingBox + * @return $this + */ + public function setDebugLayoutPaddingBox($debugLayoutPaddingBox) + { + $this->debugLayoutPaddingBox = $debugLayoutPaddingBox; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayoutPaddingBox() + { + return $this->debugLayoutPaddingBox; + } + + /** + * @param boolean $debugPng + * @return $this + */ + public function setDebugPng($debugPng) + { + $this->debugPng = $debugPng; + return $this; + } + + /** + * @return boolean + */ + public function getDebugPng() + { + return $this->debugPng; + } + + /** + * @param string $defaultFont + * @return $this + */ + public function setDefaultFont($defaultFont) + { + if (!($defaultFont === null || trim($defaultFont) === "")) { + $this->defaultFont = $defaultFont; + } else { + $this->defaultFont = "serif"; + } + return $this; + } + + /** + * @return string + */ + public function getDefaultFont() + { + return $this->defaultFont; + } + + /** + * @param string $defaultMediaType + * @return $this + */ + public function setDefaultMediaType($defaultMediaType) + { + $this->defaultMediaType = $defaultMediaType; + return $this; + } + + /** + * @return string + */ + public function getDefaultMediaType() + { + return $this->defaultMediaType; + } + + /** + * @param string|float[] $defaultPaperSize + * @return $this + */ + public function setDefaultPaperSize($defaultPaperSize): self + { + $this->defaultPaperSize = $defaultPaperSize; + return $this; + } + + /** + * @param string $defaultPaperOrientation + * @return $this + */ + public function setDefaultPaperOrientation(string $defaultPaperOrientation): self + { + $this->defaultPaperOrientation = $defaultPaperOrientation; + return $this; + } + + /** + * @return string|float[] + */ + public function getDefaultPaperSize() + { + return $this->defaultPaperSize; + } + + /** + * @return string + */ + public function getDefaultPaperOrientation(): string + { + return $this->defaultPaperOrientation; + } + + /** + * @param int $dpi + * @return $this + */ + public function setDpi($dpi) + { + $this->dpi = $dpi; + return $this; + } + + /** + * @return int + */ + public function getDpi() + { + return $this->dpi; + } + + /** + * @param string $fontCache + * @return $this + */ + public function setFontCache($fontCache) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($fontCache, "fontCache") === true) { + $this->fontCache = $fontCache; + } + return $this; + } + + /** + * @return string + */ + public function getFontCache() + { + return $this->fontCache; + } + + /** + * @param string $fontDir + * @return $this + */ + public function setFontDir($fontDir) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($fontDir, "fontDir") === true) { + $this->fontDir = $fontDir; + } + return $this; + } + + /** + * @return string + */ + public function getFontDir() + { + return $this->fontDir; + } + + /** + * @param float $fontHeightRatio + * @return $this + */ + public function setFontHeightRatio($fontHeightRatio) + { + $this->fontHeightRatio = $fontHeightRatio; + return $this; + } + + /** + * @return float + */ + public function getFontHeightRatio() + { + return $this->fontHeightRatio; + } + + /** + * @param boolean $isFontSubsettingEnabled + * @return $this + */ + public function setIsFontSubsettingEnabled($isFontSubsettingEnabled) + { + $this->isFontSubsettingEnabled = $isFontSubsettingEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsFontSubsettingEnabled() + { + return $this->isFontSubsettingEnabled; + } + + /** + * @return boolean + */ + public function isFontSubsettingEnabled() + { + return $this->getIsFontSubsettingEnabled(); + } + + /** + * @deprecated + * @param boolean $isHtml5ParserEnabled + * @return $this + */ + public function setIsHtml5ParserEnabled($isHtml5ParserEnabled) + { + $this->isHtml5ParserEnabled = $isHtml5ParserEnabled; + return $this; + } + + /** + * @deprecated + * @return boolean + */ + public function getIsHtml5ParserEnabled() + { + return $this->isHtml5ParserEnabled; + } + + /** + * @deprecated + * @return boolean + */ + public function isHtml5ParserEnabled() + { + return $this->getIsHtml5ParserEnabled(); + } + + /** + * @param boolean $isJavascriptEnabled + * @return $this + */ + public function setIsJavascriptEnabled($isJavascriptEnabled) + { + $this->isJavascriptEnabled = $isJavascriptEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsJavascriptEnabled() + { + return $this->isJavascriptEnabled; + } + + /** + * @return boolean + */ + public function isJavascriptEnabled() + { + return $this->getIsJavascriptEnabled(); + } + + /** + * @param boolean $isPhpEnabled + * @return $this + */ + public function setIsPhpEnabled($isPhpEnabled) + { + $this->isPhpEnabled = $isPhpEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsPhpEnabled() + { + return $this->isPhpEnabled; + } + + /** + * @return boolean + */ + public function isPhpEnabled() + { + return $this->getIsPhpEnabled(); + } + + /** + * @param boolean $isRemoteEnabled + * @return $this + */ + public function setIsRemoteEnabled($isRemoteEnabled) + { + $this->isRemoteEnabled = $isRemoteEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsRemoteEnabled() + { + return $this->isRemoteEnabled; + } + + /** + * @return boolean + */ + public function isRemoteEnabled() + { + return $this->getIsRemoteEnabled(); + } + + /** + * @param array|null $allowedRemoteHosts + * @return $this + */ + public function setAllowedRemoteHosts($allowedRemoteHosts) + { + if (is_array($allowedRemoteHosts)) { + // Set hosts to lowercase + foreach ($allowedRemoteHosts as &$host) { + $host = mb_strtolower($host, "UTF-8"); + } + + unset($host); + } + + $this->allowedRemoteHosts = $allowedRemoteHosts; + return $this; + } + + /** + * @return array|null + */ + public function getAllowedRemoteHosts() + { + return $this->allowedRemoteHosts; + } + + /** + * @param boolean $isRemoteEnabled + * @return $this + */ + public function setIsPdfAEnabled($isPdfAEnabled) + { + $this->isPdfAEnabled = $isPdfAEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsPdfAEnabled() + { + return $this->isPdfAEnabled; + } + + /** + * @return boolean + */ + public function isPdfAEnabled() + { + return $this->getIsPdfAEnabled(); + } + + /** + * @param string $logOutputFile + * @return $this + */ + public function setLogOutputFile($logOutputFile) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($logOutputFile, "logOutputFile") === true) { + $this->logOutputFile = $logOutputFile; + } + return $this; + } + + /** + * @return string + */ + public function getLogOutputFile() + { + return $this->logOutputFile; + } + + /** + * @param string $tempDir + * @return $this + */ + public function setTempDir($tempDir) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($tempDir, "tempDir") === true) { + $this->tempDir = $tempDir; + } + return $this; + } + + /** + * @return string + */ + public function getTempDir() + { + return $this->tempDir; + } + + /** + * @param string $rootDir + * @return $this + */ + public function setRootDir($rootDir) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($rootDir, "rootDir") === true) { + $this->rootDir = $rootDir; + } + return $this; + } + + /** + * @return string + */ + public function getRootDir() + { + return $this->rootDir; + } + + /** + * Sets the HTTP context + * + * @param resource|array $httpContext + * @return $this + */ + public function setHttpContext($httpContext) + { + $this->httpContext = is_array($httpContext) ? stream_context_create($httpContext) : $httpContext; + return $this; + } + + /** + * Returns the HTTP context + * + * @return resource + */ + public function getHttpContext() + { + return $this->httpContext; + } + + + public function validateArtifactPath(?string $path, string $option) + { + if ($path === null) { + return true; + } + $parsed_uri = parse_url($path); + if ($parsed_uri === false || (array_key_exists("scheme", $parsed_uri) && strtolower($parsed_uri["scheme"]) === "phar")) { + return false; + } + return true; + } + + public function validateLocalUri(string $uri) + { + if ($uri === null || strlen($uri) === 0) { + return [false, "The URI must not be empty."]; + } + + $realfile = realpath(str_replace("file://", "", $uri)); + + $dirs = $this->chroot; + $dirs[] = $this->rootDir; + $chrootValid = false; + foreach ($dirs as $chrootPath) { + $chrootPath = realpath($chrootPath); + if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) { + $chrootValid = true; + break; + } + } + if ($chrootValid !== true) { + return [false, "Permission denied. The file could not be found under the paths specified by Options::chroot."]; + } + + if (!$realfile) { + return [false, "File not found."]; + } + + return [true, null]; + } + + public function validatePharUri(string $uri) + { + if ($uri === null || strlen($uri) === 0) { + return [false, "The URI must not be empty."]; + } + + $file = substr(substr($uri, 0, strpos($uri, ".phar") + 5), 7); + return $this->validateLocalUri($file); + } + + public function validateRemoteUri(string $uri) + { + if ($uri === null || strlen($uri) === 0) { + return [false, "The URI must not be empty."]; + } + + if (!$this->isRemoteEnabled) { + return [false, "Remote file requested, but remote file download is disabled."]; + } + + if (is_array($this->allowedRemoteHosts) && count($this->allowedRemoteHosts) > 0) { + $host = parse_url($uri, PHP_URL_HOST); + $host = mb_strtolower($host, "UTF-8"); + + if (!in_array($host, $this->allowedRemoteHosts, true)) { + return [false, "Remote host is not in allowed list: " . $host]; + } + } + + return [true, null]; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/PhpEvaluator.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/PhpEvaluator.php new file mode 100644 index 0000000..4a46555 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/PhpEvaluator.php @@ -0,0 +1,62 @@ +_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); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Absolute.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Absolute.php new file mode 100644 index 0000000..2df9a74 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Absolute.php @@ -0,0 +1,128 @@ +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); + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/AbstractPositioner.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/AbstractPositioner.php new file mode 100644 index 0000000..a75c09f --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/AbstractPositioner.php @@ -0,0 +1,48 @@ +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); + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Block.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Block.php new file mode 100644 index 0000000..e6f65ea --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Block.php @@ -0,0 +1,40 @@ +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); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Fixed.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Fixed.php new file mode 100644 index 0000000..13eb9e9 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Fixed.php @@ -0,0 +1,92 @@ +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); + } + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Inline.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Inline.php new file mode 100644 index 0000000..f1036e9 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/Inline.php @@ -0,0 +1,56 @@ +find_block_parent(); + $cb = $frame->get_containing_block(); + + if (!$block) { + // FIXME: An inline frame without block parent should not be + // possible, but this can occur currently when the body is styled + // with `display: inline !important;` or `display: inline-block !important;` + $frame->set_position($cb["x"], $cb["y"]); + return; + } + + $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); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/ListBullet.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/ListBullet.php new file mode 100644 index 0000000..081d594 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/ListBullet.php @@ -0,0 +1,42 @@ +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); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/NullPositioner.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/NullPositioner.php new file mode 100644 index 0000000..6ad425c --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/NullPositioner.php @@ -0,0 +1,26 @@ +get_cellmap(); + $frame->set_position($cellmap->get_frame_position($frame)); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/TableRow.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/TableRow.php new file mode 100644 index 0000000..79c0fcf --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Positioner/TableRow.php @@ -0,0 +1,34 @@ +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); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer.php new file mode 100644 index 0000000..15391fb --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer.php @@ -0,0 +1,307 @@ +_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(); + + [$x, $y] = $frame->get_padding_box(); + [$originX, $originY] = $style->transform_origin; + $w = (float) $style->length_in_pt($style->width); + $h = (float) $style->length_in_pt($style->height); + + foreach ($transformList as $transform) { + [$function, $values] = $transform; + + if ($function === "matrix") { + $function = "transform"; + } elseif ($function === "translate") { + $values[0] = $style->length_in_pt($values[0], $w); + $values[1] = $style->length_in_pt($values[1], $h); + } + + $values[] = $x + $style->length_in_pt($originX, $w); + $values[] = $y + $style->length_in_pt($originY, $h); + + 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": + $this->_render_frame("table-row", $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 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": + $this->_renderers[$type] = new TableRow($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); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/AbstractRenderer.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/AbstractRenderer.php new file mode 100644 index 0000000..293bed2 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/AbstractRenderer.php @@ -0,0 +1,1298 @@ +_dompdf = $dompdf; + $this->_canvas = $dompdf->getCanvas(); + } + + /** + * Render a frame. + * + * Specialized in child classes + * + * @param Frame $frame The frame to render + */ + abstract function render(Frame $frame); + + /** + * @param Frame $frame + * @param float[] $border_box + */ + protected function _render_background(Frame $frame, array $border_box): void + { + $style = $frame->get_style(); + $color = $style->background_color; + $image = $style->background_image; + [$x, $y, $w, $h] = $border_box; + + if ($color === "transparent" && $image === "none") { + return; + } + + if ($style->has_border_radius()) { + [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box); + $this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl); + } + + if ($color !== "transparent") { + $this->_canvas->filled_rectangle($x, $y, $w, $h, $color); + } + + if ($image !== "none") { + $this->_background_image($image, $x, $y, $w, $h, $style); + } + + if ($style->has_border_radius()) { + $this->_canvas->clipping_end(); + } + } + + /** + * @param Frame $frame + * @param float[] $border_box + * @param string $corner_style + */ + protected function _render_border(Frame $frame, array $border_box, string $corner_style = "bevel"): void + { + $style = $frame->get_style(); + $bp = $style->get_border_properties(); + [$x, $y, $w, $h] = $border_box; + [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box); + + // Short-cut: If all the borders are "solid" with the same color and + // style, and no radius, we'd better draw a rectangle + if ($bp["top"]["style"] === "solid" && + $bp["top"] === $bp["right"] && + $bp["right"] === $bp["bottom"] && + $bp["bottom"] === $bp["left"] && + !$style->has_border_radius() + ) { + $props = $bp["top"]; + if ($props["color"] === "transparent" || $props["width"] <= 0) { + return; + } + + $width = (float)$style->length_in_pt($props["width"]); + $this->_canvas->rectangle($x + $width / 2, $y + $width / 2, $w - $width, $h - $width, $props["color"], $width); + return; + } + + // Do it the long way + $widths = [ + (float)$style->length_in_pt($bp["top"]["width"]), + (float)$style->length_in_pt($bp["right"]["width"]), + (float)$style->length_in_pt($bp["bottom"]["width"]), + (float)$style->length_in_pt($bp["left"]["width"]) + ]; + + foreach ($bp as $side => $props) { + if ($props["style"] === "none" || + $props["style"] === "hidden" || + $props["color"] === "transparent" || + $props["width"] <= 0 + ) { + continue; + } + + [$x, $y, $w, $h] = $border_box; + $method = "_border_" . $props["style"]; + + switch ($side) { + case "top": + $length = $w; + $r1 = $tl; + $r2 = $tr; + break; + + case "bottom": + $length = $w; + $y += $h; + $r1 = $bl; + $r2 = $br; + break; + + case "left": + $length = $h; + $r1 = $tl; + $r2 = $bl; + break; + + case "right": + $length = $h; + $x += $w; + $r1 = $tr; + $r2 = $br; + break; + + default: + break; + } + + // draw rounded corners + $this->$method($x, $y, $length, $props["color"], $widths, $side, $corner_style, $r1, $r2); + } + } + + /** + * @param Frame $frame + * @param float[] $border_box + * @param string $corner_style + */ + protected function _render_outline(Frame $frame, array $border_box, string $corner_style = "bevel"): void + { + $style = $frame->get_style(); + + $width = $style->outline_width; + $outline_style = $style->outline_style; + $color = $style->outline_color; + + if ($outline_style === "none" || $color === "transparent" || $width <= 0) { + return; + } + + $offset = $style->outline_offset; + + [$x, $y, $w, $h] = $border_box; + $d = $width + $offset; + $outline_box = [$x - $d, $y - $d, $w + $d * 2, $h + $d * 2]; + [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $outline_box); + + $x -= $offset; + $y -= $offset; + $w += $offset * 2; + $h += $offset * 2; + + // For a simple outline, we can draw a rectangle + if ($outline_style === "solid" && !$style->has_border_radius()) { + $x -= $width / 2; + $y -= $width / 2; + $w += $width; + $h += $width; + + $this->_canvas->rectangle($x, $y, $w, $h, $color, $width); + return; + } + + $x -= $width; + $y -= $width; + $w += $width * 2; + $h += $width * 2; + + $method = "_border_" . $outline_style; + $widths = array_fill(0, 4, $width); + $sides = ["top", "right", "left", "bottom"]; + + foreach ($sides as $side) { + switch ($side) { + case "top": + $length = $w; + $side_x = $x; + $side_y = $y; + $r1 = $tl; + $r2 = $tr; + break; + + case "bottom": + $length = $w; + $side_x = $x; + $side_y = $y + $h; + $r1 = $bl; + $r2 = $br; + break; + + case "left": + $length = $h; + $side_x = $x; + $side_y = $y; + $r1 = $tl; + $r2 = $bl; + break; + + case "right": + $length = $h; + $side_x = $x + $w; + $side_y = $y; + $r1 = $tr; + $r2 = $br; + break; + + default: + break; + } + + $this->$method($side_x, $side_y, $length, $color, $widths, $side, $corner_style, $r1, $r2); + } + } + + /** + * Render a background image over a rectangular area + * + * @param string $url The background image to load + * @param float $x The left edge of the rectangular area + * @param float $y The top edge of the rectangular area + * @param float $width The width of the rectangular area + * @param float $height The height of the rectangular area + * @param Style $style The associated Style object + * + * @throws \Exception + */ + protected function _background_image(string $url, float $x, float $y, float $width, float $height, Style $style): void + { + if (!function_exists("imagecreatetruecolor")) { + throw new \Exception("The PHP GD extension is required, but is not installed."); + } + + $sheet = $style->get_stylesheet(); + + // Skip degenerate cases + if ($width == 0 || $height == 0) { + return; + } + + $box_width = $width; + $box_height = $height; + + //debugpng + if ($this->_dompdf->getOptions()->getDebugPng()) { + print '[_background_image ' . $url . ']'; + } + + list($img, $type, /*$msg*/) = Cache::resolve_url( + $url, + $sheet->get_protocol(), + $sheet->get_host(), + $sheet->get_base_path(), + $this->_dompdf->getOptions() + ); + + // Bail if the image is no good + if (Cache::is_broken($img)) { + return; + } + + //Try to optimize away reading and composing of same background multiple times + //Postponing read with imagecreatefrom ...() + //final composition parameters and name not known yet + //Therefore read dimension directly from file, instead of creating gd object first. + //$img_w = imagesx($src); $img_h = imagesy($src); + + list($img_w, $img_h) = Helpers::dompdf_getimagesize($img, $this->_dompdf->getHttpContext()); + if ($img_w == 0 || $img_h == 0) { + return; + } + + // save for later check if file needs to be resized. + $org_img_w = $img_w; + $org_img_h = $img_h; + + $repeat = $style->background_repeat; + $dpi = $this->_dompdf->getOptions()->getDpi(); + + //Increase background resolution and dependent box size according to image resolution to be placed in + //Then image can be copied in without resize + $bg_width = round((float)($width * $dpi) / 72); + $bg_height = round((float)($height * $dpi) / 72); + + list($img_w, $img_h) = $this->_resize_background_image( + $img_w, + $img_h, + $bg_width, + $bg_height, + $style->background_size, + $dpi + ); + //Need %bg_x, $bg_y as background pos, where img starts, converted to pixel + + list($bg_x, $bg_y) = $style->background_position; + + if (Helpers::is_percent($bg_x)) { + // The point $bg_x % from the left edge of the image is placed + // $bg_x % from the left edge of the background rectangle + $p = ((float)$bg_x) / 100.0; + $x1 = $p * $img_w; + $x2 = $p * $bg_width; + + $bg_x = $x2 - $x1; + } else { + $bg_x = (float)($style->length_in_pt($bg_x) * $dpi) / 72; + } + + $bg_x = round($bg_x + (float)$style->length_in_pt($style->border_left_width) * $dpi / 72); + + if (Helpers::is_percent($bg_y)) { + // The point $bg_y % from the left edge of the image is placed + // $bg_y % from the left edge of the background rectangle + $p = ((float)$bg_y) / 100.0; + $y1 = $p * $img_h; + $y2 = $p * $bg_height; + + $bg_y = $y2 - $y1; + } else { + $bg_y = (float)($style->length_in_pt($bg_y) * $dpi) / 72; + } + + $bg_y = round($bg_y + (float)$style->length_in_pt($style->border_top_width) * $dpi / 72); + + //clip background to the image area on partial repeat. Nothing to do if img off area + //On repeat, normalize start position to the tile at immediate left/top or 0/0 of area + //On no repeat with positive offset: move size/start to have offset==0 + //Handle x/y Dimensions separately + + if ($repeat !== "repeat" && $repeat !== "repeat-x") { + //No repeat x + if ($bg_x < 0) { + $bg_width = $img_w + $bg_x; + } else { + $x += ($bg_x * 72) / $dpi; + $bg_width = $bg_width - $bg_x; + if ($bg_width > $img_w) { + $bg_width = $img_w; + } + $bg_x = 0; + } + + if ($bg_width <= 0) { + return; + } + + $width = (float)($bg_width * 72) / $dpi; + } else { + //repeat x + if ($bg_x < 0) { + $bg_x = -((-$bg_x) % $img_w); + } else { + $bg_x = $bg_x % $img_w; + if ($bg_x > 0) { + $bg_x -= $img_w; + } + } + } + + if ($repeat !== "repeat" && $repeat !== "repeat-y") { + //no repeat y + if ($bg_y < 0) { + $bg_height = $img_h + $bg_y; + } else { + $y += ($bg_y * 72) / $dpi; + $bg_height = $bg_height - $bg_y; + if ($bg_height > $img_h) { + $bg_height = $img_h; + } + $bg_y = 0; + } + if ($bg_height <= 0) { + return; + } + $height = (float)($bg_height * 72) / $dpi; + } else { + //repeat y + if ($bg_y < 0) { + $bg_y = -((-$bg_y) % $img_h); + } else { + $bg_y = $bg_y % $img_h; + if ($bg_y > 0) { + $bg_y -= $img_h; + } + } + } + + //Optimization, if repeat has no effect + if ($repeat === "repeat" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height) { + $repeat = "repeat-x"; + } + + if ($repeat === "repeat" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width) { + $repeat = "repeat-y"; + } + + if (($repeat === "repeat-x" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width) || + ($repeat === "repeat-y" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height) + ) { + $repeat = "no-repeat"; + } + + // Avoid rendering identical background-image variants multiple times + // This is not dependent of background color of box! .'_'.(is_array($bg_color) ? $bg_color["hex"] : $bg_color) + // Note: Here, bg_* are the start values, not end values after going through the tile loops! + + $key = implode("_", [$bg_width, $bg_height, $img_w, $img_h, $bg_x, $bg_y, $repeat]); + // FIXME: This will fail when a file with that exact name exists in the + // same directory, included in the document as regular image + $cpdfKey = $img . "_" . $key; + $tmpFile = Cache::getTempImage($img, $key); + $cached = ($this->_canvas instanceof CPDF && $this->_canvas->get_cpdf()->image_iscached($cpdfKey)) + || ($tmpFile !== null && file_exists($tmpFile)); + + if (!$cached) { + // img: image url string + // img_w, img_h: original image size in px + // width, height: box size in pt + // bg_width, bg_height: box size in px + // x, y: left/top edge of box on page in pt + // start_x, start_y: placement of image relative to pattern + // $repeat: repeat mode + // $bg: GD object of result image + // $src: GD object of original image + + // Create a new image to fit over the background rectangle + $bg = imagecreatetruecolor($bg_width, $bg_height); + $cpdfFromGd = true; + + switch (strtolower($type)) { + case "png": + $cpdfFromGd = false; + imagesavealpha($bg, true); + imagealphablending($bg, false); + $src = @imagecreatefrompng($img); + break; + + case "jpeg": + $src = @imagecreatefromjpeg($img); + break; + + case "webp": + $src = @imagecreatefromwebp($img); + break; + + case "gif": + $src = @imagecreatefromgif($img); + break; + + case "bmp": + $src = @Helpers::imagecreatefrombmp($img); + break; + + default: + return; // Unsupported image type + } + + if ($src == null) { + return; + } + + if ($img_w != $org_img_w || $img_h != $org_img_h) { + $newSrc = imagecreatetruecolor($img_w, $img_h); + imagealphablending($newSrc, false); + imagesavealpha($newSrc, true); + imagecopyresampled($newSrc, $src, 0, 0, 0, 0, $img_w, $img_h, imagesx($src), imagesy($src)); + if (PHP_MAJOR_VERSION < 8) { + imagedestroy($src); + } + $src = $newSrc; + } + + if ($src == null) { + return; + } + + //Background color if box is not relevant here + //Non transparent image: box clipped to real size. Background non relevant. + //Transparent image: The image controls the transparency and lets shine through whatever background. + //However on transparent image preset the composed image with the transparency color, + //to keep the transparency when copying over the non transparent parts of the tiles. + $ti = imagecolortransparent($src); + $palletsize = imagecolorstotal($src); + + if ($ti >= 0 && $ti < $palletsize) { + $tc = imagecolorsforindex($src, $ti); + $ti = imagecolorallocate($bg, $tc['red'], $tc['green'], $tc['blue']); + imagefill($bg, 0, 0, $ti); + imagecolortransparent($bg, $ti); + } + + //This has only an effect for the non repeatable dimension. + //compute start of src and dest coordinates of the single copy + if ($bg_x < 0) { + $dst_x = 0; + $src_x = -$bg_x; + } else { + $src_x = 0; + $dst_x = $bg_x; + } + + if ($bg_y < 0) { + $dst_y = 0; + $src_y = -$bg_y; + } else { + $src_y = 0; + $dst_y = $bg_y; + } + + //For historical reasons exchange meanings of variables: + //start_* will be the start values, while bg_* will be the temporary start values in the loops + $start_x = $bg_x; + $start_y = $bg_y; + + // Copy regions from the source image to the background + if ($repeat === "no-repeat") { + // Simply place the image on the background + imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $img_h); + + } elseif ($repeat === "repeat-x") { + for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) { + if ($bg_x < 0) { + $dst_x = 0; + $src_x = -$bg_x; + $w = $img_w + $bg_x; + } else { + $dst_x = $bg_x; + $src_x = 0; + $w = $img_w; + } + imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $img_h); + } + } elseif ($repeat === "repeat-y") { + + for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) { + if ($bg_y < 0) { + $dst_y = 0; + $src_y = -$bg_y; + $h = $img_h + $bg_y; + } else { + $dst_y = $bg_y; + $src_y = 0; + $h = $img_h; + } + imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $h); + } + } elseif ($repeat === "repeat") { + for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) { + for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) { + if ($bg_x < 0) { + $dst_x = 0; + $src_x = -$bg_x; + $w = $img_w + $bg_x; + } else { + $dst_x = $bg_x; + $src_x = 0; + $w = $img_w; + } + + if ($bg_y < 0) { + $dst_y = 0; + $src_y = -$bg_y; + $h = $img_h + $bg_y; + } else { + $dst_y = $bg_y; + $src_y = 0; + $h = $img_h; + } + imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $h); + } + } + } else { + print 'Unknown repeat!'; + } + + if (PHP_MAJOR_VERSION < 8) { + imagedestroy($src); + } + + if ($cpdfFromGd && $this->_canvas instanceof CPDF) { + // Skip writing temp file as the GD object is added directly + } else { + $tmpDir = $this->_dompdf->getOptions()->getTempDir(); + $tmpName = @tempnam($tmpDir, "bg_dompdf_img_"); + @unlink($tmpName); + $tmpFile = "$tmpName.png"; + + imagepng($bg, $tmpFile); + if (PHP_MAJOR_VERSION < 8) { + imagedestroy($bg); + } + + Cache::addTempImage($img, $tmpFile, $key); + } + } else { + $bg = null; + $cpdfFromGd = $tmpFile === null; + } + + if ($this->_dompdf->getOptions()->getDebugPng()) { + print '[_background_image ' . $tmpFile . ']'; + } + + $this->_canvas->clipping_rectangle($x, $y, $box_width, $box_height); + + // When using cpdf and optimization to direct png creation from gd object is available, + // don't create temp file, but place gd object directly into the pdf + if ($cpdfFromGd && $this->_canvas instanceof CPDF) { + // Note: CPDF_Adapter image converts y position + $this->_canvas->get_cpdf()->addImagePng($bg, $cpdfKey, $x, $this->_canvas->get_height() - $y - $height, $width, $height); + + if (isset($bg) && PHP_MAJOR_VERSION < 8) { + imagedestroy($bg); + } + } else { + $this->_canvas->image($tmpFile, $x, $y, $width, $height); + } + + $this->_canvas->clipping_end(); + } + + /** + * @param float $img_width + * @param float $img_height + * @param float $container_width + * @param float $container_height + * @param array|string $bg_resize + * @param int $dpi + * + * @return float[] + */ + protected function _resize_background_image( + float $img_width, + float $img_height, + float $container_width, + float $container_height, + $bg_resize, + int $dpi + ): array { + // We got two some specific numbers and/or auto definitions + if (is_array($bg_resize)) { + $is_auto_width = $bg_resize[0] === 'auto'; + if ($is_auto_width) { + $new_img_width = $img_width; + } else { + $new_img_width = $bg_resize[0]; + if (Helpers::is_percent($new_img_width)) { + $new_img_width = round(($container_width / 100) * (float)$new_img_width); + } else { + $new_img_width = round($new_img_width * $dpi / 72); + } + } + + $is_auto_height = $bg_resize[1] === 'auto'; + if ($is_auto_height) { + $new_img_height = $img_height; + } else { + $new_img_height = $bg_resize[1]; + if (Helpers::is_percent($new_img_height)) { + $new_img_height = round(($container_height / 100) * (float)$new_img_height); + } else { + $new_img_height = round($new_img_height * $dpi / 72); + } + } + + // if one of both was set to auto the other one needs to scale proportionally + if ($is_auto_width !== $is_auto_height) { + if ($is_auto_height) { + $new_img_height = round($new_img_width * ($img_height / $img_width)); + } else { + $new_img_width = round($new_img_height * ($img_width / $img_height)); + } + } + } else { + $container_ratio = $container_height / $container_width; + + if ($bg_resize === 'cover' || $bg_resize === 'contain') { + $img_ratio = $img_height / $img_width; + + if ( + ($bg_resize === 'cover' && $container_ratio > $img_ratio) || + ($bg_resize === 'contain' && $container_ratio < $img_ratio) + ) { + $new_img_height = $container_height; + $new_img_width = round($container_height / $img_ratio); + } else { + $new_img_width = $container_width; + $new_img_height = round($container_width * $img_ratio); + } + } else { + $new_img_width = $img_width; + $new_img_height = $img_height; + } + } + + return [$new_img_width, $new_img_height]; + } + + // Border rendering functions + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_dotted($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + $this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "dotted", $r1, $r2); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_dashed($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + $this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "dashed", $r1, $r2); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_solid($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + $this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "solid", $r1, $r2); + } + + /** + * @param string $side + * @param float $ratio + * @param float $top + * @param float $right + * @param float $bottom + * @param float $left + * @param float $x + * @param float $y + * @param float $length + * @param float $r1 + * @param float $r2 + */ + protected function _apply_ratio($side, $ratio, $top, $right, $bottom, $left, &$x, &$y, &$length, &$r1, &$r2) + { + switch ($side) { + case "top": + $r1 -= $left * $ratio; + $r2 -= $right * $ratio; + $x += $left * $ratio; + $y += $top * $ratio; + $length -= $left * $ratio + $right * $ratio; + break; + + case "bottom": + $r1 -= $right * $ratio; + $r2 -= $left * $ratio; + $x += $left * $ratio; + $y -= $bottom * $ratio; + $length -= $left * $ratio + $right * $ratio; + break; + + case "left": + $r1 -= $top * $ratio; + $r2 -= $bottom * $ratio; + $x += $left * $ratio; + $y += $top * $ratio; + $length -= $top * $ratio + $bottom * $ratio; + break; + + case "right": + $r1 -= $bottom * $ratio; + $r2 -= $top * $ratio; + $x -= $right * $ratio; + $y += $top * $ratio; + $length -= $top * $ratio + $bottom * $ratio; + break; + + default: + return; + } + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_double($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + list($top, $right, $bottom, $left) = $widths; + + $third_widths = [$top / 3, $right / 3, $bottom / 3, $left / 3]; + + // draw the outer border + $this->_border_solid($x, $y, $length, $color, $third_widths, $side, $corner_style, $r1, $r2); + + $this->_apply_ratio($side, 2 / 3, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2); + + $this->_border_solid($x, $y, $length, $color, $third_widths, $side, $corner_style, $r1, $r2); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_groove($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + list($top, $right, $bottom, $left) = $widths; + + $half_widths = [$top / 2, $right / 2, $bottom / 2, $left / 2]; + + $this->_border_inset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2); + + $this->_apply_ratio($side, 0.5, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2); + + $this->_border_outset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_ridge($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + list($top, $right, $bottom, $left) = $widths; + + $half_widths = [$top / 2, $right / 2, $bottom / 2, $left / 2]; + + $this->_border_outset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2); + + $this->_apply_ratio($side, 0.5, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2); + + $this->_border_inset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2); + } + + /** + * @param $c + * @return mixed + */ + protected function _tint($c) + { + if (!is_numeric($c)) { + return $c; + } + + return min(1, $c + 0.16); + } + + /** + * @param $c + * @return mixed + */ + protected function _shade($c) + { + if (!is_numeric($c)) { + return $c; + } + + return max(0, $c - 0.33); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_inset($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + switch ($side) { + case "top": + case "left": + $shade = array_map([$this, "_shade"], $color); + $this->_border_solid($x, $y, $length, $shade, $widths, $side, $corner_style, $r1, $r2); + break; + + case "bottom": + case "right": + $tint = array_map([$this, "_tint"], $color); + $this->_border_solid($x, $y, $length, $tint, $widths, $side, $corner_style, $r1, $r2); + break; + + default: + return; + } + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_outset($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + switch ($side) { + case "top": + case "left": + $tint = array_map([$this, "_tint"], $color); + $this->_border_solid($x, $y, $length, $tint, $widths, $side, $corner_style, $r1, $r2); + break; + + case "bottom": + case "right": + $shade = array_map([$this, "_shade"], $color); + $this->_border_solid($x, $y, $length, $shade, $widths, $side, $corner_style, $r1, $r2); + break; + + default: + return; + } + } + + /** + * Get the dash pattern and cap style for the given border style, width, and + * line length. + * + * The base pattern is adjusted so that it fits the given line length + * symmetrically. + * + * @param string $style + * @param float $width + * @param float $length + * + * @return array + */ + protected function dashPattern(string $style, float $width, float $length): array + { + if ($style === "dashed") { + $w = 3 * $width; + + if ($length < $w) { + $s = $w; + } else { + // Scale dashes and gaps + $r = round($length / $w); + $r = $r % 2 === 0 ? $r + 1 : $r; + $s = $length / $r; + } + + return [[$s], "butt"]; + } + + if ($style === "dotted") { + // Draw circles along the line + // Round caps extend outwards by half line width, so a zero dash + // width results in a circle + $gap = $width <= 1 ? 2 : 1; + $w = ($gap + 1) * $width; + + if ($length < $w) { + $s = $w; + } else { + // Only scale gaps + $l = $length - $width; + $r = max(round($l / $w), 1); + $s = $l / $r; + } + + return [[0, $s], "round"]; + } + + return [[], "butt"]; + } + + /** + * Draws a solid, dotted, or dashed line, observing the border radius + * + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param string $pattern_name + * @param float $r1 + * @param float $r2 + */ + protected function _border_line($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $pattern_name = "none", $r1 = 0, $r2 = 0) + { + /** used by $$side */ + [$top, $right, $bottom, $left] = $widths; + $width = $$side; + + // No need to clip corners if border radius is large enough + $cornerClip = $corner_style === "bevel" && ($r1 < $width || $r2 < $width); + $lineLength = $length - $r1 - $r2; + [$pattern, $cap] = $this->dashPattern($pattern_name, $width, $lineLength); + + // Determine arc border radius for corner arcs + $halfWidth = $width / 2; + $ar1 = max($r1 - $halfWidth, 0); + $ar2 = max($r2 - $halfWidth, 0); + + // Small angle adjustments to prevent the background from shining through + $adj1 = $ar1 / 80; + $adj2 = $ar2 / 80; + + // Adjust line width and corner angles to account for the fact that + // round caps extend outwards. The line is actually only shifted below, + // not shortened, as otherwise the end dash (circle) will vanish + // occasionally + $dl = $cap === "round" ? $halfWidth : 0; + + if ($cap === "round" && $ar1 > 0) { + $adj1 -= rad2deg(asin($halfWidth / $ar1)); + } + if ($cap === "round" && $ar2 > 0) { + $adj2 -= rad2deg(asin($halfWidth / $ar2)); + } + + switch ($side) { + case "top": + if ($cornerClip) { + $points = [ + $x, $y, + $x, $y - 1, // Extend outwards to avoid gaps + $x + $length, $y - 1, // Extend outwards to avoid gaps + $x + $length, $y, + $x + $length - max($right, $r2), $y + max($width, $r2), + $x + max($left, $r1), $y + max($width, $r1) + ]; + $this->_canvas->clipping_polygon($points); + } + + $y += $halfWidth; + + if ($ar1 > 0 && $adj1 > -22.5) { + $this->_canvas->arc($x + $r1, $y + $ar1, $ar1, $ar1, 90 - $adj1, 135 + $adj1, $color, $width, $pattern, $cap); + } + + if ($lineLength > 0) { + $this->_canvas->line($x + $dl + $r1, $y, $x + $dl + $length - $r2, $y, $color, $width, $pattern, $cap); + } + + if ($ar2 > 0 && $adj2 > -22.5) { + $this->_canvas->arc($x + $length - $r2, $y + $ar2, $ar2, $ar2, 45 - $adj2, 90 + $adj2, $color, $width, $pattern, $cap); + } + break; + + case "bottom": + if ($cornerClip) { + $points = [ + $x, $y, + $x, $y + 1, // Extend outwards to avoid gaps + $x + $length, $y + 1, // Extend outwards to avoid gaps + $x + $length, $y, + $x + $length - max($right, $r2), $y - max($width, $r2), + $x + max($left, $r1), $y - max($width, $r1) + ]; + $this->_canvas->clipping_polygon($points); + } + + $y -= $halfWidth; + + if ($ar1 > 0 && $adj1 > -22.5) { + $this->_canvas->arc($x + $r1, $y - $ar1, $ar1, $ar1, 225 - $adj1, 270 + $adj1, $color, $width, $pattern, $cap); + } + + if ($lineLength > 0) { + $this->_canvas->line($x + $dl + $r1, $y, $x + $dl + $length - $r2, $y, $color, $width, $pattern, $cap); + } + + if ($ar2 > 0 && $adj2 > -22.5) { + $this->_canvas->arc($x + $length - $r2, $y - $ar2, $ar2, $ar2, 270 - $adj2, 315 + $adj2, $color, $width, $pattern, $cap); + } + break; + + case "left": + if ($cornerClip) { + $points = [ + $x, $y, + $x - 1, $y, // Extend outwards to avoid gaps + $x - 1, $y + $length, // Extend outwards to avoid gaps + $x, $y + $length, + $x + max($width, $r2), $y + $length - max($bottom, $r2), + $x + max($width, $r1), $y + max($top, $r1) + ]; + $this->_canvas->clipping_polygon($points); + } + + $x += $halfWidth; + + if ($ar1 > 0 && $adj1 > -22.5) { + $this->_canvas->arc($x + $ar1, $y + $r1, $ar1, $ar1, 135 - $adj1, 180 + $adj1, $color, $width, $pattern, $cap); + } + + if ($lineLength > 0) { + $this->_canvas->line($x, $y + $dl + $r1, $x, $y + $dl + $length - $r2, $color, $width, $pattern, $cap); + } + + if ($ar2 > 0 && $adj2 > -22.5) { + $this->_canvas->arc($x + $ar2, $y + $length - $r2, $ar2, $ar2, 180 - $adj2, 225 + $adj2, $color, $width, $pattern, $cap); + } + break; + + case "right": + if ($cornerClip) { + $points = [ + $x, $y, + $x + 1, $y, // Extend outwards to avoid gaps + $x + 1, $y + $length, // Extend outwards to avoid gaps + $x, $y + $length, + $x - max($width, $r2), $y + $length - max($bottom, $r2), + $x - max($width, $r1), $y + max($top, $r1) + ]; + $this->_canvas->clipping_polygon($points); + } + + $x -= $halfWidth; + + if ($ar1 > 0 && $adj1 > -22.5) { + $this->_canvas->arc($x - $ar1, $y + $r1, $ar1, $ar1, 0 - $adj1, 45 + $adj1, $color, $width, $pattern, $cap); + } + + if ($lineLength > 0) { + $this->_canvas->line($x, $y + $dl + $r1, $x, $y + $dl + $length - $r2, $color, $width, $pattern, $cap); + } + + if ($ar2 > 0 && $adj2 > -22.5) { + $this->_canvas->arc($x - $ar2, $y + $length - $r2, $ar2, $ar2, 315 - $adj2, 360 + $adj2, $color, $width, $pattern, $cap); + } + break; + } + + if ($cornerClip) { + $this->_canvas->clipping_end(); + } + } + + /** + * @param float $opacity + */ + protected function _set_opacity(float $opacity): void + { + if ($opacity >= 0.0 && $opacity <= 1.0) { + $this->_canvas->set_opacity($opacity); + } + } + + /** + * Add a named destination if the element has an ID or is an anchor element + * with `name` attribute. + * + * @param DOMElement $node + */ + protected function addNamedDest(DOMElement $node): void + { + $id = $node->getAttribute("id"); + if ($id !== "") { + $this->_canvas->add_named_dest($id); + } + + if ($node->nodeName === "a") { + $name = $node->getAttribute("name"); + if ($name !== "") { + $this->_canvas->add_named_dest($name); + } + } + } + + /** + * Add a hyperlink if the element is an anchor element with `href` + * attribute. + * + * @param DOMElement $node + * @param float[] $borderBox + */ + protected function addHyperlink(DOMElement $node, array $borderBox): void + { + if ($node->nodeName === "a" && ($href = $node->getAttribute("href")) !== "") { + [$x, $y, $w, $h] = $borderBox; + $dompdf = $this->_dompdf; + $href = Helpers::build_url( + $dompdf->getProtocol(), + $dompdf->getBaseHost(), + $dompdf->getBasePath(), + $href, + $dompdf->getOptions()->getChroot() + ) ?? $href; + $this->_canvas->add_link($href, $x, $y, $w, $h); + } + } + + /** + * @param float[] $box + * @param array|string $color + * @param array $style + */ + protected function debugLayout(array $box, $color = "red", array $style = []): void + { + $this->_canvas->rectangle($box[0], $box[1], $box[2], $box[3], Color::parse($color), 0.1, $style); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Block.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Block.php new file mode 100644 index 0000000..ab2768d --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Block.php @@ -0,0 +1,81 @@ +get_style(); + $node = $frame->get_node(); + + $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); + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + $this->debugBlockLayout($frame, "red", false); + } + + /** + * @param Frame $frame + * @param array|string $color + * @param bool $lines + */ + protected function debugBlockLayout(Frame $frame, $color, bool $lines = false): void + { + $options = $this->_dompdf->getOptions(); + $debugLayout = $options->getDebugLayout(); + + if (!$debugLayout) { + return; + } + + if ($options->getDebugLayoutBlocks()) { + $this->debugLayout($frame->get_border_box(), $color); + + if ($options->getDebugLayoutPaddingBox()) { + $this->debugLayout($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->debugLayout([$cx + $line->left, $line->y, $lw, $line->h], "orange"); + } + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Image.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Image.php new file mode 100644 index 0000000..1373bb0 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Image.php @@ -0,0 +1,73 @@ +get_style(); + $node = $frame->get_node(); + $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(); + + if (Cache::is_broken($src) && ($alt = $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(); + } + } + + $this->addNamedDest($node); + $this->debugBlockLayout($frame, "blue"); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Inline.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Inline.php new file mode 100644 index 0000000..5d9e284 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Inline.php @@ -0,0 +1,121 @@ +get_first_child(); + while ($child && !$child->is_in_flow()) { + $child = $child->get_next_sibling(); + } + + if (!$child) { + return; // No children, no service + } + + $style = $frame->get_style(); + $node = $frame->get_node(); + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + // Draw background & border behind each child. To do this, we need to + // to figure out just how much space each child takes. Retrieve the + // position of the first child again, to account for text and vertical + // alignment + [$x, $y] = $child->get_position(); + [$w, $h] = $this->get_child_size($frame); + + [, , $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); + $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); + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + + $options = $this->_dompdf->getOptions(); + + if ($options->getDebugLayout() && $options->getDebugLayoutInline()) { + $this->debugLayout($border_box, "blue"); + + if ($options->getDebugLayoutPaddingBox()) { + $padding_box = [ + $x + $style->border_left_width, + $y + $style->border_top_width, + $w - $style->border_left_width - $style->border_right_width, + $h - $style->border_top_width - $style->border_bottom_width + ]; + $this->debugLayout($padding_box, "blue", [0.5, 0.5]); + } + } + } + + protected function get_child_size(Frame $frame): array + { + $w = 0.0; + $h = 0.0; + + foreach ($frame->get_children() as $child) { + if (!$child->is_in_flow()) { + continue; + } + + // Exclude trailing white space + 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_border_box(); + + if ($auto_width || $auto_height) { + [$child_w2, $child_h2] = $this->get_child_size($child); + + if ($auto_width) { + $child_w = $child_w2; + } + + if ($auto_height) { + $child_h = $child_h2; + } + } + + $w += $child_w; + $h = max($h, $child_h); + } + + return [$w, $h]; + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/ListBullet.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/ListBullet.php new file mode 100644 index 0000000..dadfe2b --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/ListBullet.php @@ -0,0 +1,213 @@ +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) { + 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; + + default: + case "decimal": + case "decimal-leading-zero": + case "lower-alpha": + case "lower-latin": + case "lower-roman": + case "lower-greek": + case "upper-alpha": + case "upper-latin": + case "upper-roman": + $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 = (int) $node->getAttribute("dompdf-counter"); + $text = $this->make_counter($index, $bullet_style, $pad); + + $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); + break; + + case "none": + break; + } + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/TableCell.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/TableCell.php new file mode 100644 index 0000000..fbf9178 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/TableCell.php @@ -0,0 +1,189 @@ +get_style(); + $node = $frame->get_node(); + + if (trim($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 === null) { + throw new Exception("Parent table not found for table cell"); + } + + 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); + } + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + $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"); + } + } + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/TableRow.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/TableRow.php new file mode 100644 index 0000000..b1608e8 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/TableRow.php @@ -0,0 +1,40 @@ +get_style(); + $node = $frame->get_node(); + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + $border_box = $frame->get_border_box(); + + // FIXME: Render background onto the area consisting of all spanned + // cells. In the separated border model, the border-spacing area should + // be left out. Currently, the background is inherited by the table + // cells instead, which does not handle transparent backgrounds and + // background images correctly. + // See https://www.w3.org/TR/CSS21/tables.html#table-layers + + $this->_render_outline($frame, $border_box); + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/TableRowGroup.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/TableRowGroup.php new file mode 100644 index 0000000..eb5d23b --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/TableRowGroup.php @@ -0,0 +1,40 @@ +get_style(); + $node = $frame->get_node(); + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + $border_box = $frame->get_border_box(); + + // FIXME: Render background onto the area consisting of all spanned + // cells. In the separated border model, the border-spacing area should + // be left out. Currently, the background is inherited by the table + // cells instead, which does not handle transparent backgrounds and + // background images correctly. + // See https://www.w3.org/TR/CSS21/tables.html#table-layers + + $this->_render_outline($frame, $border_box); + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + } +} diff --git a/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Text.php b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Text.php new file mode 100644 index 0000000..656f3c9 --- /dev/null +++ b/app/libs/dompdf/vendor/dompdf/dompdf/src/Renderer/Text.php @@ -0,0 +1,161 @@ +_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)); + + [$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); + } + + $options = $this->_dompdf->getOptions(); + + if ($options->getDebugLayout() && $options->getDebugLayoutLines()) { + $fontMetrics = $this->_dompdf->getFontMetrics(); + $textWidth = $fontMetrics->getTextWidth($text, $font, $size, $word_spacing, $letter_spacing); + $this->debugLayout([$x, $y, $textWidth, $frame_font_size], "orange", [0.5, 0.5]); + } + } +}