** * @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); } /** * @param string $value * @param int $default * * @return array|string */ protected function parse_counter_prop(string $value, int $default) { $ident = self::CSS_IDENTIFIER; $integer = self::CSS_INTEGER; $pattern = "/($ident)(?:\s+($integer))?/"; if (!preg_match_all($pattern, $value, $matches, PREG_SET_ORDER)) { return "none"; } $counters = []; foreach ($matches as $match) { $counter = $match[1]; $value = isset($match[2]) ? (int) $match[2] : $default; $counters[$counter] = $value; } return $counters; } /** * @param string $computed * @return array|string * * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-increment */ protected function _get_counter_increment($computed) { if ($computed === "none") { return $computed; } return $this->parse_counter_prop($computed, 1); } /** * @param string $computed * @return array|string * * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-reset */ protected function _get_counter_reset($computed) { if ($computed === "none") { return $computed; } return $this->parse_counter_prop($computed, 0); } /** * @param string $computed * @return string[]|string * * @link https://www.w3.org/TR/CSS21/generate.html#propdef-content */ protected function _get_content($computed) { if ($computed === "normal" || $computed === "none") { return $computed; } return $this->parse_property_value($computed); } /*==============================*/ /** * Parse a property value into its components. * * @param string $value * * @return string[] */ protected function parse_property_value(string $value): array { $ident = self::CSS_IDENTIFIER; $number = self::CSS_NUMBER; $pattern = "/\n" . "\s* \" ( (?:[^\"]|\\\\[\"])* ) (?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_length(string $val): ?float { return mb_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); return $computed !== null && $computed >= 0 ? $computed : null; } /** * @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 mb_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); if ($computed === null || $computed < 0) { return null; } // Retain valid percentage declarations return mb_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 { // 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 { return \in_array($val, self::BORDER_STYLES, true) ? $val : null; } /** * 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) { // 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($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"]; 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"]; 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) { $parts = preg_split("/\s+/", $val); if (\count($parts) > 2) { return null; } switch ($parts[0]) { case "left": $x = "0%"; break; case "right": $x = "100%"; break; case "top": $y = "0%"; break; case "bottom": $y = "100%"; break; case "center": $x = "50%"; $y = "50%"; break; default: $x = $parts[0]; break; } if (isset($parts[1])) { switch ($parts[1]) { case "left": $x = "0%"; break; case "right": $x = "100%"; break; case "top": $y = "0%"; break; case "bottom": $y = "100%"; break; case "center": if ($parts[0] === "left" || $parts[0] === "right" || $parts[0] === "center") { $y = "50%"; } else { $x = "50%"; } break; default: $y = $parts[1]; break; } } else { $y = "50%"; } if (!isset($x)) { $x = "0%"; } if (!isset($y)) { $y = "0%"; } 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) { if ($val === "cover" || $val === "contain") { return $val; } $parts = preg_split("/\s+/", $val); if (\count($parts) > 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) { if ($val === "none" || mb_substr($val, 0, 4) === "url(") { $props["background_image"] = $val; } elseif ($val === "scroll" || $val === "fixed") { $props["background_attachment"] = $val; } elseif ($val === "repeat" || $val === "repeat-x" || $val === "repeat-y" || $val === "no-repeat") { $props["background_repeat"] = $val; } elseif ($this->is_color_value($val)) { $props["background_color"] = $val; } else { $pos_size[] = $val; } } 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-size */ protected function _compute_font_size(string $size) { $parent_font_size = isset($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"; } }