/** * Render a background image over a rectangular area * * @param string $url The background image to load * @param float $x The left edge of the rectangular area * @param float $y The top edge of the rectangular area * @param float $width The width of the rectangular area * @param float $height The height of the rectangular area * @param Style $style The associated Style object * * @throws \Exception */ protected function _background_image($url, $x, $y, $width, $height, $style) { if (!function_exists("imagecreatetruecolor")) { throw new \Exception("The PHP GD extension is required, but is not installed."); } $sheet = $style->get_stylesheet(); // Skip degenerate cases if ($width == 0 || $height == 0) { return; } $box_width = $width; $box_height = $height; //debugpng if ($this->_dompdf->get_option("debugPng")) { print '[_background_image ' . $url . ']'; } list($img, $type, ) = Cache::resolve_url($url, $sheet->get_protocol(), $sheet->get_host(), $sheet->get_base_path(), $this->_dompdf); // Bail if the image is no good if (Cache::is_broken($img)) { return; } //Try to optimize away reading and composing of same background multiple times //Postponing read with imagecreatefrom ...() //final composition parameters and name not known yet //Therefore read dimension directly from file, instead of creating gd object first. //$img_w = imagesx($src); $img_h = imagesy($src); list($img_w, $img_h) = Helpers::dompdf_getimagesize($img); if (!isset($img_w) || $img_w == 0 || !isset($img_h) || $img_h == 0) { return; } $repeat = $style->background_repeat; $dpi = $this->_dompdf->get_option("dpi"); //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((double) ($width * $dpi) / 72); $bg_height = round((double) ($height * $dpi) / 72); //Need %bg_x, $bg_y as background pos, where img starts, converted to pixel list($bg_x, $bg_y) = $style->background_position; if (Helpers::is_percent($bg_x)) { // The point $bg_x % from the left edge of the image is placed // $bg_x % from the left edge of the background rectangle $p = (double) $bg_x / 100.0; $x1 = $p * $img_w; $x2 = $p * $bg_width; $bg_x = $x2 - $x1; } else { $bg_x = (double) ($style->length_in_pt($bg_x) * $dpi) / 72; } $bg_x = round($bg_x + $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 = (double) $bg_y / 100.0; $y1 = $p * $img_h; $y2 = $p * $bg_height; $bg_y = $y2 - $y1; } else { $bg_y = (double) ($style->length_in_pt($bg_y) * $dpi) / 72; } $bg_y = round($bg_y + $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 = (double) ($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 = (double) ($bg_height * 72) / $dpi; } else { //repeat y if ($bg_y < 0) { $bg_y = -(-$bg_y % $img_h); } else { $bg_y = $bg_y % $img_h; if ($bg_y > 0) { $bg_y -= $img_h; } } } //Optimization, if repeat has no effect if ($repeat === "repeat" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height) { $repeat = "repeat-x"; } if ($repeat === "repeat" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width) { $repeat = "repeat-y"; } if ($repeat === "repeat-x" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width || $repeat === "repeat-y" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height) { $repeat = "no-repeat"; } //Use filename as indicator only //different names for different variants to have different copies in the pdf //This is not dependent of background color of box! .'_'.(is_array($bg_color) ? $bg_color["hex"] : $bg_color) //Note: Here, bg_* are the start values, not end values after going through the tile loops! $filedummy = $img; $is_png = false; $filedummy .= '_' . $bg_width . '_' . $bg_height . '_' . $bg_x . '_' . $bg_y . '_' . $repeat; //Optimization to avoid multiple times rendering the same image. //If check functions are existing and identical image already cached, //then skip creation of duplicate, because it is not needed by addImagePng if ($this->_canvas instanceof CPDF && $this->_canvas->get_cpdf()->image_iscached($filedummy)) { $bg = null; } else { // Create a new image to fit over the background rectangle $bg = imagecreatetruecolor($bg_width, $bg_height); switch (strtolower($type)) { case "png": $is_png = true; imagesavealpha($bg, true); imagealphablending($bg, false); $src = imagecreatefrompng($img); break; case "jpeg": $src = imagecreatefromjpeg($img); break; case "gif": $src = imagecreatefromgif($img); break; case "bmp": $src = Helpers::imagecreatefrombmp($img); break; default: return; // Unsupported image type } if ($src == null) { return; } //Background color if box is not relevant here //Non transparent image: box clipped to real size. Background non relevant. //Transparent image: The image controls the transparency and lets shine through whatever background. //However on transparent image preset the composed image with the transparency color, //to keep the transparency when copying over the non transparent parts of the tiles. $ti = imagecolortransparent($src); if ($ti >= 0) { $tc = imagecolorsforindex($src, $ti); $ti = imagecolorallocate($bg, $tc['red'], $tc['green'], $tc['blue']); imagefill($bg, 0, 0, $ti); imagecolortransparent($bg, $ti); } //This has only an effect for the non repeatable dimension. //compute start of src and dest coordinates of the single copy if ($bg_x < 0) { $dst_x = 0; $src_x = -$bg_x; } else { $src_x = 0; $dst_x = $bg_x; } if ($bg_y < 0) { $dst_y = 0; $src_y = -$bg_y; } else { $src_y = 0; $dst_y = $bg_y; } //For historical reasons exchange meanings of variables: //start_* will be the start values, while bg_* will be the temporary start values in the loops $start_x = $bg_x; $start_y = $bg_y; // Copy regions from the source image to the background if ($repeat === "no-repeat") { // Simply place the image on the background imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $img_h); } else { if ($repeat === "repeat-x") { for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) { if ($bg_x < 0) { $dst_x = 0; $src_x = -$bg_x; $w = $img_w + $bg_x; } else { $dst_x = $bg_x; $src_x = 0; $w = $img_w; } imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $img_h); } } else { if ($repeat === "repeat-y") { for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) { if ($bg_y < 0) { $dst_y = 0; $src_y = -$bg_y; $h = $img_h + $bg_y; } else { $dst_y = $bg_y; $src_y = 0; $h = $img_h; } imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $h); } } else { if ($repeat === "repeat") { for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) { for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) { if ($bg_x < 0) { $dst_x = 0; $src_x = -$bg_x; $w = $img_w + $bg_x; } else { $dst_x = $bg_x; $src_x = 0; $w = $img_w; } if ($bg_y < 0) { $dst_y = 0; $src_y = -$bg_y; $h = $img_h + $bg_y; } else { $dst_y = $bg_y; $src_y = 0; $h = $img_h; } imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $h); } } } else { print 'Unknown repeat!'; } } } } imagedestroy($src); } /* End optimize away creation of duplicates */ $this->_canvas->clipping_rectangle($x, $y, $box_width, $box_height); //img: image url string //img_w, img_h: original image size in px //width, height: box size in pt //bg_width, bg_height: box size in px //x, y: left/top edge of box on page in pt //start_x, start_y: placement of image relative to pattern //$repeat: repeat mode //$bg: GD object of result image //$src: GD object of original image //When using cpdf and optimization to direct png creation from gd object is available, //don't create temp file, but place gd object directly into the pdf if (!$is_png && $this->_canvas instanceof CPDF) { // Note: CPDF_Adapter image converts y position $this->_canvas->get_cpdf()->addImagePng($filedummy, $x, $this->_canvas->get_height() - $y - $height, $width, $height, $bg); } else { $tmp_dir = $this->_dompdf->get_option("temp_dir"); $tmp_name = tempnam($tmp_dir, "bg_dompdf_img_"); @unlink($tmp_name); $tmp_file = "{$tmp_name}.png"; //debugpng if ($this->_dompdf->get_option("debugPng")) { print '[_background_image ' . $tmp_file . ']'; } imagepng($bg, $tmp_file); $this->_canvas->image($tmp_file, $x, $y, $width, $height); imagedestroy($bg); //debugpng if ($this->_dompdf->get_option("debugPng")) { print '[_background_image unlink ' . $tmp_file . ']'; } if (!$this->_dompdf->get_option("debugKeepTemp")) { unlink($tmp_file); } } $this->_canvas->clipping_end(); }
/** * @return string */ public function __toString() { // Skip empty text frames // if ( $this->is_text_node() && // preg_replace("/\s/", "", $this->_node->data) === "" ) // return ""; $str = "<b>" . $this->_node->nodeName . ":</b><br/>"; //$str .= spl_object_hash($this->_node) . "<br/>"; $str .= "Id: " . $this->get_id() . "<br/>"; $str .= "Class: " . get_class($this) . "<br/>"; if ($this->is_text_node()) { $tmp = htmlspecialchars($this->_node->nodeValue); $str .= "<pre>'" . mb_substr($tmp, 0, 70) . (mb_strlen($tmp) > 70 ? "..." : "") . "'</pre>"; } elseif ($css_class = $this->_node->getAttribute("class")) { $str .= "CSS class: '{$css_class}'<br/>"; } if ($this->_parent) { $str .= "\nParent:" . $this->_parent->_node->nodeName . " (" . spl_object_hash($this->_parent->_node) . ") " . "<br/>"; } if ($this->_prev_sibling) { $str .= "Prev: " . $this->_prev_sibling->_node->nodeName . " (" . spl_object_hash($this->_prev_sibling->_node) . ") " . "<br/>"; } if ($this->_next_sibling) { $str .= "Next: " . $this->_next_sibling->_node->nodeName . " (" . spl_object_hash($this->_next_sibling->_node) . ") " . "<br/>"; } $d = $this->get_decorator(); while ($d && $d != $d->get_decorator()) { $str .= "Decorator: " . get_class($d) . "<br/>"; $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: <pre>" . $this->_style->__toString() . "</pre>"; if ($this->_decorator instanceof FrameDecorator\Block) { $str .= "Lines:<pre>"; 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 .= "</pre>"; } $str .= "\n"; if (php_sapi_name() === "cli") { $str = strip_tags(str_replace(array("<br/>", "<b>", "</b>"), array("\n", "", ""), $str)); } return $str; }
/** * Set inherited properties in this style using values in $parent * * @param Style $parent * * @return Style */ function inherit(Style $parent) { // Set parent font size $this->_parent_font_size = $parent->get_font_size(); foreach (self::$_inherited as $prop) { //inherit the !important property also. //if local property is also !important, don't inherit. if (isset($parent->_props[$prop]) && (!isset($this->_props[$prop]) || isset($parent->_important_props[$prop]) && !isset($this->_important_props[$prop]))) { if (isset($parent->_important_props[$prop])) { $this->_important_props[$prop] = true; } //see __set and __get, on all assignments clear cache! $this->_prop_cache[$prop] = null; $this->_props[$prop] = $parent->_props[$prop]; } } foreach ($this->_props as $prop => $value) { if ($value === "inherit") { if (isset($parent->_important_props[$prop])) { $this->_important_props[$prop] = true; } //do not assign direct, but //implicite assignment through __set, redirect to specialized, get value with __get //This is for computing defaults if the parent setting is also missing. //Therefore do not directly assign the value without __set //set _important_props before that to be able to propagate. //see __set and __get, on all assignments clear cache! //$this->_prop_cache[$prop] = null; //$this->_props[$prop] = $parent->_props[$prop]; //props_set for more obvious explicite assignment not implemented, because //too many implicite uses. // $this->props_set($prop, $parent->$prop); $this->__set($prop, $parent->__get($prop)); } } return $this; }
/** * parse regular CSS blocks * * _parse_properties() creates a new Style object based on the provided * CSS rules. * * @param string $str CSS rules * @return Style */ private function _parse_properties($str) { $properties = preg_split("/;(?=(?:[^\\(]*\\([^\\)]*\\))*(?![^\\)]*\\)))/", $str); if ($this->_dompdf->get_option('debugCss')) { print '[_parse_properties'; } // Create the style $style = new Style($this, Stylesheet::ORIG_AUTHOR); foreach ($properties as $prop) { // If the $prop contains an url, the regex may be wrong // @todo: fix the regex so that it works everytime /*if (strpos($prop, "url(") === false) { if (preg_match("/([a-z-]+)\s*:\s*[^:]+$/i", $prop, $m)) $prop = $m[0]; }*/ //A css property can have " ! important" appended (whitespace optional) //strip this off to decode core of the property correctly. //Pass on in the style to allow proper handling: //!important properties can only be overridden by other !important ones. //$style->$prop_name = is a shortcut of $style->__set($prop_name,$value);. //If no specific set function available, set _props["prop_name"] //style is always copied completely, or $_props handled separately //Therefore set a _important_props["prop_name"]=true to indicate the modifier /* Instead of short code, prefer the typical case with fast code $important = preg_match("/(.*?)!\s*important/",$prop,$match); if ( $important ) { $prop = $match[1]; } $prop = trim($prop); */ if ($this->_dompdf->get_option('debugCss')) { print '('; } $important = false; $prop = trim($prop); if (substr($prop, -9) === 'important') { $prop_tmp = rtrim(substr($prop, 0, -9)); if (substr($prop_tmp, -1) === '!') { $prop = rtrim(substr($prop_tmp, 0, -1)); $important = true; } } if ($prop === "") { if ($this->_dompdf->get_option('debugCss')) { print 'empty)'; } continue; } $i = mb_strpos($prop, ":"); if ($i === false) { if ($this->_dompdf->get_option('debugCss')) { print 'novalue' . $prop . ')'; } continue; } $prop_name = rtrim(mb_strtolower(mb_substr($prop, 0, $i))); $value = ltrim(mb_substr($prop, $i + 1)); if ($this->_dompdf->get_option('debugCss')) { print $prop_name . ':=' . $value . ($important ? '!IMPORTANT' : '') . ')'; } //New style, anyway empty //if ($important || !$style->important_get($prop_name) ) { //$style->$prop_name = array($value,$important); //assignment might be replaced by overloading through __set, //and overloaded functions might check _important_props, //therefore set _important_props first. if ($important) { $style->important_set($prop_name); } //For easier debugging, don't use overloading of assignments with __set $style->{$prop_name} = $value; //$style->props_set($prop_name, $value); } if ($this->_dompdf->get_option('debugCss')) { print '_parse_properties]'; } return $style; }