t($this->parent_style) ? $this->parent_style->__get("font_size") : self::$default_font_size; switch ($size) { case "xx-small": case "x-small": case "small": case "medium": case "large": case "x-large": case "xx-large": $fs = self::$default_font_size * self::$font_size_keywords[$size]; break; case "smaller": $fs = 8 / 9 * $parent_font_size; break; case "larger": $fs = 6 / 5 * $parent_font_size; break; default: $fs = $this->single_length_in_pt($size, $parent_font_size, $parent_font_size); break; } return $fs; } /** * @link https://www.w3.org/TR/CSS21/fonts.html#font-boldness */ protected function _compute_font_weight(string $weight) { $computed_weight = $weight; if ($weight === "bolder") { //TODO: One font weight heavier than the parent element (among the available weights of the font). $computed_weight = "bold"; } elseif ($weight === "lighter") { //TODO: One font weight lighter than the parent element (among the available weights of the font). $computed_weight = "normal"; } return $computed_weight; } /** * 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 { $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))$/"; $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|100|200|300|400|500|600|700|800|900)$/"; 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 = $val; if ($alignment === "") { $alignment = "left"; if ($this->__get("direction") === "rtl") { $alignment = "right"; } } 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) { 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) { 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) { 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); return $computed !== null && $computed >= 0 ? $computed : null; } /** * @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 $break) { 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 $break) { 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) { 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) { 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) { // 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) { // 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) { // 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) { // 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) { 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) { // 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) { // 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 array 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 { $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) { 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) { $parts = preg_split("/\s+/", $val); if (\count($parts) > 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($parsed_val)"; } } /** * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style */ protected function _set_list_style(string $value): array { static $positions = ["inside", "outside"]; static $types = [ "disc", "circle", "square", "decimal-leading-zero", "decimal", "1", "lower-roman", "upper-roman", "a", "A", "lower-greek", "lower-latin", "upper-latin", "lower-alpha", "upper-alpha", "armenian", "georgian", "hebrew", "cjk-ideographic", "hiragana", "katakana", "hiragana-iroha", "katakana-iroha", "none" ]; $components = $this->parse_property_value($value); $props = []; foreach ($components as $val) { /* https://www.w3.org/TR/CSS21/generate.html#list-style * A value of 'none' for the 'list-style' property sets both 'list-style-type' and 'list-style-image' to 'none' */ if ($val === "none") { $props["list_style_type"] = $val; $props["list_style_image"] = $val; continue; } //On setting or merging or inheriting list_style_image as well as list_style_type, //and url exists, then url has precedence, otherwise fall back to list_style_type //Firefox is wrong here (list_style_image gets overwritten on explicit list_style_type) //Internet Explorer 7/8 and dompdf is right. if (mb_substr($val, 0, 4) === "url(") { $props["list_style_image"] = $val; continue; } if (\in_array($val, $types, true)) { $props["list_style_type"] = $val; } elseif (\in_array($val, $positions, true)) { $props["list_style_position"] = $val; } } return $props; } /** * @link https://www.w3.org/TR/css-page-3/#page-size-prop */ protected function _compute_size(string $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]; } /** * @param string $computed * @return array * * @link https://www.w3.org/TR/css-transforms-1/#transform-property */ protected function _get_transform($computed) { //TODO: should be handled in setter (lengths set to absolute) $number = "\s*([^,\s]+)\s*"; $tr_value = "\s*([^,\s]+)\s*"; $angle = "\s*([^,\s]+(?:deg|rad)?)\s*"; if (!preg_match_all("/[a-z]+\([^\)]+\)/i", $computed, $parts, PREG_SET_ORDER)) { return []; } $functions = [ //"matrix" => "\($number,$number,$number,$number,$number,$number\)", "translate" => "\($tr_value(?:,$tr_value)?\)", "translateX" => "\($tr_value\)", "translateY" => "\($tr_value\)", "scale" => "\($number(?:,$number)?\)", "scaleX" => "\($number\)", "scaleY" => "\($number\)", "rotate" => "\($angle\)", "skew" => "\($angle(?:,$angle)?\)", "skewX" => "\($angle\)", "skewY" => "\($angle\)", ]; $transforms = []; foreach ($parts as $part) { $t = $part[0]; foreach ($functions as $name => $pattern) { if (preg_match("/$name\s*$pattern/i", $t, $matches)) { $values = \array_slice($matches, 1); switch ($name) { // units case "rotate": case "skew": case "skewX": case "skewY": foreach ($values as $i => $value) { if (strpos($value, "rad")) { $values[$i] = rad2deg((float) $value); } else { $values[$i] = (float) $value; } } switch ($name) { case "skew": if (!isset($values[1])) { $values[1] = 0; } break; case "skewX": $name = "skew"; $values = [$values[0], 0]; break; case "skewY": $name = "skew"; $values = [0, $values[0]]; break; } break; // units case "translate": $values[0] = $this->length_in_pt($values[0], (float)$this->length_in_pt($this->width)); if (isset($values[1])) { $values[1] = $this->length_in_pt($values[1], (float)$this->length_in_pt($this->height)); } else { $values[1] = 0; } break; case "translateX": $name = "translate"; $values = [$this->length_in_pt($values[0], (float)$this->length_in_pt($this->width)), 0]; break; case "translateY": $name = "translate"; $values = [0, $this->length_in_pt($values[0], (float)$this->length_in_pt($this->height))]; break; // units case "scale": if (!isset($values[1])) { $values[1] = $values[0]; } break; case "scaleX": $name = "scale"; $values = [$values[0], 1.0]; break; case "scaleY": $name = "scale"; $values = [1.0, $values[0]]; break; } $transforms[] = [ $name, $values, ]; } } } return $transforms; } /** * @param string $computed * @return array * * @link https://www.w3.org/TR/css-transforms-1/#transform-origin-property */ protected function _get_transform_origin($computed) { //TODO: should be handled in setter $values = preg_split("/\s+/", $computed); $values = array_map(function ($value) { if (\in_array($value, ["top", "left"], true)) { return 0; } elseif (\in_array($value, ["bottom", "right"], true)) { return "100%"; } else { return $value; } }, $values); if (!isset($values[1])) { $values[1] = $values[0]; } return $values; } /** * @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*$/'; $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) { 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"; } }