Files
Sistema-Educativo-Laravel/vendor/dompdf/dompdf/src/Css/Style.php
T

4632 lines
141 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* @package dompdf
* @link https://github.com/dompdf/dompdf
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
namespace Dompdf\Css;
use Dompdf\Adapter\CPDF;
use Dompdf\Css\Content\Attr;
use Dompdf\Css\Content\CloseQuote;
use Dompdf\Css\Content\ContentPart;
use Dompdf\Css\Content\Counter;
use Dompdf\Css\Content\Counters;
use Dompdf\Css\Content\NoCloseQuote;
use Dompdf\Css\Content\NoOpenQuote;
use Dompdf\Css\Content\OpenQuote;
use Dompdf\Css\Content\StringPart;
use Dompdf\Css\Content\Url;
use Dompdf\Exception;
use Dompdf\FontMetrics;
use Dompdf\Frame;
use Dompdf\Helpers;
/**
* Represents CSS properties.
*
* The Style class is responsible for handling and storing CSS properties.
* It includes methods to resolve colors and lengths, as well as getters &
* setters for many CSS properties.
*
* Access to the different CSS properties is provided by the methods
* {@link Style::set_prop()} and {@link Style::get_specified()}, and the
* property overload methods {@link Style::__set()} and {@link Style::__get()},
* as well as {@link Style::set_used()}. The latter methods operate on used
* values and permit access to any (CSS) property using the following syntax:
*
* ```
* $style->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 = "" .
'"(?>(?:\\\\["]|[^"])*)(?<!\\\\)"|' . // String ""
"'(?>(?:\\\\[']|[^'])*)(?<!\\\\)'"; // String ''
protected const CSS_VAR = "var\((([^()]|(?R))*)\)";
/**
* @link https://www.w3.org/TR/css-values-4/#calc-syntax
*/
protected const CSS_MATH_FUNCTIONS = [
// Basic Arithmetic
"calc" => 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<float>
*/
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<string, string[]>
*/
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<string, string>
*/
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<string, mixed>
*/
protected static $_defaults = null;
/**
* Lookup table for properties that inherit by default.
*
* @link https://www.w3.org/TR/CSS21/propidx.html
*
* @var array<string, true>
*/
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<string, string[]>
*/
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<string, true>
*/
protected static $_dependent_props = [];
/**
* Caches method_exists result
*
* @var array<string, bool>
*/
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<string, true>
*/
protected $_important_props = [];
/**
* Specified (or declared) values of the CSS properties.
*
* https://www.w3.org/TR/css-cascade-3/#value-stages
*
* @var array<string, mixed>
*/
protected $_props = [];
/**
* Used to track which CSS property were set directly versus
* those set via shorthand property
*
* @var array<string, true>
*/
protected $_props_specified = [];
/**
* Computed values of the CSS properties.
*
* @var array<string, mixed>
*/
protected $_props_computed = [];
/**
* Used values of the CSS properties.
*
* @var array<string, mixed>
*/
protected $_props_used = [];
/**
* Marks properties with non-final used values that should be cleared on
* style reset.
*
* @var array<string, true>
*/
protected $non_final_used = [];
/**
* Used to track CSS property assignment entry/exit in order to watch
* for circular dependencies.
*
* @var array<int, string>
*/
protected $_prop_stack = [];
/**
* Used to track CSS variable resolution entry/exit in order to watch
* for circular dependencies.
*
* @var array<int, string>
*/
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>$string) |\n" . // String
"\s* (url \( (?> (\\\\[\"'()] | [^\"'()])* ) (?<!\\\\)\) ) |\n" . // URL without quotes
"\s* ($ident (\( ((?> \g<string> | [^\"'()]+ ) | (?-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 arent 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;
// <length-percentage> 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;
// <number> 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;
// <angle> 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";
}
}