PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.23
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.23
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 All 169 releases
woocommerce-pos / vendor_prefixed / dompdf / dompdf / src / Css / Style.php

Style.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.23, at vendor_prefixed/dompdf/dompdf/src/Css/Style.php

3,673 lines 136.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @package dompdf
5 * @link https://github.com/dompdf/dompdf
6 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
7 */
8 namespace WCPOS\Vendor\Dompdf\Css;
9
10 use WCPOS\Vendor\Dompdf\Adapter\CPDF;
11 use WCPOS\Vendor\Dompdf\Css\Content\Attr;
12 use WCPOS\Vendor\Dompdf\Css\Content\CloseQuote;
13 use WCPOS\Vendor\Dompdf\Css\Content\ContentPart;
14 use WCPOS\Vendor\Dompdf\Css\Content\Counter;
15 use WCPOS\Vendor\Dompdf\Css\Content\Counters;
16 use WCPOS\Vendor\Dompdf\Css\Content\NoCloseQuote;
17 use WCPOS\Vendor\Dompdf\Css\Content\NoOpenQuote;
18 use WCPOS\Vendor\Dompdf\Css\Content\OpenQuote;
19 use WCPOS\Vendor\Dompdf\Css\Content\StringPart;
20 use WCPOS\Vendor\Dompdf\Css\Content\Url;
21 use WCPOS\Vendor\Dompdf\Exception;
22 use WCPOS\Vendor\Dompdf\FontMetrics;
23 use WCPOS\Vendor\Dompdf\Frame;
24 use WCPOS\Vendor\Dompdf\Helpers;
25 /**
26 * Represents CSS properties.
27 *
28 * The Style class is responsible for handling and storing CSS properties.
29 * It includes methods to resolve colors and lengths, as well as getters &
30 * setters for many CSS properties.
31 *
32 * Access to the different CSS properties is provided by the methods
33 * {@link Style::set_prop()} and {@link Style::get_specified()}, and the
34 * property overload methods {@link Style::__set()} and {@link Style::__get()},
35 * as well as {@link Style::set_used()}. The latter methods operate on used
36 * values and permit access to any (CSS) property using the following syntax:
37 *
38 * ```
39 * $style->margin_top = 10.0;
40 * echo $style->margin_top; // Returns `10.0`
41 * ```
42 *
43 * To declare a property from a string, use {@link Style::set_prop()}:
44 *
45 * ```
46 * $style->set_prop("margin_top", "1em");
47 * echo $style->get_specified("margin_top"); // Returns `1em`
48 * echo $style->margin_top; // Returns `12.0`, assuming the default font size
49 * ```
50 *
51 * Actual CSS parsing is performed in the {@link Stylesheet} class.
52 *
53 * @property string $azimuth
54 * @property string $background_attachment
55 * @property array|string $background_color
56 * @property string $background_image Image URL or `none`
57 * @property string $background_image_resolution
58 * @property array $background_position Pair of `[x, y]`, each value being a length in pt or a percentage value
59 * @property string $background_repeat
60 * @property array|string $background_size `cover`, `contain`, or `[width, height]`, each being a length, percentage, or `auto`
61 * @property string $border_collapse
62 * @property string $border_color Only use for setting all sides to the same color
63 * @property float[] $border_spacing Pair of `[horizontal, vertical]` spacing
64 * @property string $border_style Only use for setting all sides to the same style
65 * @property array|string $border_top_color
66 * @property array|string $border_right_color
67 * @property array|string $border_bottom_color
68 * @property array|string $border_left_color
69 * @property string $border_top_style Valid border style
70 * @property string $border_right_style Valid border style
71 * @property string $border_bottom_style Valid border style
72 * @property string $border_left_style Valid border style
73 * @property float $border_top_width Length in pt
74 * @property float $border_right_width Length in pt
75 * @property float $border_bottom_width Length in pt
76 * @property float $border_left_width Length in pt
77 * @property string $border_width Only use for setting all sides to the same width
78 * @property float|string $border_bottom_left_radius Radius in pt or a percentage value
79 * @property float|string $border_bottom_right_radius Radius in pt or a percentage value
80 * @property float|string $border_top_left_radius Radius in pt or a percentage value
81 * @property float|string $border_top_right_radius Radius in pt or a percentage value
82 * @property string $border_radius Only use for setting all corners to the same radius
83 * @property float|string $bottom Length in pt, a percentage value, or `auto`
84 * @property string $caption_side
85 * @property string $clear
86 * @property string $clip
87 * @property array|string $color
88 * @property ContentPart[]|string $content List of content components, `normal`, or `none`
89 * @property array|string $counter_increment Array defining the counters to increment or `none`
90 * @property array|string $counter_reset Array defining the counters to reset or `none`
91 * @property string $cue_after
92 * @property string $cue_before
93 * @property string $cue
94 * @property string $cursor
95 * @property string $direction
96 * @property string $display
97 * @property string $elevation
98 * @property string $empty_cells
99 * @property string $float
100 * @property string $font_family
101 * @property float $font_size Length in pt
102 * @property string $font_style `normal`, `italic`, or `oblique`
103 * @property string $font_variant
104 * @property int $font_weight Number in the range [1, 1000]
105 * @property float|string $height Length in pt, a percentage value, or `auto`
106 * @property string $image_resolution
107 * @property string $inset Only use for setting all box insets to the same length
108 * @property float|string $left Length in pt, a percentage value, or `auto`
109 * @property float $letter_spacing Length in pt
110 * @property float $line_height Length in pt
111 * @property string $list_style_image Image URL or `none`
112 * @property string $list_style_position `inside` or `outside`
113 * @property string $list_style_type
114 * @property float|string $margin_right Length in pt, a percentage value, or `auto`
115 * @property float|string $margin_left Length in pt, a percentage value, or `auto`
116 * @property float|string $margin_top Length in pt, a percentage value, or `auto`
117 * @property float|string $margin_bottom Length in pt, a percentage value, or `auto`
118 * @property string $margin Only use for setting all sides to the same length
119 * @property float|string $max_height Length in pt, a percentage value, or `none`
120 * @property float|string $max_width Length in pt, a percentage value, or `none`
121 * @property float|string $min_height Length in pt, a percentage value, or `auto`
122 * @property float|string $min_width Length in pt, a percentage value, or `auto`
123 * @property float $opacity Number in the range [0, 1]
124 * @property int $orphans
125 * @property array|string $outline_color
126 * @property string $outline_style Valid border style, except for `hidden`
127 * @property float $outline_width Length in pt
128 * @property float $outline_offset Length in pt
129 * @property string $overflow
130 * @property string $overflow_wrap
131 * @property float|string $padding_top Length in pt or a percentage value
132 * @property float|string $padding_right Length in pt or a percentage value
133 * @property float|string $padding_bottom Length in pt or a percentage value
134 * @property float|string $padding_left Length in pt or a percentage value
135 * @property string $padding Only use for setting all sides to the same length
136 * @property string $page_break_after
137 * @property string $page_break_before
138 * @property string $page_break_inside
139 * @property string $pause_after
140 * @property string $pause_before
141 * @property string $pause
142 * @property string $pitch_range
143 * @property string $pitch
144 * @property string $play_during
145 * @property string $position
146 * @property array|string $quotes List of quote pairs, or `none`
147 * @property string $richness
148 * @property float|string $right Length in pt, a percentage value, or `auto`
149 * @property float[]|string $size Pair of `[width, height]` or `auto`
150 * @property string $speak_header
151 * @property string $speak_numeral
152 * @property string $speak_punctuation
153 * @property string $speak
154 * @property string $speech_rate
155 * @property string $src
156 * @property string $stress
157 * @property string $table_layout
158 * @property string $text_align
159 * @property string $text_decoration
160 * @property float|string $text_indent Length in pt or a percentage value
161 * @property string $text_transform
162 * @property float|string $top Length in pt, a percentage value, or `auto`
163 * @property array $transform List of transforms
164 * @property array $transform_origin Triplet of `[x, y, z]`, each value being a length in pt, or a percentage value for x and y
165 * @property string $unicode_bidi
166 * @property string $unicode_range
167 * @property string $vertical_align
168 * @property string $visibility
169 * @property string $voice_family
170 * @property string $volume
171 * @property string $white_space
172 * @property int $widows
173 * @property float|string $width Length in pt, a percentage value, or `auto`
174 * @property string $word_break
175 * @property float $word_spacing Length in pt
176 * @property int|string $z_index Integer value or `auto`
177 * @property string $_dompdf_keep
178 *
179 * @package dompdf
180 */
181 class Style
182 {
183 protected const CSS_IDENTIFIER = "-?[_a-zA-Z]+[_a-zA-Z0-9-]*";
184 protected const CSS_INTEGER = "[+-]?\\d+";
185 protected const CSS_NUMBER = "[+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?";
186 protected const CSS_STRING = "" . '"(?>(?:\\\\["]|[^"])*)(?<!\\\\)"|' . "'(?>(?:\\\\[']|[^'])*)(?<!\\\\)'";
187 // String ''
188 protected const CSS_VAR = "var\\((([^()]|(?R))*)\\)";
189 /**
190 * @link https://www.w3.org/TR/css-values-4/#calc-syntax
191 */
192 protected const CSS_MATH_FUNCTIONS = [
193 // Basic Arithmetic
194 "calc" => \true,
195 // Comparison Functions
196 "min" => \true,
197 "max" => \true,
198 "clamp" => \true,
199 // Stepped Value Functions
200 "round" => \true,
201 // Not fully supported
202 "mod" => \true,
203 "rem" => \true,
204 // Trigonometric Functions
205 "sin" => \true,
206 "cos" => \true,
207 "tan" => \true,
208 "asin" => \true,
209 "acos" => \true,
210 "atan" => \true,
211 "atan2" => \true,
212 // Exponential Functions
213 "pow" => \true,
214 "sqrt" => \true,
215 "hypot" => \true,
216 "log" => \true,
217 "exp" => \true,
218 // Sign-Related Functions
219 "abs" => \true,
220 "sign" => \true,
221 ];
222 /**
223 * https://www.w3.org/TR/css-values-3/#custom-idents
224 */
225 protected const CUSTOM_IDENT_FORBIDDEN = ["inherit", "initial", "unset", "default"];
226 /**
227 * Default font size, in points.
228 *
229 * @var float
230 */
231 public static $default_font_size = 12;
232 /**
233 * Default line height, as a fraction of the font size.
234 *
235 * @var float
236 */
237 public static $default_line_height = 1.2;
238 /**
239 * Default "absolute" font sizes relative to the default font-size
240 * https://www.w3.org/TR/css-fonts-3/#absolute-size-value
241 *
242 * @var array<float>
243 */
244 public static $font_size_keywords = [
245 "xx-small" => 0.6,
246 // 3/5
247 "x-small" => 0.75,
248 // 3/4
249 "small" => 0.889,
250 // 8/9
251 "medium" => 1,
252 // 1
253 "large" => 1.2,
254 // 6/5
255 "x-large" => 1.5,
256 // 3/2
257 "xx-large" => 2.0,
258 ];
259 /**
260 * List of valid text-align keywords.
261 */
262 public const TEXT_ALIGN_KEYWORDS = ["left", "right", "center", "justify"];
263 /**
264 * List of valid vertical-align keywords.
265 */
266 public const VERTICAL_ALIGN_KEYWORDS = ["baseline", "bottom", "middle", "sub", "super", "text-bottom", "text-top", "top"];
267 /**
268 * List of all block-level (outer) display types.
269 * * https://www.w3.org/TR/css-display-3/#display-type
270 * * https://www.w3.org/TR/css-display-3/#block-level
271 */
272 public const BLOCK_LEVEL_TYPES = [
273 "block",
274 // "flow-root",
275 "list-item",
276 // "flex",
277 // "grid",
278 "table",
279 ];
280 /**
281 * List of all inline-level (outer) display types.
282 * * https://www.w3.org/TR/css-display-3/#display-type
283 * * https://www.w3.org/TR/css-display-3/#inline-level
284 */
285 public const INLINE_LEVEL_TYPES = [
286 "inline",
287 "inline-block",
288 // "inline-flex",
289 // "inline-grid",
290 "inline-table",
291 ];
292 /**
293 * List of all table-internal (outer) display types.
294 * * https://www.w3.org/TR/css-display-3/#layout-specific-display
295 */
296 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"];
297 /**
298 * List of all inline (inner) display types.
299 */
300 public const INLINE_TYPES = ["inline"];
301 /**
302 * List of all block (inner) display types.
303 */
304 public const BLOCK_TYPES = ["block", "inline-block", "table-cell", "list-item"];
305 /**
306 * List of all table (inner) display types.
307 */
308 public const TABLE_TYPES = ["table", "inline-table"];
309 /**
310 * Lookup table for valid display types. Initially computed from the
311 * different constants.
312 *
313 * @var array
314 */
315 protected static $valid_display_types = [];
316 /**
317 * List of all positioned types.
318 */
319 public const POSITIONED_TYPES = ["relative", "absolute", "fixed"];
320 /**
321 * List of valid border styles.
322 */
323 public const BORDER_STYLES = ["none", "hidden", "dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset"];
324 /**
325 * List of valid outline-style values.
326 * Same as the border styles, except `auto` is allowed, `hidden` is not.
327 *
328 * @link https://www.w3.org/TR/css-ui-4/#typedef-outline-line-style
329 */
330 protected const OUTLINE_STYLES = ["auto", "none", "dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset"];
331 /**
332 * Map of CSS shorthand properties and their corresponding sub-properties.
333 * The order of the sub-properties is relevant for the fallback getter,
334 * which is used in case no specific getter method is defined.
335 *
336 * @var array<string, string[]>
337 */
338 protected static $_props_shorthand = ["background" => [
339 "background_image",
340 "background_position",
341 "background_size",
342 "background_repeat",
343 // "background_origin",
344 // "background_clip",
345 "background_attachment",
346 "background_color",
347 ], "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" => [
348 "font_family",
349 "font_size",
350 // "font_stretch",
351 "font_style",
352 "font_variant",
353 "font_weight",
354 "line_height",
355 ], "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"]];
356 /**
357 * Maps legacy property names to actual property names.
358 *
359 * @var array<string, string>
360 */
361 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"];
362 /**
363 * Default style values.
364 *
365 * @link https://www.w3.org/TR/CSS21/propidx.html
366 *
367 * @var array<string, mixed>
368 */
369 protected static $_defaults = null;
370 /**
371 * Lookup table for properties that inherit by default.
372 *
373 * @link https://www.w3.org/TR/CSS21/propidx.html
374 *
375 * @var array<string, true>
376 */
377 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];
378 /**
379 * @var array<string, string[]>
380 */
381 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"]];
382 /**
383 * Lookup table for dependent properties. Initially computed from the
384 * dependency map.
385 *
386 * @var array<string, true>
387 */
388 protected static $_dependent_props = [];
389 /**
390 * Caches method_exists result
391 *
392 * @var array<string, bool>
393 */
394 protected static $_methods_cache = [];
395 /**
396 * The stylesheet this style belongs to
397 *
398 * @var Stylesheet
399 */
400 protected $_stylesheet;
401 /**
402 * Media queries attached to the style
403 *
404 * This is a two-dimensional array where the first dimension represents
405 * the media query grouping (logic-or) and the second dimension the
406 * media queries within the grouping.
407 *
408 * The structure of the actual query element is:
409 * - media query feature
410 * - media query value or condition
411 * - media query operator (e.g., not)
412 *
413 * @var array
414 */
415 protected $_media_queries;
416 /**
417 * Properties set by an `!important` declaration.
418 *
419 * @var array<string, true>
420 */
421 protected $_important_props = [];
422 /**
423 * Specified (or declared) values of the CSS properties.
424 *
425 * https://www.w3.org/TR/css-cascade-3/#value-stages
426 *
427 * @var array<string, mixed>
428 */
429 protected $_props = [];
430 /**
431 * Used to track which CSS property were set directly versus
432 * those set via shorthand property
433 *
434 * @var array<string, true>
435 */
436 protected $_props_specified = [];
437 /**
438 * Computed values of the CSS properties.
439 *
440 * @var array<string, mixed>
441 */
442 protected $_props_computed = [];
443 /**
444 * Used values of the CSS properties.
445 *
446 * @var array<string, mixed>
447 */
448 protected $_props_used = [];
449 /**
450 * Marks properties with non-final used values that should be cleared on
451 * style reset.
452 *
453 * @var array<string, true>
454 */
455 protected $non_final_used = [];
456 /**
457 * Used to track CSS property assignment entry/exit in order to watch
458 * for circular dependencies.
459 *
460 * @var array<int, string>
461 */
462 protected $_prop_stack = [];
463 /**
464 * Used to track CSS variable resolution entry/exit in order to watch
465 * for circular dependencies.
466 *
467 * @var array<int, string>
468 */
469 protected $_var_stack = [];
470 /**
471 * Style of the parent element in document tree.
472 *
473 * @var Style
474 */
475 protected $parent_style;
476 /**
477 * @var Frame|null
478 */
479 protected $_frame;
480 /**
481 * The origin of the style
482 *
483 * @var int
484 */
485 protected $_origin = Stylesheet::ORIG_AUTHOR;
486 /**
487 * The computed bottom spacing
488 *
489 * @var float|string|null
490 */
491 private $_computed_bottom_spacing = null;
492 /**
493 * @var bool|null
494 */
495 private $has_border_radius_cache = null;
496 /**
497 * @var array|null
498 */
499 private $resolved_border_radius = null;
500 /**
501 * @var FontMetrics
502 */
503 private $fontMetrics;
504 /**
505 * @param Stylesheet $stylesheet The stylesheet the style is associated with.
506 * @param int $origin
507 */
508 public function __construct(Stylesheet $stylesheet, int $origin = Stylesheet::ORIG_AUTHOR)
509 {
510 $this->fontMetrics = $stylesheet->getFontMetrics();
511 $this->_stylesheet = $stylesheet;
512 $this->_media_queries = [];
513 $this->_origin = $origin;
514 $this->parent_style = null;
515 if (!isset(self::$_defaults)) {
516 // Shorthand
517 $d =& self::$_defaults;
518 // All CSS 2.1 properties, and their default values
519 // Some properties are specified with their computed value for
520 // efficiency; this only works if the computed value is not
521 // dependent on another property
522 $d["azimuth"] = "center";
523 $d["background_attachment"] = "scroll";
524 $d["background_color"] = "transparent";
525 $d["background_image"] = "none";
526 $d["background_image_resolution"] = "normal";
527 $d["background_position"] = [0.0, 0.0];
528 $d["background_repeat"] = "repeat";
529 $d["background"] = "";
530 $d["border_collapse"] = "separate";
531 $d["border_color"] = "";
532 $d["border_spacing"] = [0.0, 0.0];
533 $d["border_style"] = "";
534 $d["border_top"] = "";
535 $d["border_right"] = "";
536 $d["border_bottom"] = "";
537 $d["border_left"] = "";
538 $d["border_top_color"] = "currentcolor";
539 $d["border_right_color"] = "currentcolor";
540 $d["border_bottom_color"] = "currentcolor";
541 $d["border_left_color"] = "currentcolor";
542 $d["border_top_style"] = "none";
543 $d["border_right_style"] = "none";
544 $d["border_bottom_style"] = "none";
545 $d["border_left_style"] = "none";
546 $d["border_top_width"] = "medium";
547 $d["border_right_width"] = "medium";
548 $d["border_bottom_width"] = "medium";
549 $d["border_left_width"] = "medium";
550 $d["border_width"] = "";
551 $d["border_bottom_left_radius"] = 0.0;
552 $d["border_bottom_right_radius"] = 0.0;
553 $d["border_top_left_radius"] = 0.0;
554 $d["border_top_right_radius"] = 0.0;
555 $d["border_radius"] = "";
556 $d["border"] = "";
557 $d["bottom"] = "auto";
558 $d["caption_side"] = "top";
559 $d["clear"] = "none";
560 $d["clip"] = "auto";
561 $d["color"] = "#000000";
562 $d["content"] = "normal";
563 $d["counter_increment"] = "none";
564 $d["counter_reset"] = "none";
565 $d["cue_after"] = "none";
566 $d["cue_before"] = "none";
567 $d["cue"] = "";
568 $d["cursor"] = "auto";
569 $d["direction"] = "ltr";
570 $d["display"] = "inline";
571 $d["elevation"] = "level";
572 $d["empty_cells"] = "show";
573 $d["float"] = "none";
574 $d["font_family"] = $stylesheet->get_dompdf()->getOptions()->getDefaultFont();
575 $d["font_size"] = "medium";
576 $d["font_style"] = "normal";
577 $d["font_variant"] = "normal";
578 $d["font_weight"] = 400;
579 $d["font"] = "";
580 $d["height"] = "auto";
581 $d["image_resolution"] = "normal";
582 $d["inset"] = "";
583 $d["left"] = "auto";
584 $d["letter_spacing"] = "normal";
585 $d["line_height"] = "normal";
586 $d["list_style_image"] = "none";
587 $d["list_style_position"] = "outside";
588 $d["list_style_type"] = "disc";
589 $d["list_style"] = "";
590 $d["margin_right"] = 0.0;
591 $d["margin_left"] = 0.0;
592 $d["margin_top"] = 0.0;
593 $d["margin_bottom"] = 0.0;
594 $d["margin"] = "";
595 $d["max_height"] = "none";
596 $d["max_width"] = "none";
597 $d["min_height"] = "auto";
598 $d["min_width"] = "auto";
599 $d["orphans"] = 2;
600 $d["outline_color"] = "currentcolor";
601 // "invert" special color is not supported
602 $d["outline_style"] = "none";
603 $d["outline_width"] = "medium";
604 $d["outline_offset"] = 0.0;
605 $d["outline"] = "";
606 $d["overflow"] = "visible";
607 $d["overflow_wrap"] = "normal";
608 $d["padding_top"] = 0.0;
609 $d["padding_right"] = 0.0;
610 $d["padding_bottom"] = 0.0;
611 $d["padding_left"] = 0.0;
612 $d["padding"] = "";
613 $d["page_break_after"] = "auto";
614 $d["page_break_before"] = "auto";
615 $d["page_break_inside"] = "auto";
616 $d["pause_after"] = "0";
617 $d["pause_before"] = "0";
618 $d["pause"] = "";
619 $d["pitch_range"] = "50";
620 $d["pitch"] = "medium";
621 $d["play_during"] = "auto";
622 $d["position"] = "static";
623 $d["quotes"] = "auto";
624 $d["richness"] = "50";
625 $d["right"] = "auto";
626 $d["size"] = "auto";
627 // @page
628 $d["speak_header"] = "once";
629 $d["speak_numeral"] = "continuous";
630 $d["speak_punctuation"] = "none";
631 $d["speak"] = "normal";
632 $d["speech_rate"] = "medium";
633 $d["stress"] = "50";
634 $d["table_layout"] = "auto";
635 $d["text_align"] = "";
636 $d["text_decoration"] = "none";
637 $d["text_indent"] = 0.0;
638 $d["text_transform"] = "none";
639 $d["top"] = "auto";
640 $d["unicode_bidi"] = "normal";
641 $d["vertical_align"] = "baseline";
642 $d["visibility"] = "visible";
643 $d["voice_family"] = "";
644 $d["volume"] = "medium";
645 $d["white_space"] = "normal";
646 $d["widows"] = 2;
647 $d["width"] = "auto";
648 $d["word_break"] = "normal";
649 $d["word_spacing"] = "normal";
650 $d["z_index"] = "auto";
651 // CSS3
652 $d["opacity"] = 1.0;
653 $d["background_size"] = ["auto", "auto"];
654 $d["transform"] = [];
655 $d["transform_origin"] = ["50%", "50%", 0.0];
656 // for @font-face
657 $d["src"] = "";
658 $d["unicode_range"] = "";
659 // vendor-prefixed properties
660 $d["_dompdf_keep"] = "";
661 // Compute dependent props from dependency map
662 foreach (self::$_dependency_map as $props) {
663 foreach ($props as $prop) {
664 self::$_dependent_props[$prop] = \true;
665 }
666 }
667 // Compute valid display-type lookup table
668 self::$valid_display_types = ["none" => \true, "-dompdf-br" => \true, "-dompdf-image" => \true, "-dompdf-list-bullet" => \true, "-dompdf-page" => \true];
669 foreach (self::BLOCK_LEVEL_TYPES as $val) {
670 self::$valid_display_types[$val] = \true;
671 }
672 foreach (self::INLINE_LEVEL_TYPES as $val) {
673 self::$valid_display_types[$val] = \true;
674 }
675 foreach (self::TABLE_INTERNAL_TYPES as $val) {
676 self::$valid_display_types[$val] = \true;
677 }
678 }
679 }
680 /**
681 * Clear all non-final used values.
682 */
683 public function reset() : void
684 {
685 foreach (\array_keys($this->non_final_used) as $prop) {
686 unset($this->_props_used[$prop]);
687 }
688 $this->non_final_used = [];
689 }
690 /**
691 * @param array $media_queries
692 */
693 public function set_media_queries(array $media_queries) : void
694 {
695 $this->_media_queries = $media_queries;
696 }
697 /**
698 * @return array
699 */
700 public function get_media_queries() : array
701 {
702 return $this->_media_queries;
703 }
704 /**
705 * @param Frame $frame
706 */
707 public function set_frame(Frame $frame) : void
708 {
709 $this->_frame = $frame;
710 }
711 /**
712 * @return Frame|null
713 */
714 public function get_frame() : ?Frame
715 {
716 return $this->_frame;
717 }
718 /**
719 * @param int $origin
720 */
721 public function set_origin(int $origin) : void
722 {
723 $this->_origin = $origin;
724 }
725 /**
726 * @return int
727 */
728 public function get_origin() : int
729 {
730 return $this->_origin;
731 }
732 /**
733 * Returns the {@link Stylesheet} the style is associated with.
734 *
735 * @return Stylesheet
736 */
737 public function get_stylesheet() : Stylesheet
738 {
739 return $this->_stylesheet;
740 }
741 public function is_custom_property(string $prop) : bool
742 {
743 return \substr($prop, 0, 2) === "--";
744 }
745 public function is_absolute() : bool
746 {
747 $position = $this->__get("position");
748 return $position === "absolute" || $position === "fixed";
749 }
750 public function is_in_flow() : bool
751 {
752 $float = $this->__get("float");
753 return $float === "none" && !$this->is_absolute();
754 }
755 /**
756 * Converts any CSS length value into an absolute length in points.
757 *
758 * length_in_pt() takes a single length (e.g. '1em') or an array of
759 * lengths and returns an absolute length. If an array is passed, then
760 * the return value is the sum of all elements. If any of the lengths
761 * provided are "auto" or "none" then that value is returned.
762 *
763 * If a reference size is not provided, the current font size is used.
764 *
765 * @param float|string|array $length The numeric length (or string measurement) or array of lengths to resolve.
766 * @param float|null $ref_size An absolute reference size to resolve percentage lengths.
767 *
768 * @return float|string
769 */
770 public function length_in_pt($length, ?float $ref_size = null)
771 {
772 $font_size = $this->__get("font_size");
773 $ref_size = $ref_size ?? $font_size;
774 if (!\is_array($length)) {
775 $length = [$length];
776 }
777 $ret = 0.0;
778 foreach ($length as $l) {
779 if ($l === "auto" || $l === "none") {
780 return $l;
781 }
782 // Assume numeric values are already in points
783 if (\is_numeric($l)) {
784 $ret += (float) $l;
785 continue;
786 }
787 $val = $this->single_length_in_pt((string) $l, $ref_size, $font_size);
788 $ret += $val ?? 0;
789 }
790 return $ret;
791 }
792 /**
793 * Convert a length declaration to pt.
794 *
795 * @param string $l The length declaration.
796 * @param float $ref_size Reference size for percentage declarations.
797 * @param float|null $font_size Font size for resolving font-size relative units.
798 *
799 * @return float|null The length in pt, or `null` for invalid declarations.
800 */
801 protected function single_length_in_pt(string $l, float $ref_size = 0, ?float $font_size = null) : ?float
802 {
803 static $cache = [];
804 $font_size = $font_size ?? $this->__get("font_size");
805 $dpi = $this->_stylesheet->get_dompdf()->getOptions()->getDpi();
806 $key = "{$l}/{$dpi}/{$ref_size}/{$font_size}";
807 if (\array_key_exists($key, $cache)) {
808 return $cache[$key];
809 }
810 $number = self::CSS_NUMBER;
811 $pattern = "/^({$number})([a-zA-Z%]*)?\$/";
812 if (!\preg_match($pattern, $l, $matches)) {
813 $ident = self::CSS_IDENTIFIER;
814 $pattern = "/^({$ident})\\(.*\\)\$/i";
815 if (\preg_match($pattern, $l)) {
816 $value = $this->evaluate_func($this->parse_func($l), $ref_size, $font_size);
817 return $cache[$key] = $value;
818 }
819 return null;
820 }
821 $v = (float) $matches[1];
822 $unit = \strtolower($matches[2]);
823 if ($unit === "") {
824 // Legacy support for unitless values, not covered by spec. Might
825 // want to restrict this to unitless `0` in the future
826 $value = $v;
827 } elseif ($unit === "%") {
828 $value = $v / 100 * $ref_size;
829 } elseif ($unit === "px") {
830 $value = $v * 72 / $dpi;
831 } elseif ($unit === "pt") {
832 $value = $v;
833 } elseif ($unit === "rem") {
834 $tree = $this->_stylesheet->get_dompdf()->getTree();
835 $root_style = $tree !== null ? $tree->get_root()->get_style() : null;
836 $root_font_size = $root_style === null || $root_style === $this ? $font_size : $root_style->__get("font_size");
837 $value = $v * $root_font_size;
838 // Skip caching if the root style is not available yet, as to avoid
839 // incorrectly cached values if the root font size is different from
840 // the default
841 if ($root_style === null) {
842 return $value;
843 }
844 } elseif ($unit === "em") {
845 $value = $v * $font_size;
846 } elseif ($unit === "cm") {
847 $value = $v * 72 / 2.54;
848 } elseif ($unit === "mm") {
849 $value = $v * 72 / 25.4;
850 } elseif ($unit === "ex") {
851 // FIXME: em:ex ratio?
852 $value = $v * $font_size / 2;
853 } elseif ($unit === "in") {
854 $value = $v * 72;
855 } elseif ($unit === "pc") {
856 $value = $v * 12;
857 } else {
858 // Invalid or unsupported declaration
859 $value = null;
860 }
861 return $cache[$key] = $value;
862 }
863 /**
864 * Shunting-yard Algorithm
865 * @param string $expr infix expression
866 * @return array
867 */
868 private function parse_func(string $expr) : array
869 {
870 if (\substr_count($expr, '(') !== \substr_count($expr, ')')) {
871 return [];
872 }
873 $expr = \str_replace(['(', ')', '*', '/', ','], [' ( ', ' ) ', ' * ', ' / ', ' , '], $expr);
874 $expr = \trim(\preg_replace('/\\s+/', ' ', $expr));
875 if ($expr === '') {
876 return [];
877 }
878 $precedence = ['*' => 3, '/' => 3, '+' => 2, '-' => 2, ',' => 1];
879 $opStack = [];
880 $queue = [];
881 $parts = \explode(' ', $expr);
882 foreach ($parts as $part) {
883 if ($part === '(') {
884 $opStack[] = $part;
885 } elseif (\array_key_exists(\strtolower($part), self::CSS_MATH_FUNCTIONS)) {
886 $opStack[] = \strtolower($part);
887 } elseif ($part === ')') {
888 while (\count($opStack) > 0 && \end($opStack) !== '(' && !\array_key_exists(\end($opStack), self::CSS_MATH_FUNCTIONS)) {
889 $queue[] = \array_pop($opStack);
890 }
891 if (\end($opStack) === '(') {
892 \array_pop($opStack);
893 }
894 if (\count($opStack) > 0 && \array_key_exists(\end($opStack), self::CSS_MATH_FUNCTIONS)) {
895 $queue[] = \array_pop($opStack);
896 }
897 } elseif (\array_key_exists($part, $precedence)) {
898 while (\count($opStack) > 0 && \end($opStack) !== '(' && $precedence[\end($opStack)] >= $precedence[$part]) {
899 $queue[] = \array_pop($opStack);
900 }
901 $opStack[] = $part;
902 } else {
903 $queue[] = $part;
904 }
905 }
906 while (\count($opStack) > 0) {
907 $queue[] = \array_pop($opStack);
908 }
909 return $queue;
910 }
911 /**
912 * Reverse Polish Notation
913 * @param array $rpn
914 * @param float $ref_size
915 * @param float|null $font_size
916 * @return float|null
917 */
918 private function evaluate_func(array $rpn, float $ref_size = 0, ?float $font_size = null) : ?float
919 {
920 if (\count($rpn) === 0) {
921 return null;
922 }
923 $ops = ['*', '/', '+', '-', ','];
924 $stack = [];
925 foreach ($rpn as $part) {
926 if (\array_key_exists($part, self::CSS_MATH_FUNCTIONS)) {
927 $argv = \array_pop($stack);
928 if (!\is_array($argv)) {
929 $argv = [$argv];
930 }
931 $argc = \count($argv);
932 switch ($part) {
933 case 'abs':
934 case 'acos':
935 case 'asin':
936 case 'atan':
937 case 'cos':
938 case 'exp':
939 case 'sin':
940 case 'sqrt':
941 case 'tan':
942 if ($argc !== 1) {
943 return null;
944 }
945 $stack[] = \call_user_func_array($part, $argv);
946 break;
947 case 'atan2':
948 case 'hypot':
949 case 'pow':
950 if ($argc !== 2) {
951 return null;
952 }
953 $stack[] = \call_user_func_array($part, $argv);
954 break;
955 case 'log':
956 if ($argc === 1) {
957 $stack[] = \log($argv[0]);
958 } elseif ($argc === 2) {
959 $stack[] = \log($argv[0], $argv[1]);
960 } else {
961 return null;
962 }
963 break;
964 case 'max':
965 $stack[] = \max($argv);
966 break;
967 case 'min':
968 $stack[] = \min($argv);
969 break;
970 case 'mod':
971 if ($argc !== 2 || $argv[1] === 0.0) {
972 return null;
973 }
974 if ($argv[1] > 0) {
975 $stack[] = $argv[0] - \floor($argv[0] / $argv[1]) * $argv[1];
976 } else {
977 $stack[] = $argv[0] - \ceil($argv[0] * -1 / $argv[1]) * $argv[1] * -1;
978 }
979 break;
980 case 'rem':
981 if ($argc !== 2 || $argv[1] === 0.0) {
982 return null;
983 }
984 $stack[] = $argv[0] - \intval($argv[0] / $argv[1]) * $argv[1];
985 break;
986 case 'round':
987 if ($argc !== 2 || $argv[1] === 0.0) {
988 return null;
989 }
990 if ($argv[0] >= 0) {
991 $stack[] = \round($argv[0] / $argv[1], 0, \PHP_ROUND_HALF_UP) * $argv[1];
992 } else {
993 $stack[] = \round($argv[0] / $argv[1], 0, \PHP_ROUND_HALF_DOWN) * $argv[1];
994 }
995 break;
996 case 'calc':
997 if ($argc !== 1) {
998 return null;
999 }
1000 $stack[] = $argv[0];
1001 break;
1002 case 'clamp':
1003 if ($argc !== 3) {
1004 return null;
1005 }
1006 $stack[] = \max($argv[0], \min($argv[1], $argv[2]));
1007 break;
1008 case 'sign':
1009 if ($argc !== 1) {
1010 return null;
1011 }
1012 $stack[] = $argv[0] == 0 ? 0.0 : $argv[0] / \abs($argv[0]);
1013 break;
1014 default:
1015 return null;
1016 }
1017 } elseif (\in_array($part, $ops, \true)) {
1018 $rightValue = \array_pop($stack);
1019 $leftValue = \array_pop($stack);
1020 if ($rightValue === null || $leftValue === null) {
1021 return null;
1022 }
1023 switch ($part) {
1024 case '*':
1025 $stack[] = $leftValue * $rightValue;
1026 break;
1027 case '/':
1028 if ($rightValue === 0.0) {
1029 return null;
1030 }
1031 $stack[] = $leftValue / $rightValue;
1032 break;
1033 case '+':
1034 $stack[] = $leftValue + $rightValue;
1035 break;
1036 case '-':
1037 $stack[] = $leftValue - $rightValue;
1038 break;
1039 case ',':
1040 if (\is_array($leftValue)) {
1041 $leftValue[] = $rightValue;
1042 $stack[] = $leftValue;
1043 } else {
1044 $stack[] = [$leftValue, $rightValue];
1045 }
1046 break;
1047 }
1048 } else {
1049 $val = $this->single_length_in_pt($part, $ref_size, $font_size);
1050 if ($val === null) {
1051 return null;
1052 }
1053 $stack[] = $val;
1054 }
1055 }
1056 if (\count($stack) > 1) {
1057 return null;
1058 }
1059 return \floatval(\end($stack));
1060 }
1061 /**
1062 * Resolves the actual values for used CSS custom properties.
1063 *
1064 * This function receives the whole content of the var() function, which
1065 * can also include a fallback value.
1066 */
1067 private function parse_var($matches)
1068 {
1069 $variable = \is_array($matches) ? $matches[1] : $matches;
1070 if (\in_array($variable, $this->_var_stack, \true)) {
1071 return null;
1072 }
1073 \array_push($this->_var_stack, $variable);
1074 // Split property name and an optional fallback value.
1075 [$custom_prop, $fallback] = \explode(',', $variable, 2) + ['', ''];
1076 $fallback = \trim($fallback);
1077 // Try to retrieve the custom property value, or use the fallback value
1078 // if the value could not be resolved.
1079 $value = $this->computed($custom_prop) ?? $fallback;
1080 // If the resolved value also has vars in it, resolve again.
1081 $pattern = self::CSS_VAR;
1082 $value = \preg_replace_callback("/{$pattern}/", [$this, "parse_var"], $value);
1083 \array_pop($this->_var_stack);
1084 return $value ?: null;
1085 }
1086 /**
1087 * Resolve inherited property values using the provided parent style or the
1088 * default values, in case no parent style exists.
1089 *
1090 * https://www.w3.org/TR/css-cascade-3/#inheriting
1091 *
1092 * @param Style|null $parent
1093 */
1094 public function inherit(?Style $parent = null) : void
1095 {
1096 $this->parent_style = $parent;
1097 // Clear the computed font size, as it might depend on the parent
1098 // font size
1099 unset($this->_props_computed["font_size"]);
1100 unset($this->_props_used["font_size"]);
1101 if ($parent) {
1102 // For properties that inherit by default: When the cascade did
1103 // not result in a value, inherit the parent value. Inheritance
1104 // is handled via the specific sub-properties for shorthands. Custom
1105 // properties (variables) are selected by the -- prefix.
1106 foreach ($parent->_props as $prop => $val) {
1107 if (!isset($this->_props[$prop]) && (isset(self::$_inherited[$prop]) || $this->is_custom_property($prop))) {
1108 $parent_val = $parent->computed($prop);
1109 if ($this->is_custom_property($prop)) {
1110 $this->set_prop($prop, $parent_val);
1111 } else {
1112 $this->_props[$prop] = $parent_val;
1113 $this->_props_computed[$prop] = $parent_val;
1114 $this->_props_used[$prop] = null;
1115 }
1116 }
1117 }
1118 }
1119 foreach ($this->_props as $prop => $val) {
1120 if ($val === "inherit") {
1121 if ($parent && isset($parent->_props[$prop])) {
1122 $parent_val = $parent->computed($prop);
1123 if ($this->is_custom_property($prop)) {
1124 $this->set_prop($prop, $parent_val);
1125 } else {
1126 $this->_props[$prop] = $parent_val;
1127 $this->_props_computed[$prop] = $parent_val;
1128 $this->_props_used[$prop] = null;
1129 }
1130 } else {
1131 if ($this->is_custom_property($prop)) {
1132 $this->set_prop($prop, "unset");
1133 } else {
1134 // Parent prop not set, use default
1135 $this->_props[$prop] = self::$_defaults[$prop];
1136 unset($this->_props_computed[$prop]);
1137 unset($this->_props_used[$prop]);
1138 }
1139 }
1140 }
1141 }
1142 }
1143 /**
1144 * Override properties in this style with those in $style
1145 *
1146 * @param Style $style
1147 */
1148 public function merge(Style $style) : void
1149 {
1150 foreach ($style->_props as $prop => $val) {
1151 $important = isset($style->_important_props[$prop]);
1152 // `!important` declarations take precedence over normal ones
1153 if (!$important && isset($this->_important_props[$prop])) {
1154 continue;
1155 }
1156 if ($important) {
1157 $this->_important_props[$prop] = \true;
1158 }
1159 if ($this->is_custom_property($prop)) {
1160 $this->set_prop($prop, $val, $important);
1161 } else {
1162 $this->_props[$prop] = $val;
1163 }
1164 // Copy an existing computed value only for non-dependent
1165 // properties; otherwise it may be invalid for the current style
1166 if (!isset(self::$_dependent_props[$prop]) && \array_key_exists($prop, $style->_props_computed)) {
1167 $this->_props_computed[$prop] = $style->_props_computed[$prop];
1168 $this->_props_used[$prop] = null;
1169 } else {
1170 unset($this->_props_computed[$prop]);
1171 unset($this->_props_used[$prop]);
1172 }
1173 if (\array_key_exists($prop, $style->_props_specified)) {
1174 $this->_props_specified[$prop] = \true;
1175 }
1176 }
1177 // re-evalutate CSS variables
1178 foreach (\array_keys($this->_props) as $prop) {
1179 if (!$this->is_custom_property($prop)) {
1180 continue;
1181 }
1182 $this->set_prop($prop, $this->_props[$prop], isset($this->_important_props[$prop]));
1183 }
1184 }
1185 /**
1186 * Clear information about important declarations after the style has been
1187 * finalized during stylesheet loading.
1188 */
1189 public function clear_important() : void
1190 {
1191 $this->_important_props = [];
1192 }
1193 /**
1194 * Clear border-radius and bottom-spacing cache as necessary when a given
1195 * property is set.
1196 *
1197 * @param string $prop The property that is set.
1198 */
1199 protected function clear_cache(string $prop) : void
1200 {
1201 // Clear border-radius cache on setting any border-radius
1202 // property
1203 if ($prop === "border_top_left_radius" || $prop === "border_top_right_radius" || $prop === "border_bottom_left_radius" || $prop === "border_bottom_right_radius") {
1204 $this->has_border_radius_cache = null;
1205 $this->resolved_border_radius = null;
1206 }
1207 // Clear bottom-spacing cache if necessary. Border style can
1208 // disable/enable border calculations
1209 if ($prop === "margin_bottom" || $prop === "padding_bottom" || $prop === "border_bottom_width" || $prop === "border_bottom_style") {
1210 $this->_computed_bottom_spacing = null;
1211 }
1212 }
1213 /**
1214 * Set a style property from a value declaration.
1215 *
1216 * Setting `$clear_dependencies` to `false` is useful for saving a bit of
1217 * unnecessary work while loading stylesheets.
1218 *
1219 * @param string $prop The property to set.
1220 * @param mixed $val The value declaration or computed value.
1221 * @param bool $important Whether the declaration is important.
1222 * @param bool $clear_dependencies Whether to clear computed values of dependent properties.
1223 */
1224 public function set_prop(string $prop, $val, bool $important = \false, bool $clear_dependencies = \true) : void
1225 {
1226 // Skip some checks for CSS custom properties.
1227 if (!$this->is_custom_property($prop)) {
1228 $prop = \str_replace("-", "_", $prop);
1229 // Legacy property aliases
1230 if (isset(self::$_props_alias[$prop])) {
1231 $prop = self::$_props_alias[$prop];
1232 }
1233 if (!isset(self::$_defaults[$prop])) {
1234 global $_dompdf_warnings;
1235 $_dompdf_warnings[] = "'{$prop}' is not a recognized CSS property.";
1236 return;
1237 }
1238 }
1239 $this->_props_specified[$prop] = \true;
1240 // Trim declarations unconditionally, but only lower-case for comparison
1241 // with the general keywords. Properties must handle case-insensitive
1242 // comparisons individually
1243 if (\is_string($val)) {
1244 $val = \trim($val);
1245 $lower = \strtolower($val);
1246 if ($lower === "initial" || $lower === "inherit" || $lower === "unset") {
1247 $val = $lower;
1248 }
1249 }
1250 if (isset(self::$_props_shorthand[$prop])) {
1251 // Shorthand properties directly set their respective sub-properties
1252 // https://www.w3.org/TR/css-cascade-3/#shorthand
1253 if ($val === "initial" || $val === "inherit" || $val === "unset") {
1254 foreach (self::$_props_shorthand[$prop] as $sub_prop) {
1255 $this->set_prop($sub_prop, $val, $important, $clear_dependencies);
1256 }
1257 } else {
1258 $method = "_set_{$prop}";
1259 // Resolve the CSS custom property value(s).
1260 $pattern = self::CSS_VAR;
1261 // Always set the specified value for properties that use CSS variables
1262 // so that an invalid initial value does not prevent re-computation later.
1263 $this->_props[$prop] = $val;
1264 //TODO: we shouldn't need to parse this twice
1265 \preg_match_all("/{$pattern}/", $val, $matches, \PREG_SET_ORDER);
1266 foreach ($matches as $match) {
1267 if ($this->parse_var($match) === null) {
1268 // unset specified as for specific prop under expectation it will be overridden
1269 foreach (self::$_props_shorthand[$prop] as $sub_prop) {
1270 unset($this->_props_specified[$sub_prop]);
1271 }
1272 return;
1273 }
1274 }
1275 $val = \preg_replace_callback("/{$pattern}/", [$this, "parse_var"], $val);
1276 if (!isset(self::$_methods_cache[$method])) {
1277 self::$_methods_cache[$method] = \method_exists($this, $method);
1278 }
1279 if (self::$_methods_cache[$method]) {
1280 $values = $this->{$method}($val);
1281 if ($values === []) {
1282 return;
1283 }
1284 // Each missing sub-property is assigned its initial value
1285 // https://www.w3.org/TR/css-cascade-3/#shorthand
1286 foreach (self::$_props_shorthand[$prop] as $sub_prop) {
1287 $sub_val = $values[$sub_prop] ?? self::$_defaults[$sub_prop];
1288 $this->set_prop($sub_prop, $sub_val, $important, $clear_dependencies);
1289 unset($this->_props_specified[$sub_prop]);
1290 }
1291 }
1292 }
1293 } else {
1294 // Legacy support for `word-break: break-word`
1295 // https://www.w3.org/TR/css-text-3/#valdef-word-break-break-word
1296 if ($prop === "word_break" && \is_string($val) && \strcasecmp($val, "break-word") === 0) {
1297 $val = "normal";
1298 $this->set_prop("overflow_wrap", "anywhere", $important, $clear_dependencies);
1299 }
1300 // `!important` declarations take precedence over normal ones
1301 if (!$important && isset($this->_important_props[$prop])) {
1302 return;
1303 }
1304 if ($important) {
1305 $this->_important_props[$prop] = \true;
1306 }
1307 // https://www.w3.org/TR/css-cascade-3/#inherit-initial
1308 if ($val === "unset") {
1309 $val = isset(self::$_inherited[$prop]) || $this->is_custom_property($prop) ? "inherit" : "initial";
1310 }
1311 // https://www.w3.org/TR/css-cascade-3/#valdef-all-initial
1312 if ($val === "initial" && !$this->is_custom_property($prop)) {
1313 $val = self::$_defaults[$prop];
1314 }
1315 // Always set the specified value for properties that use CSS variables
1316 // so that an invalid initial value does not prevent re-computation later.
1317 if (\is_string($val) && \preg_match("/" . self::CSS_VAR . "/", $val)) {
1318 $this->_props[$prop] = $val;
1319 }
1320 $computed = $this->compute_prop($prop, $val);
1321 // Skip invalid declarations
1322 if ($computed === null) {
1323 return;
1324 }
1325 $this->_props[$prop] = $val;
1326 $this->_props_computed[$prop] = $computed;
1327 $this->_props_used[$prop] = null;
1328 //TODO: this should be a directed dependency map
1329 if ($this->is_custom_property($prop) && !\in_array($prop, $this->_prop_stack, \true)) {
1330 \array_push($this->_prop_stack, $prop);
1331 $specified_props = \array_filter($this->_props, function ($key) {
1332 return \array_key_exists($key, $this->_props_specified);
1333 }, \ARRAY_FILTER_USE_KEY);
1334 // copy existing props filtered by those set explicitly before parsing vars
1335 foreach ($specified_props as $specified_prop => $specified_value) {
1336 if (!$this->is_custom_property($specified_prop) || \strpos($specified_value, "var({$prop}") !== \false) {
1337 $this->set_prop($specified_prop, $specified_value, isset($this->_important_props[$specified_prop]), \true);
1338 if (isset(self::$_props_shorthand[$specified_prop])) {
1339 foreach (self::$_props_shorthand[$specified_prop] as $sub_prop) {
1340 if (\array_key_exists($sub_prop, $specified_props)) {
1341 $this->set_prop($sub_prop, $specified_props[$sub_prop], isset($this->_important_props[$sub_prop]), \true);
1342 }
1343 }
1344 }
1345 }
1346 }
1347 \array_pop($this->_prop_stack);
1348 }
1349 if ($clear_dependencies) {
1350 // Clear the computed values of any dependent properties, so
1351 // they can be re-computed
1352 if (isset(self::$_dependency_map[$prop])) {
1353 foreach (self::$_dependency_map[$prop] as $dependent) {
1354 unset($this->_props_computed[$dependent]);
1355 unset($this->_props_used[$dependent]);
1356 }
1357 }
1358 $this->clear_cache($prop);
1359 }
1360 }
1361 }
1362 /**
1363 * Get the specified value of a style property.
1364 *
1365 * @param string $prop
1366 *
1367 * @return mixed
1368 * @throws Exception
1369 */
1370 public function get_specified(string $prop)
1371 {
1372 // Legacy property aliases
1373 if (isset(self::$_props_alias[$prop])) {
1374 $prop = self::$_props_alias[$prop];
1375 }
1376 if (!isset(self::$_defaults[$prop]) && !$this->is_custom_property($prop)) {
1377 throw new Exception("'{$prop}' is not a recognized CSS property.");
1378 }
1379 return $this->_props[$prop] ?? self::$_defaults[$prop];
1380 }
1381 /**
1382 * Set a style property to its final value.
1383 *
1384 * This sets the specified and used value of the style property to the given
1385 * value, meaning the value is not parsed and thus should have a type
1386 * compatible with the property.
1387 *
1388 * If a shorthand property is specified, all of its sub-properties are set
1389 * to the given value.
1390 *
1391 * @param string $prop The property to set.
1392 * @param mixed $val The final value of the property.
1393 *
1394 * @throws Exception
1395 */
1396 public function __set(string $prop, $val)
1397 {
1398 // Legacy property aliases
1399 if (isset(self::$_props_alias[$prop])) {
1400 $prop = self::$_props_alias[$prop];
1401 }
1402 if (!isset(self::$_defaults[$prop]) && !$this->is_custom_property($prop)) {
1403 throw new Exception("'{$prop}' is not a recognized CSS property.");
1404 }
1405 if (isset(self::$_props_shorthand[$prop])) {
1406 foreach (self::$_props_shorthand[$prop] as $sub_prop) {
1407 $this->__set($sub_prop, $val);
1408 }
1409 } else {
1410 $this->_props[$prop] = $val;
1411 $this->_props_computed[$prop] = $val;
1412 $this->_props_used[$prop] = $val;
1413 $this->clear_cache($prop);
1414 }
1415 }
1416 /**
1417 * Set the used value of a style property.
1418 *
1419 * Used values are cleared on style reset.
1420 *
1421 * If a shorthand property is specified, all of its sub-properties are set
1422 * to the given value.
1423 *
1424 * @param string $prop The property to set.
1425 * @param mixed $val The used value of the property.
1426 *
1427 * @throws Exception
1428 */
1429 public function set_used(string $prop, $val) : void
1430 {
1431 // Legacy property aliases
1432 if (isset(self::$_props_alias[$prop])) {
1433 $prop = self::$_props_alias[$prop];
1434 }
1435 if (!isset(self::$_defaults[$prop])) {
1436 throw new Exception("'{$prop}' is not a recognized CSS property.");
1437 }
1438 if (isset(self::$_props_shorthand[$prop])) {
1439 foreach (self::$_props_shorthand[$prop] as $sub_prop) {
1440 $this->set_used($sub_prop, $val);
1441 }
1442 } else {
1443 $this->_props_used[$prop] = $val;
1444 $this->non_final_used[$prop] = \true;
1445 }
1446 }
1447 /**
1448 * Get the used or computed value of a style property, depending on whether
1449 * the used value has been determined yet.
1450 *
1451 * @param string $prop
1452 *
1453 * @return mixed
1454 * @throws Exception
1455 */
1456 public function __get(string $prop)
1457 {
1458 // Legacy property aliases
1459 if (isset(self::$_props_alias[$prop])) {
1460 $prop = self::$_props_alias[$prop];
1461 }
1462 if (!isset(self::$_defaults[$prop]) && !$this->is_custom_property($prop)) {
1463 throw new Exception("'{$prop}' is not a recognized CSS property.");
1464 }
1465 if (isset($this->_props_used[$prop])) {
1466 return $this->_props_used[$prop];
1467 }
1468 $method = "_get_{$prop}";
1469 if (!isset(self::$_methods_cache[$method])) {
1470 self::$_methods_cache[$method] = \method_exists($this, $method);
1471 }
1472 if (isset(self::$_props_shorthand[$prop])) {
1473 // Don't cache shorthand values, always use getter. If no dedicated
1474 // getter exists, use a simple fallback getter concatenating all
1475 // sub-property values
1476 if (self::$_methods_cache[$method]) {
1477 return $this->{$method}();
1478 } else {
1479 return \implode(" ", \array_map(function ($sub_prop) {
1480 $val = $this->__get($sub_prop);
1481 return \is_array($val) ? \implode(" ", $val) : $val;
1482 }, self::$_props_shorthand[$prop]));
1483 }
1484 } else {
1485 $computed = $this->computed($prop);
1486 $used = self::$_methods_cache[$method] ? $this->{$method}($computed) : $computed;
1487 $this->_props_used[$prop] = $used;
1488 return $used;
1489 }
1490 }
1491 /**
1492 * @param string $prop The property to compute.
1493 * @param mixed $val The value to compute. Non-string values are treated as already computed.
1494 *
1495 * @return mixed The computed value.
1496 */
1497 protected function compute_prop(string $prop, $val)
1498 {
1499 // During style merge, the parent style is not available yet, so
1500 // temporarily use the initial value for `inherit` properties. The
1501 // keyword is properly resolved during inheritance
1502 if ($val === "inherit" && !$this->is_custom_property($prop)) {
1503 $val = self::$_defaults[$prop];
1504 }
1505 // Check for values which are already computed
1506 if (!\is_string($val)) {
1507 return $val;
1508 }
1509 // Resolve the CSS custom property value(s).
1510 $pattern = self::CSS_VAR;
1511 $val = \preg_replace_callback("/{$pattern}/", [$this, "parse_var"], $val);
1512 $method = "_compute_{$prop}";
1513 if (!isset(self::$_methods_cache[$method])) {
1514 self::$_methods_cache[$method] = \method_exists($this, $method);
1515 }
1516 if (self::$_methods_cache[$method]) {
1517 return $this->{$method}($val);
1518 } elseif ($val !== "") {
1519 return \strtolower($val);
1520 } else {
1521 return null;
1522 }
1523 }
1524 /**
1525 * Get the computed value for the given property.
1526 *
1527 * @param string $prop The property to get the computed value of.
1528 *
1529 * @return mixed The computed value.
1530 */
1531 protected function computed(string $prop)
1532 {
1533 if (!\array_key_exists($prop, $this->_props_computed)) {
1534 if (!\array_key_exists($prop, $this->_props) && $this->is_custom_property($prop)) {
1535 return null;
1536 }
1537 $val = $this->_props[$prop] ?? self::$_defaults[$prop];
1538 $computed = $this->compute_prop($prop, $val);
1539 if ($computed === null) {
1540 if ($this->is_custom_property($prop)) {
1541 return null;
1542 }
1543 $computed = $this->compute_prop($prop, self::$_defaults[$prop]);
1544 }
1545 $this->_props_computed[$prop] = $computed;
1546 }
1547 return $this->_props_computed[$prop];
1548 }
1549 /**
1550 * @param float $cbw The width of the containing block.
1551 * @return float|string|null
1552 */
1553 public function computed_bottom_spacing(float $cbw)
1554 {
1555 // Caching the bottom spacing independently of the given width is a bit
1556 // iffy, but should be okay, as the containing block should only
1557 // potentially change after a page break, and the style is reset in that
1558 // case
1559 if ($this->_computed_bottom_spacing !== null) {
1560 return $this->_computed_bottom_spacing;
1561 }
1562 return $this->_computed_bottom_spacing = $this->length_in_pt([$this->margin_bottom, $this->padding_bottom, $this->border_bottom_width], $cbw);
1563 }
1564 /**
1565 * Returns an `array(r, g, b, "r" => r, "g" => g, "b" => b, "alpha" => alpha, "hex" => "#rrggbb")`
1566 * based on the provided CSS color value.
1567 *
1568 * @param string|null $color
1569 * @return array|string|null
1570 */
1571 public function munge_color($color)
1572 {
1573 return Color::parse($color);
1574 }
1575 /**
1576 * @return string
1577 */
1578 public function get_font_family_raw() : string
1579 {
1580 return \trim($this->_props["font_family"], " \t\n\r\v\"'");
1581 }
1582 /**
1583 * @return string[]
1584 */
1585 public function get_font_family_computed() : array
1586 {
1587 return $this->computed("font_family");
1588 }
1589 /**
1590 * Getter for the `font-family` CSS property.
1591 *
1592 * Uses the {@link FontMetrics} class to resolve the font family into an
1593 * actual font file.
1594 *
1595 * @param string[] $computed
1596 * @return string
1597 *
1598 * @throws Exception
1599 *
1600 * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-family
1601 */
1602 protected function _get_font_family($computed) : string
1603 {
1604 // TODO: It probably makes sense to perform the font selection outside
1605 // the Style class completely. It is now done primarily in
1606 // `FrameDecorator\Text::apply_font_mapping`
1607 // Select the appropriate font. First determine the subtype, then check
1608 // the specified font-families for a candidate.
1609 $fontMetrics = $this->getFontMetrics();
1610 $weight = $this->__get("font_weight");
1611 $fontStyle = $this->__get("font_style");
1612 $subtype = $fontMetrics->getType($weight . ' ' . $fontStyle);
1613 foreach ($computed as $family) {
1614 $font = $fontMetrics->getFont($family, $subtype);
1615 if ($font !== null) {
1616 return $font;
1617 }
1618 }
1619 $font = $fontMetrics->getFont(null, $subtype);
1620 if ($font !== null) {
1621 return $font;
1622 }
1623 $specified = \implode(", ", $computed);
1624 throw new Exception("Unable to find a suitable font replacement for: '{$specified}'");
1625 }
1626 /**
1627 * @param float $computed
1628 * @return float
1629 *
1630 * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-size
1631 */
1632 protected function _get_font_size($computed)
1633 {
1634 // Computed value may be negative when specified via `calc()`
1635 return \max($computed, 0.0);
1636 }
1637 /**
1638 * @param float|string $computed
1639 * @return float
1640 *
1641 * @link https://www.w3.org/TR/css-text-4/#word-spacing-property
1642 */
1643 protected function _get_word_spacing($computed)
1644 {
1645 if (\is_float($computed)) {
1646 return $computed;
1647 }
1648 // Resolve percentage values
1649 $font_size = $this->__get("font_size");
1650 return $this->single_length_in_pt($computed, $font_size);
1651 }
1652 /**
1653 * @param float|string $computed
1654 * @return float
1655 *
1656 * @link https://www.w3.org/TR/css-text-4/#letter-spacing-property
1657 */
1658 protected function _get_letter_spacing($computed)
1659 {
1660 if (\is_float($computed)) {
1661 return $computed;
1662 }
1663 // Resolve percentage values
1664 $font_size = $this->__get("font_size");
1665 return $this->single_length_in_pt($computed, $font_size);
1666 }
1667 /**
1668 * @param float|string $computed
1669 * @return float
1670 *
1671 * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-line-height
1672 */
1673 protected function _get_line_height($computed)
1674 {
1675 // Lengths have been computed to float, number values to string
1676 if (\is_float($computed)) {
1677 // Computed value may be negative when specified via `calc()`
1678 return \max($computed, 0.0);
1679 }
1680 $font_size = $this->__get("font_size");
1681 $factor = $computed === "normal" ? self::$default_line_height : (float) $computed;
1682 return $factor * $font_size;
1683 }
1684 /**
1685 * @param string $computed
1686 * @param bool $current_is_parent
1687 *
1688 * @return array|string
1689 */
1690 protected function get_color_value($computed, bool $current_is_parent = \false)
1691 {
1692 if ($computed === "currentcolor") {
1693 // https://www.w3.org/TR/css-color-4/#resolving-other-colors
1694 if ($current_is_parent) {
1695 // Use the `color` value from the parent for the `color`
1696 // property itself
1697 return isset($this->parent_style) ? $this->parent_style->__get("color") : $this->munge_color(self::$_defaults["color"]);
1698 }
1699 return $this->__get("color");
1700 }
1701 return $this->munge_color($computed) ?? "transparent";
1702 }
1703 /**
1704 * Returns the color as an array
1705 *
1706 * The array has the following format:
1707 * `array(r, g, b, "r" => r, "g" => g, "b" => b, "alpha" => alpha, "hex" => "#rrggbb")`
1708 *
1709 * @param string $computed
1710 * @return array|string
1711 *
1712 * @link https://www.w3.org/TR/CSS21/colors.html#propdef-color
1713 */
1714 protected function _get_color($computed)
1715 {
1716 return $this->get_color_value($computed, \true);
1717 }
1718 /**
1719 * Returns the background color as an array
1720 *
1721 * See {@link Style::_get_color()} for format of the color array.
1722 *
1723 * @param string $computed
1724 * @return array|string
1725 *
1726 * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-color
1727 */
1728 protected function _get_background_color($computed)
1729 {
1730 return $this->get_color_value($computed);
1731 }
1732 /**
1733 * Returns the background image URI, or "none"
1734 *
1735 * @param string $computed
1736 * @return string
1737 *
1738 * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-image
1739 */
1740 protected function _get_background_image($computed) : string
1741 {
1742 return $this->_stylesheet->resolve_url($computed, \true);
1743 }
1744 /**
1745 * Returns the border color as an array
1746 *
1747 * See {@link Style::_get_color()} for format of the color array.
1748 *
1749 * @param string $computed
1750 * @return array|string
1751 *
1752 * @link https://www.w3.org/TR/CSS21/box.html#border-color-properties
1753 */
1754 protected function _get_border_top_color($computed)
1755 {
1756 return $this->get_color_value($computed);
1757 }
1758 /**
1759 * @param string $computed
1760 * @return array|string
1761 */
1762 protected function _get_border_right_color($computed)
1763 {
1764 return $this->get_color_value($computed);
1765 }
1766 /**
1767 * @param string $computed
1768 * @return array|string
1769 */
1770 protected function _get_border_bottom_color($computed)
1771 {
1772 return $this->get_color_value($computed);
1773 }
1774 /**
1775 * @param string $computed
1776 * @return array|string
1777 */
1778 protected function _get_border_left_color($computed)
1779 {
1780 return $this->get_color_value($computed);
1781 }
1782 /**
1783 * Return an array of all border properties.
1784 *
1785 * The returned array has the following structure:
1786 *
1787 * ```
1788 * array("top" => array("width" => [border-width],
1789 * "style" => [border-style],
1790 * "color" => [border-color (array)]),
1791 * "bottom" ... )
1792 * ```
1793 *
1794 * @return array
1795 */
1796 public function get_border_properties() : array
1797 {
1798 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")]];
1799 }
1800 /**
1801 * Return a single border-side property
1802 *
1803 * @param string $side
1804 * @return string
1805 */
1806 protected function get_border_side(string $side) : string
1807 {
1808 $color = $this->__get("border_{$side}_color");
1809 return $this->__get("border_{$side}_width") . " " . $this->__get("border_{$side}_style") . " " . (\is_array($color) ? $color["hex"] : $color);
1810 }
1811 /**
1812 * Return full border properties as a string
1813 *
1814 * Border properties are returned just as specified in CSS:
1815 * `[width] [style] [color]`
1816 * e.g. "1px solid blue"
1817 *
1818 * @return string
1819 *
1820 * @link https://www.w3.org/TR/CSS21/box.html#border-shorthand-properties
1821 */
1822 protected function _get_border_top() : string
1823 {
1824 return $this->get_border_side("top");
1825 }
1826 /**
1827 * @return string
1828 */
1829 protected function _get_border_right() : string
1830 {
1831 return $this->get_border_side("right");
1832 }
1833 /**
1834 * @return string
1835 */
1836 protected function _get_border_bottom() : string
1837 {
1838 return $this->get_border_side("bottom");
1839 }
1840 /**
1841 * @return string
1842 */
1843 protected function _get_border_left() : string
1844 {
1845 return $this->get_border_side("left");
1846 }
1847 public function has_border_radius() : bool
1848 {
1849 if (isset($this->has_border_radius_cache)) {
1850 return $this->has_border_radius_cache;
1851 }
1852 // Use a fixed ref size here. We don't know the border-box width here
1853 // and font size might be 0. Since we are only interested in whether
1854 // there is any border radius at all, this should do
1855 $tl = (float) $this->length_in_pt($this->border_top_left_radius, 12);
1856 $tr = (float) $this->length_in_pt($this->border_top_right_radius, 12);
1857 $br = (float) $this->length_in_pt($this->border_bottom_right_radius, 12);
1858 $bl = (float) $this->length_in_pt($this->border_bottom_left_radius, 12);
1859 $this->has_border_radius_cache = $tl + $tr + $br + $bl > 0;
1860 return $this->has_border_radius_cache;
1861 }
1862 /**
1863 * Get the final border-radius values to use.
1864 *
1865 * Percentage values are resolved relative to the width of the border box.
1866 * The border radius is additionally scaled for the given render box, and
1867 * constrained by its width and height.
1868 *
1869 * @param float[] $border_box The border box of the frame.
1870 * @param float[]|null $render_box The box to resolve the border radius for.
1871 *
1872 * @return float[] A 4-tuple of top-left, top-right, bottom-right, and bottom-left radius.
1873 */
1874 public function resolve_border_radius(array $border_box, ?array $render_box = null) : array
1875 {
1876 $render_box = $render_box ?? $border_box;
1877 $use_cache = $render_box === $border_box;
1878 if ($use_cache && isset($this->resolved_border_radius)) {
1879 return $this->resolved_border_radius;
1880 }
1881 [$x, $y, $w, $h] = $border_box;
1882 // Resolve percentages relative to width, as long as we have no support
1883 // for per-axis radii
1884 $tl = (float) $this->length_in_pt($this->border_top_left_radius, $w);
1885 $tr = (float) $this->length_in_pt($this->border_top_right_radius, $w);
1886 $br = (float) $this->length_in_pt($this->border_bottom_right_radius, $w);
1887 $bl = (float) $this->length_in_pt($this->border_bottom_left_radius, $w);
1888 if ($tl + $tr + $br + $bl > 0) {
1889 [$rx, $ry, $rw, $rh] = $render_box;
1890 $t_offset = $y - $ry;
1891 $r_offset = $rx + $rw - $x - $w;
1892 $b_offset = $ry + $rh - $y - $h;
1893 $l_offset = $x - $rx;
1894 if ($tl > 0) {
1895 $tl = \max($tl + ($t_offset + $l_offset) / 2, 0);
1896 }
1897 if ($tr > 0) {
1898 $tr = \max($tr + ($t_offset + $r_offset) / 2, 0);
1899 }
1900 if ($br > 0) {
1901 $br = \max($br + ($b_offset + $r_offset) / 2, 0);
1902 }
1903 if ($bl > 0) {
1904 $bl = \max($bl + ($b_offset + $l_offset) / 2, 0);
1905 }
1906 if ($tl + $bl > $rh) {
1907 $f = $rh / ($tl + $bl);
1908 $tl = $f * $tl;
1909 $bl = $f * $bl;
1910 }
1911 if ($tr + $br > $rh) {
1912 $f = $rh / ($tr + $br);
1913 $tr = $f * $tr;
1914 $br = $f * $br;
1915 }
1916 if ($tl + $tr > $rw) {
1917 $f = $rw / ($tl + $tr);
1918 $tl = $f * $tl;
1919 $tr = $f * $tr;
1920 }
1921 if ($bl + $br > $rw) {
1922 $f = $rw / ($bl + $br);
1923 $bl = $f * $bl;
1924 $br = $f * $br;
1925 }
1926 }
1927 $values = [$tl, $tr, $br, $bl];
1928 if ($use_cache) {
1929 $this->resolved_border_radius = $values;
1930 }
1931 return $values;
1932 }
1933 /**
1934 * Returns the outline color as an array
1935 *
1936 * See {@link Style::_get_color()} for format of the color array.
1937 *
1938 * @param string $computed
1939 * @return array|string
1940 *
1941 * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-color
1942 */
1943 protected function _get_outline_color($computed)
1944 {
1945 return $this->get_color_value($computed);
1946 }
1947 /**
1948 * @param string $computed
1949 * @return string
1950 *
1951 * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-style
1952 */
1953 protected function _get_outline_style($computed) : string
1954 {
1955 return $computed === "auto" ? "solid" : $computed;
1956 }
1957 /**
1958 * Return full outline properties as a string
1959 *
1960 * Outline properties are returned just as specified in CSS:
1961 * `[width] [style] [color]`
1962 * e.g. "1px solid blue"
1963 *
1964 * @return string
1965 *
1966 * @link https://www.w3.org/TR/CSS21/box.html#border-shorthand-properties
1967 */
1968 protected function _get_outline() : string
1969 {
1970 $color = $this->__get("outline_color");
1971 return $this->__get("outline_width") . " " . $this->__get("outline_style") . " " . (\is_array($color) ? $color["hex"] : $color);
1972 }
1973 /**
1974 * Returns the list style image URI, or "none"
1975 *
1976 * @param string $computed
1977 * @return string
1978 *
1979 * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-image
1980 */
1981 protected function _get_list_style_image($computed) : string
1982 {
1983 return $this->_stylesheet->resolve_url($computed, \true);
1984 }
1985 /**
1986 * @param array|string $computed
1987 * @return array|string
1988 *
1989 * @link https://www.w3.org/TR/css-content-3/#quotes
1990 */
1991 protected function _get_quotes($computed)
1992 {
1993 if ($computed === "auto") {
1994 // TODO: Use typographically appropriate quotes for the current
1995 // language here
1996 return [['"', '"'], ["'", "'"]];
1997 }
1998 return $computed;
1999 }
2000 /*==============================*/
2001 /**
2002 * Parse a property value into its components.
2003 *
2004 * @param string $value
2005 *
2006 * @return string[]
2007 */
2008 protected function parse_property_value(string $value) : array
2009 {
2010 $string = self::CSS_STRING;
2011 $ident = self::CSS_IDENTIFIER;
2012 $number = self::CSS_NUMBER;
2013 $pattern = "/\n" . "\\s* (?<string>{$string}) |\n" . "\\s* (url \\( (?> (\\\\[\"'()] | [^\"'()])* ) (?<!\\\\)\\) ) |\n" . "\\s* ({$ident} (\\( ((?> \\g<string> | [^\"'()]+ ) | (?-2))* \\)) ) |\n" . "\\s* ({$ident}) |\n" . "\\s* (\\#[0-9a-fA-F]*) |\n" . "\\s* ({$number} [a-zA-Z%]*) |\n" . "\\s* ([\\/,;]) \n" . "/iSx";
2014 if (!\preg_match_all($pattern, $value, $matches)) {
2015 return [];
2016 }
2017 return \array_map("trim", $matches[0]);
2018 }
2019 protected function is_color_value(string $val) : bool
2020 {
2021 return $val === "currentcolor" || $val === "transparent" || isset(Color::$cssColorNames[$val]) || \preg_match("/^#|rgb\\(|rgba\\(|cmyk\\(/", $val);
2022 }
2023 /**
2024 * @param string $val
2025 * @return string|null
2026 */
2027 protected function compute_color_value(string $val) : ?string
2028 {
2029 // https://www.w3.org/TR/css-color-4/#resolving-other-colors
2030 $val = \strtolower($val);
2031 $munged_color = $val !== "currentcolor" ? $this->munge_color($val) : $val;
2032 if ($munged_color === null) {
2033 return null;
2034 }
2035 return \is_array($munged_color) ? $munged_color["hex"] : $munged_color;
2036 }
2037 /**
2038 * @param string $val
2039 * @return int|null
2040 */
2041 protected function compute_integer(string $val) : ?int
2042 {
2043 $integer = self::CSS_INTEGER;
2044 return \preg_match("/^{$integer}\$/", $val) ? (int) $val : null;
2045 }
2046 /**
2047 * @param string $val
2048 * @return float|null
2049 */
2050 protected function compute_number(string $val) : ?float
2051 {
2052 $number = self::CSS_NUMBER;
2053 return \preg_match("/^{$number}\$/", $val) ? (float) $val : null;
2054 }
2055 /**
2056 * @param string $val
2057 * @return float|null
2058 */
2059 protected function compute_length(string $val) : ?float
2060 {
2061 return \strpos($val, "%") === \false ? $this->single_length_in_pt($val) : null;
2062 }
2063 /**
2064 * @param string $val
2065 * @return float|null
2066 */
2067 protected function compute_length_positive(string $val) : ?float
2068 {
2069 $computed = $this->compute_length($val);
2070 // Negative non-`calc` values are invalid
2071 if ($computed === null || $computed < 0 && !\preg_match("/^-?[_a-zA-Z]/", $val)) {
2072 return null;
2073 }
2074 return $computed;
2075 }
2076 /**
2077 * @param string $val
2078 * @return float|string|null
2079 */
2080 protected function compute_length_percentage(string $val)
2081 {
2082 // Compute with a fixed ref size to decide whether percentage values
2083 // are valid
2084 $computed = $this->single_length_in_pt($val, 12);
2085 if ($computed === null) {
2086 return null;
2087 }
2088 // Retain valid percentage declarations
2089 return \strpos($val, "%") === \false ? $computed : $val;
2090 }
2091 /**
2092 * @param string $val
2093 * @return float|string|null
2094 */
2095 protected function compute_length_percentage_positive(string $val)
2096 {
2097 // Compute with a fixed ref size to decide whether percentage values
2098 // are valid
2099 $computed = $this->single_length_in_pt($val, 12);
2100 // Negative non-`calc` values are invalid
2101 if ($computed === null || $computed < 0 && !\preg_match("/^-?[_a-zA-Z]/", $val)) {
2102 return null;
2103 }
2104 // Retain valid percentage declarations
2105 return \strpos($val, "%") === \false ? $computed : $val;
2106 }
2107 /**
2108 * @param string $val
2109 * @param string $style_prop The corresponding border-/outline-style property.
2110 *
2111 * @return float|null
2112 *
2113 * @link https://www.w3.org/TR/css-backgrounds-3/#typedef-line-width
2114 */
2115 protected function compute_line_width(string $val, string $style_prop) : ?float
2116 {
2117 $val = \strtolower($val);
2118 // Border-width keywords
2119 if ($val === "thin") {
2120 $computed = 0.5;
2121 } elseif ($val === "medium") {
2122 $computed = 1.5;
2123 } elseif ($val === "thick") {
2124 $computed = 2.5;
2125 } else {
2126 $computed = $this->compute_length_positive($val);
2127 }
2128 if ($computed === null) {
2129 return null;
2130 }
2131 // Computed width is 0 if the line style is `none` or `hidden`
2132 // https://www.w3.org/TR/css-backgrounds-3/#border-width
2133 // https://www.w3.org/TR/css-ui-4/#outline-width
2134 $lineStyle = $this->__get($style_prop);
2135 $hasLineStyle = $lineStyle !== "none" && $lineStyle !== "hidden";
2136 return $hasLineStyle ? $computed : 0.0;
2137 }
2138 /**
2139 * @param string $val
2140 * @return string|null
2141 */
2142 protected function compute_border_style(string $val) : ?string
2143 {
2144 $val = \strtolower($val);
2145 return \in_array($val, self::BORDER_STYLES, \true) ? $val : null;
2146 }
2147 /**
2148 * @param string $val
2149 * @return float|null
2150 *
2151 * @link https://www.w3.org/TR/css3-values/#angles
2152 */
2153 protected function compute_angle_or_zero(string $val) : ?float
2154 {
2155 $number = self::CSS_NUMBER;
2156 $pattern = "/^({$number})(deg|grad|rad|turn)?\$/i";
2157 if (!\preg_match($pattern, $val, $matches)) {
2158 return null;
2159 }
2160 $v = (float) $matches[1];
2161 $unit = \strtolower($matches[2] ?? "");
2162 switch ($unit) {
2163 case "deg":
2164 return $v;
2165 case "grad":
2166 return $v * 0.9;
2167 case "rad":
2168 return \rad2deg($v);
2169 case "turn":
2170 return $v * 360;
2171 default:
2172 return $v === 0.0 ? $v : null;
2173 }
2174 }
2175 /**
2176 * Common computation logic for `background-position` and `transform-origin`.
2177 *
2178 * @param string $v1
2179 * @param string $v2
2180 *
2181 * @return (float|string|null)[]
2182 */
2183 protected function computeBackgroundPositionTransformOrigin(string $v1, string $v2) : array
2184 {
2185 $x = null;
2186 $y = null;
2187 switch ($v1) {
2188 case "left":
2189 $x = 0.0;
2190 break;
2191 case "right":
2192 $x = "100%";
2193 break;
2194 case "top":
2195 $y = 0.0;
2196 break;
2197 case "bottom":
2198 $y = "100%";
2199 break;
2200 case "center":
2201 if ($v2 === "left" || $v2 === "right") {
2202 $y = "50%";
2203 } else {
2204 $x = "50%";
2205 }
2206 break;
2207 default:
2208 $x = $this->compute_length_percentage($v1);
2209 break;
2210 }
2211 switch ($v2) {
2212 case "left":
2213 $x = 0.0;
2214 break;
2215 case "right":
2216 $x = "100%";
2217 break;
2218 case "top":
2219 $y = 0.0;
2220 break;
2221 case "bottom":
2222 $y = "100%";
2223 break;
2224 case "center":
2225 if ($v1 === "top" || $v1 === "bottom") {
2226 $x = "50%";
2227 } else {
2228 $y = "50%";
2229 }
2230 break;
2231 default:
2232 $y = $this->compute_length_percentage($v2);
2233 break;
2234 }
2235 return [$x, $y];
2236 }
2237 /**
2238 * @link https://www.w3.org/TR/css-lists-3/#typedef-counter-name
2239 */
2240 protected function isValidCounterName(string $name) : bool
2241 {
2242 return $name !== "none" && !\in_array($name, self::CUSTOM_IDENT_FORBIDDEN, \true);
2243 }
2244 /**
2245 * @link https://www.w3.org/TR/css-counter-styles-3/#typedef-counter-style-name
2246 */
2247 protected function isValidCounterStyleName(string $name) : bool
2248 {
2249 return $name !== "none" && !\in_array($name, self::CUSTOM_IDENT_FORBIDDEN, \true);
2250 }
2251 /**
2252 * Parse a property value with 1 to 4 components into 4 values, as required
2253 * by shorthand properties such as `margin`, `padding`, and `border-radius`.
2254 *
2255 * @param string $prop The shorthand property with exactly 4 sub-properties to handle.
2256 * @param string $value The property value to parse.
2257 *
2258 * @return string[]
2259 */
2260 protected function set_quad_shorthand(string $prop, string $value) : array
2261 {
2262 $v = $this->parse_property_value($value);
2263 switch (\count($v)) {
2264 case 1:
2265 $values = [$v[0], $v[0], $v[0], $v[0]];
2266 break;
2267 case 2:
2268 $values = [$v[0], $v[1], $v[0], $v[1]];
2269 break;
2270 case 3:
2271 $values = [$v[0], $v[1], $v[2], $v[1]];
2272 break;
2273 case 4:
2274 $values = [$v[0], $v[1], $v[2], $v[3]];
2275 break;
2276 default:
2277 return [];
2278 }
2279 return \array_combine(self::$_props_shorthand[$prop], $values);
2280 }
2281 /*======================*/
2282 /**
2283 * @link https://www.w3.org/TR/CSS21/visuren.html#display-prop
2284 */
2285 protected function _compute_display(string $val)
2286 {
2287 $val = \strtolower($val);
2288 // Make sure that common valid, but unsupported display types have an
2289 // appropriate fallback display type
2290 switch ($val) {
2291 case "flow-root":
2292 case "flex":
2293 case "grid":
2294 case "table-caption":
2295 $val = "block";
2296 break;
2297 case "inline-flex":
2298 case "inline-grid":
2299 $val = "inline-block";
2300 break;
2301 }
2302 if (!isset(self::$valid_display_types[$val])) {
2303 return null;
2304 }
2305 // https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
2306 if ($this->is_in_flow()) {
2307 return $val;
2308 } else {
2309 switch ($val) {
2310 case "inline":
2311 case "inline-block":
2312 // case "table-row-group":
2313 // case "table-header-group":
2314 // case "table-footer-group":
2315 // case "table-row":
2316 // case "table-cell":
2317 // case "table-column-group":
2318 // case "table-column":
2319 // case "table-caption":
2320 return "block";
2321 case "inline-table":
2322 return "table";
2323 default:
2324 return $val;
2325 }
2326 }
2327 }
2328 /**
2329 * @link https://www.w3.org/TR/CSS21/colors.html#propdef-color
2330 */
2331 protected function _compute_color(string $color)
2332 {
2333 return $this->compute_color_value($color);
2334 }
2335 /**
2336 * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-color
2337 */
2338 protected function _compute_background_color(string $color)
2339 {
2340 return $this->compute_color_value($color);
2341 }
2342 /**
2343 * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-image
2344 */
2345 protected function _compute_background_image(string $val)
2346 {
2347 $parsed_val = $this->_stylesheet->resolve_url($val);
2348 if ($parsed_val === "none") {
2349 return "none";
2350 } else {
2351 return "url(\"" . \str_replace("\"", "\\\"", $parsed_val) . "\")";
2352 }
2353 }
2354 /**
2355 * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-repeat
2356 */
2357 protected function _compute_background_repeat(string $val)
2358 {
2359 $keywords = ["repeat", "repeat-x", "repeat-y", "no-repeat"];
2360 $val = \strtolower($val);
2361 return \in_array($val, $keywords, \true) ? $val : null;
2362 }
2363 /**
2364 * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-attachment
2365 */
2366 protected function _compute_background_attachment(string $val)
2367 {
2368 $keywords = ["scroll", "fixed"];
2369 $val = \strtolower($val);
2370 return \in_array($val, $keywords, \true) ? $val : null;
2371 }
2372 /**
2373 * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-position
2374 */
2375 protected function _compute_background_position(string $val)
2376 {
2377 $val = \strtolower($val);
2378 $parts = $this->parse_property_value($val);
2379 $count = \count($parts);
2380 if ($count === 0 || $count > 2) {
2381 return null;
2382 }
2383 $v1 = $parts[0];
2384 $v2 = $parts[1] ?? "center";
2385 [$x, $y] = $this->computeBackgroundPositionTransformOrigin($v1, $v2);
2386 if ($x === null || $y === null) {
2387 return null;
2388 }
2389 return [$x, $y];
2390 }
2391 /**
2392 * Compute `background-size`.
2393 *
2394 * Computes to one of the following values:
2395 * * `cover`
2396 * * `contain`
2397 * * `[width, height]`, each being a length, percentage, or `auto`
2398 *
2399 * @link https://www.w3.org/TR/css-backgrounds-3/#background-size
2400 */
2401 protected function _compute_background_size(string $val)
2402 {
2403 $val = \strtolower($val);
2404 if ($val === "cover" || $val === "contain") {
2405 return $val;
2406 }
2407 $parts = $this->parse_property_value($val);
2408 $count = \count($parts);
2409 if ($count === 0 || $count > 2) {
2410 return null;
2411 }
2412 $width = $parts[0];
2413 if ($width !== "auto") {
2414 $width = $this->compute_length_percentage_positive($width);
2415 }
2416 $height = $parts[1] ?? "auto";
2417 if ($height !== "auto") {
2418 $height = $this->compute_length_percentage_positive($height);
2419 }
2420 if ($width === null || $height === null) {
2421 return null;
2422 }
2423 return [$width, $height];
2424 }
2425 /**
2426 * @link https://www.w3.org/TR/css-backgrounds-3/#propdef-background
2427 */
2428 protected function _set_background(string $value) : array
2429 {
2430 $components = $this->parse_property_value($value);
2431 $props = [];
2432 $pos_size = [];
2433 foreach ($components as $val) {
2434 $lower = \strtolower($val);
2435 if ($lower === "none") {
2436 $props["background_image"] = $lower;
2437 } elseif (\strncmp($lower, "url(", 4) === 0) {
2438 $props["background_image"] = $val;
2439 } elseif ($lower === "scroll" || $lower === "fixed") {
2440 $props["background_attachment"] = $lower;
2441 } elseif ($lower === "repeat" || $lower === "repeat-x" || $lower === "repeat-y" || $lower === "no-repeat") {
2442 $props["background_repeat"] = $lower;
2443 } elseif ($this->is_color_value($lower)) {
2444 $props["background_color"] = $lower;
2445 } else {
2446 $pos_size[] = $lower;
2447 }
2448 }
2449 if (\count($pos_size)) {
2450 // Split value list at "/"
2451 $index = \array_search("/", $pos_size, \true);
2452 if ($index !== \false) {
2453 $pos = \array_slice($pos_size, 0, $index);
2454 $size = \array_slice($pos_size, $index + 1);
2455 } else {
2456 $pos = $pos_size;
2457 $size = [];
2458 }
2459 $props["background_position"] = \implode(" ", $pos);
2460 if (\count($size)) {
2461 $props["background_size"] = \implode(" ", $size);
2462 }
2463 }
2464 return $props;
2465 }
2466 /**
2467 * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-family
2468 */
2469 protected function _compute_font_family(string $val)
2470 {
2471 return \array_map(function ($name) {
2472 return \trim($name, " '\"");
2473 }, \preg_split("/\\s*,\\s*/", $val));
2474 }
2475 /**
2476 * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-size
2477 */
2478 protected function _compute_font_size(string $val)
2479 {
2480 $val = \strtolower($val);
2481 $parentFontSize = isset($this->parent_style) ? $this->parent_style->__get("font_size") : self::$default_font_size;
2482 switch ($val) {
2483 case "xx-small":
2484 case "x-small":
2485 case "small":
2486 case "medium":
2487 case "large":
2488 case "x-large":
2489 case "xx-large":
2490 $computed = self::$default_font_size * self::$font_size_keywords[$val];
2491 break;
2492 case "smaller":
2493 $computed = 8 / 9 * $parentFontSize;
2494 break;
2495 case "larger":
2496 $computed = 6 / 5 * $parentFontSize;
2497 break;
2498 default:
2499 $computed = $this->single_length_in_pt($val, $parentFontSize, $parentFontSize);
2500 // Negative non-`calc` values are invalid
2501 if ($computed === null || $computed < 0 && !\preg_match("/^-?[_a-zA-Z]/", $val)) {
2502 return null;
2503 }
2504 break;
2505 }
2506 return $computed;
2507 }
2508 /**
2509 * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-style
2510 */
2511 protected function _compute_font_style(string $val)
2512 {
2513 $val = \strtolower($val);
2514 return $val === "normal" || $val === "italic" || $val === "oblique" ? $val : null;
2515 }
2516 /**
2517 * @link https://www.w3.org/TR/css-fonts-4/#propdef-font-weight
2518 */
2519 protected function _compute_font_weight(string $val)
2520 {
2521 $val = \strtolower($val);
2522 switch ($val) {
2523 case "normal":
2524 return 400;
2525 case "bold":
2526 return 700;
2527 case "bolder":
2528 // https://www.w3.org/TR/css-fonts-4/#relative-weights
2529 $w = isset($this->parent_style) ? $this->parent_style->__get("font_weight") : 400;
2530 if ($w < 350) {
2531 return 400;
2532 } elseif ($w < 550) {
2533 return 700;
2534 } elseif ($w < 900) {
2535 return 900;
2536 } else {
2537 return $w;
2538 }
2539 case "lighter":
2540 // https://www.w3.org/TR/css-fonts-4/#relative-weights
2541 $w = isset($this->parent_style) ? $this->parent_style->__get("font_weight") : 400;
2542 if ($w < 100) {
2543 return $w;
2544 } elseif ($w < 550) {
2545 return 100;
2546 } elseif ($w < 750) {
2547 return 400;
2548 } else {
2549 return 700;
2550 }
2551 default:
2552 $number = self::CSS_NUMBER;
2553 $weight = \preg_match("/^{$number}\$/", $val) ? (int) $val : null;
2554 return $weight !== null && $weight >= 1 && $weight <= 1000 ? $weight : null;
2555 }
2556 }
2557 /**
2558 * @link https://www.w3.org/TR/css-fonts-4/#src-desc
2559 */
2560 protected function _compute_src(string $val)
2561 {
2562 return $val;
2563 }
2564 /**
2565 * Handle the `font` shorthand property.
2566 *
2567 * `[ font-style || font-variant || font-weight ] font-size [ / line-height ] font-family`
2568 *
2569 * @link https://www.w3.org/TR/CSS21/fonts.html#font-shorthand
2570 */
2571 protected function _set_font(string $value) : array
2572 {
2573 $value = \strtolower($value);
2574 $components = $this->parse_property_value($value);
2575 $props = [];
2576 $number = self::CSS_NUMBER;
2577 $unit = "pt|px|pc|rem|em|ex|in|cm|mm|%";
2578 $sizePattern = "/^(xx-small|x-small|small|medium|large|x-large|xx-large|smaller|larger|{$number}(?:{$unit})|0)\$/";
2579 $sizeIndex = null;
2580 // Find index of font-size to split the component list
2581 foreach ($components as $i => $val) {
2582 if (\preg_match($sizePattern, $val)) {
2583 $sizeIndex = $i;
2584 $props["font_size"] = $val;
2585 break;
2586 }
2587 }
2588 // `font-size` is mandatory
2589 if ($sizeIndex === null) {
2590 return [];
2591 }
2592 // `font-style`, `font-variant`, `font-weight` in any order
2593 $styleVariantWeight = \array_slice($components, 0, $sizeIndex);
2594 $stylePattern = "/^(italic|oblique)\$/";
2595 $variantPattern = "/^(small-caps)\$/";
2596 $weightPattern = "/^(bold|bolder|lighter|{$number})\$/";
2597 if (\count($styleVariantWeight) > 3) {
2598 return [];
2599 }
2600 foreach ($styleVariantWeight as $val) {
2601 if ($val === "normal") {
2602 // Ignore any `normal` value, as it is valid and the initial
2603 // value for all three properties
2604 } elseif (!isset($props["font_style"]) && \preg_match($stylePattern, $val)) {
2605 $props["font_style"] = $val;
2606 } elseif (!isset($props["font_variant"]) && \preg_match($variantPattern, $val)) {
2607 $props["font_variant"] = $val;
2608 } elseif (!isset($props["font_weight"]) && \preg_match($weightPattern, $val)) {
2609 $props["font_weight"] = $val;
2610 } else {
2611 // Duplicates and other values disallowed here
2612 return [];
2613 }
2614 }
2615 // Optional slash + `line-height` followed by mandatory `font-family`
2616 $lineFamily = \array_slice($components, $sizeIndex + 1);
2617 $hasLineHeight = $lineFamily !== [] && $lineFamily[0] === "/";
2618 $lineHeight = $hasLineHeight ? \array_slice($lineFamily, 1, 1) : [];
2619 $fontFamily = $hasLineHeight ? \array_slice($lineFamily, 2) : $lineFamily;
2620 $lineHeightPattern = "/^(normal|{$number}(?:{$unit})?)\$/";
2621 // Missing `font-family` or `line-height` after slash
2622 if ($fontFamily === [] || $hasLineHeight && !\preg_match($lineHeightPattern, $lineHeight[0])) {
2623 return [];
2624 }
2625 if ($hasLineHeight) {
2626 $props["line_height"] = $lineHeight[0];
2627 }
2628 $props["font_family"] = \implode("", $fontFamily);
2629 return $props;
2630 }
2631 /**
2632 * Compute `text-align`.
2633 *
2634 * If no alignment is set on the element and the direction is rtl then
2635 * the property is set to "right", otherwise it is set to "left".
2636 *
2637 * @link https://www.w3.org/TR/CSS21/text.html#propdef-text-align
2638 */
2639 protected function _compute_text_align(string $val)
2640 {
2641 $alignment = \strtolower($val);
2642 if ($alignment === "") {
2643 $alignment = $this->__get("direction") === "rtl" ? "right" : "left";
2644 }
2645 if (!\in_array($alignment, self::TEXT_ALIGN_KEYWORDS, \true)) {
2646 return null;
2647 }
2648 return $alignment;
2649 }
2650 /**
2651 * @link https://www.w3.org/TR/css-text-4/#word-spacing-property
2652 */
2653 protected function _compute_word_spacing(string $val)
2654 {
2655 $val = \strtolower($val);
2656 if ($val === "normal") {
2657 return 0.0;
2658 }
2659 return $this->compute_length_percentage($val);
2660 }
2661 /**
2662 * @link https://www.w3.org/TR/css-text-4/#letter-spacing-property
2663 */
2664 protected function _compute_letter_spacing(string $val)
2665 {
2666 $val = \strtolower($val);
2667 if ($val === "normal") {
2668 return 0.0;
2669 }
2670 return $this->compute_length_percentage($val);
2671 }
2672 /**
2673 * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-line-height
2674 */
2675 protected function _compute_line_height(string $val)
2676 {
2677 $val = \strtolower($val);
2678 if ($val === "normal") {
2679 return $val;
2680 }
2681 // Compute number values to string and lengths to float (in pt)
2682 if (\is_numeric($val)) {
2683 return (string) $val;
2684 }
2685 $font_size = $this->__get("font_size");
2686 $computed = $this->single_length_in_pt($val, $font_size);
2687 // Negative non-`calc` values are invalid
2688 if ($computed === null || $computed < 0 && !\preg_match("/^-?[_a-zA-Z]/", $val)) {
2689 return null;
2690 }
2691 return $computed;
2692 }
2693 /**
2694 * @link https://www.w3.org/TR/css-text-3/#text-indent-property
2695 */
2696 protected function _compute_text_indent(string $val)
2697 {
2698 return $this->compute_length_percentage($val);
2699 }
2700 /**
2701 * @link https://www.w3.org/TR/CSS21/page.html#propdef-page-break-before
2702 */
2703 protected function _compute_page_break_before(string $val)
2704 {
2705 $break = \strtolower($val);
2706 if ($break === "left" || $break === "right") {
2707 $break = "always";
2708 }
2709 return $break;
2710 }
2711 /**
2712 * @link https://www.w3.org/TR/CSS21/page.html#propdef-page-break-after
2713 */
2714 protected function _compute_page_break_after(string $val)
2715 {
2716 $break = \strtolower($val);
2717 if ($break === "left" || $break === "right") {
2718 $break = "always";
2719 }
2720 return $break;
2721 }
2722 /**
2723 * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-width
2724 */
2725 protected function _compute_width(string $val)
2726 {
2727 $val = \strtolower($val);
2728 if ($val === "auto") {
2729 return $val;
2730 }
2731 return $this->compute_length_percentage_positive($val);
2732 }
2733 /**
2734 * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-height
2735 */
2736 protected function _compute_height(string $val)
2737 {
2738 $val = \strtolower($val);
2739 if ($val === "auto") {
2740 return $val;
2741 }
2742 return $this->compute_length_percentage_positive($val);
2743 }
2744 /**
2745 * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-min-width
2746 */
2747 protected function _compute_min_width(string $val)
2748 {
2749 $val = \strtolower($val);
2750 // Legacy support for `none`, not covered by spec
2751 if ($val === "auto" || $val === "none") {
2752 return "auto";
2753 }
2754 return $this->compute_length_percentage_positive($val);
2755 }
2756 /**
2757 * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-min-height
2758 */
2759 protected function _compute_min_height(string $val)
2760 {
2761 $val = \strtolower($val);
2762 // Legacy support for `none`, not covered by spec
2763 if ($val === "auto" || $val === "none") {
2764 return "auto";
2765 }
2766 return $this->compute_length_percentage_positive($val);
2767 }
2768 /**
2769 * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-max-width
2770 */
2771 protected function _compute_max_width(string $val)
2772 {
2773 $val = \strtolower($val);
2774 // Legacy support for `auto`, not covered by spec
2775 if ($val === "none" || $val === "auto") {
2776 return "none";
2777 }
2778 return $this->compute_length_percentage_positive($val);
2779 }
2780 /**
2781 * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-max-height
2782 */
2783 protected function _compute_max_height(string $val)
2784 {
2785 $val = \strtolower($val);
2786 // Legacy support for `auto`, not covered by spec
2787 if ($val === "none" || $val === "auto") {
2788 return "none";
2789 }
2790 return $this->compute_length_percentage_positive($val);
2791 }
2792 /**
2793 * @link https://www.w3.org/TR/css-position-3/#inset-properties
2794 * @link https://www.w3.org/TR/css-position-3/#propdef-inset
2795 */
2796 protected function _set_inset(string $val) : array
2797 {
2798 return $this->set_quad_shorthand("inset", $val);
2799 }
2800 /**
2801 * @param string $val
2802 * @return float|string|null
2803 */
2804 protected function compute_box_inset(string $val)
2805 {
2806 $val = \strtolower($val);
2807 if ($val === "auto") {
2808 return $val;
2809 }
2810 return $this->compute_length_percentage($val);
2811 }
2812 protected function _compute_top(string $val)
2813 {
2814 return $this->compute_box_inset($val);
2815 }
2816 protected function _compute_right(string $val)
2817 {
2818 return $this->compute_box_inset($val);
2819 }
2820 protected function _compute_bottom(string $val)
2821 {
2822 return $this->compute_box_inset($val);
2823 }
2824 protected function _compute_left(string $val)
2825 {
2826 return $this->compute_box_inset($val);
2827 }
2828 /**
2829 * @link https://www.w3.org/TR/CSS21/box.html#margin-properties
2830 * @link https://www.w3.org/TR/CSS21/box.html#propdef-margin
2831 */
2832 protected function _set_margin(string $val) : array
2833 {
2834 return $this->set_quad_shorthand("margin", $val);
2835 }
2836 /**
2837 * @param string $val
2838 * @return float|string|null
2839 */
2840 protected function compute_margin(string $val)
2841 {
2842 $val = \strtolower($val);
2843 // Legacy support for `none` keyword, not covered by spec
2844 if ($val === "none") {
2845 return 0.0;
2846 }
2847 if ($val === "auto") {
2848 return $val;
2849 }
2850 return $this->compute_length_percentage($val);
2851 }
2852 protected function _compute_margin_top(string $val)
2853 {
2854 return $this->compute_margin($val);
2855 }
2856 protected function _compute_margin_right(string $val)
2857 {
2858 return $this->compute_margin($val);
2859 }
2860 protected function _compute_margin_bottom(string $val)
2861 {
2862 return $this->compute_margin($val);
2863 }
2864 protected function _compute_margin_left(string $val)
2865 {
2866 return $this->compute_margin($val);
2867 }
2868 /**
2869 * @link https://www.w3.org/TR/CSS21/box.html#padding-properties
2870 * @link https://www.w3.org/TR/CSS21/box.html#propdef-padding
2871 */
2872 protected function _set_padding(string $val) : array
2873 {
2874 return $this->set_quad_shorthand("padding", $val);
2875 }
2876 /**
2877 * @param string $val
2878 * @return float|string|null
2879 */
2880 protected function compute_padding(string $val)
2881 {
2882 $val = \strtolower($val);
2883 // Legacy support for `none` keyword, not covered by spec
2884 if ($val === "none") {
2885 return 0.0;
2886 }
2887 return $this->compute_length_percentage_positive($val);
2888 }
2889 protected function _compute_padding_top(string $val)
2890 {
2891 return $this->compute_padding($val);
2892 }
2893 protected function _compute_padding_right(string $val)
2894 {
2895 return $this->compute_padding($val);
2896 }
2897 protected function _compute_padding_bottom(string $val)
2898 {
2899 return $this->compute_padding($val);
2900 }
2901 protected function _compute_padding_left(string $val)
2902 {
2903 return $this->compute_padding($val);
2904 }
2905 /**
2906 * @param string $value `width || style || color`
2907 * @param string[] $styles The list of border styles to accept.
2908 *
2909 * @return string[]|null Array of `[width, style, color]`, or `null` if the declaration is invalid.
2910 */
2911 protected function parse_border_side(string $value, array $styles = self::BORDER_STYLES) : ?array
2912 {
2913 $value = \strtolower($value);
2914 $components = $this->parse_property_value($value);
2915 $width = null;
2916 $style = null;
2917 $color = null;
2918 foreach ($components as $val) {
2919 if ($style === null && \in_array($val, $styles, \true)) {
2920 $style = $val;
2921 } elseif ($color === null && $this->is_color_value($val)) {
2922 $color = $val;
2923 } elseif ($width === null) {
2924 // Assume width
2925 $width = $val;
2926 } else {
2927 // Duplicates are not allowed
2928 return null;
2929 }
2930 }
2931 return [$width, $style, $color];
2932 }
2933 /**
2934 * @link https://www.w3.org/TR/CSS21/box.html#border-properties
2935 * @link https://www.w3.org/TR/CSS21/box.html#propdef-border
2936 */
2937 protected function _set_border(string $value) : array
2938 {
2939 $values = $this->parse_border_side($value);
2940 if ($values === null) {
2941 return [];
2942 }
2943 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));
2944 }
2945 /**
2946 * @param string $prop
2947 * @param string $value
2948 * @return array
2949 */
2950 protected function set_border_side(string $prop, string $value) : array
2951 {
2952 $values = $this->parse_border_side($value);
2953 if ($values === null) {
2954 return [];
2955 }
2956 return \array_combine(self::$_props_shorthand[$prop], $values);
2957 }
2958 protected function _set_border_top(string $val) : array
2959 {
2960 return $this->set_border_side("border_top", $val);
2961 }
2962 protected function _set_border_right(string $val) : array
2963 {
2964 return $this->set_border_side("border_right", $val);
2965 }
2966 protected function _set_border_bottom(string $val) : array
2967 {
2968 return $this->set_border_side("border_bottom", $val);
2969 }
2970 protected function _set_border_left(string $val) : array
2971 {
2972 return $this->set_border_side("border_left", $val);
2973 }
2974 /**
2975 * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-color
2976 */
2977 protected function _set_border_color(string $val) : array
2978 {
2979 return $this->set_quad_shorthand("border_color", $val);
2980 }
2981 protected function _compute_border_top_color(string $val)
2982 {
2983 return $this->compute_color_value($val);
2984 }
2985 protected function _compute_border_right_color(string $val)
2986 {
2987 return $this->compute_color_value($val);
2988 }
2989 protected function _compute_border_bottom_color(string $val)
2990 {
2991 return $this->compute_color_value($val);
2992 }
2993 protected function _compute_border_left_color(string $val)
2994 {
2995 return $this->compute_color_value($val);
2996 }
2997 /**
2998 * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-style
2999 */
3000 protected function _set_border_style(string $val) : array
3001 {
3002 return $this->set_quad_shorthand("border_style", $val);
3003 }
3004 protected function _compute_border_top_style(string $val)
3005 {
3006 return $this->compute_border_style($val);
3007 }
3008 protected function _compute_border_right_style(string $val)
3009 {
3010 return $this->compute_border_style($val);
3011 }
3012 protected function _compute_border_bottom_style(string $val)
3013 {
3014 return $this->compute_border_style($val);
3015 }
3016 protected function _compute_border_left_style(string $val)
3017 {
3018 return $this->compute_border_style($val);
3019 }
3020 /**
3021 * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-width
3022 */
3023 protected function _set_border_width(string $val) : array
3024 {
3025 return $this->set_quad_shorthand("border_width", $val);
3026 }
3027 protected function _compute_border_top_width(string $val)
3028 {
3029 return $this->compute_line_width($val, "border_top_style");
3030 }
3031 protected function _compute_border_right_width(string $val)
3032 {
3033 return $this->compute_line_width($val, "border_right_style");
3034 }
3035 protected function _compute_border_bottom_width(string $val)
3036 {
3037 return $this->compute_line_width($val, "border_bottom_style");
3038 }
3039 protected function _compute_border_left_width(string $val)
3040 {
3041 return $this->compute_line_width($val, "border_left_style");
3042 }
3043 /**
3044 * @link https://www.w3.org/TR/css-backgrounds-3/#corners
3045 * @link https://www.w3.org/TR/css-backgrounds-3/#propdef-border-radius
3046 */
3047 protected function _set_border_radius(string $val) : array
3048 {
3049 return $this->set_quad_shorthand("border_radius", $val);
3050 }
3051 protected function _compute_border_top_left_radius(string $val)
3052 {
3053 return $this->compute_length_percentage_positive($val);
3054 }
3055 protected function _compute_border_top_right_radius(string $val)
3056 {
3057 return $this->compute_length_percentage_positive($val);
3058 }
3059 protected function _compute_border_bottom_right_radius(string $val)
3060 {
3061 return $this->compute_length_percentage_positive($val);
3062 }
3063 protected function _compute_border_bottom_left_radius(string $val)
3064 {
3065 return $this->compute_length_percentage_positive($val);
3066 }
3067 /**
3068 * @link https://www.w3.org/TR/css-ui-4/#outline-props
3069 * @link https://www.w3.org/TR/css-ui-4/#propdef-outline
3070 */
3071 protected function _set_outline(string $value) : array
3072 {
3073 $values = $this->parse_border_side($value, self::OUTLINE_STYLES);
3074 if ($values === null) {
3075 return [];
3076 }
3077 return \array_combine(self::$_props_shorthand["outline"], $values);
3078 }
3079 protected function _compute_outline_color(string $val)
3080 {
3081 return $this->compute_color_value($val);
3082 }
3083 protected function _compute_outline_style(string $val)
3084 {
3085 $val = \strtolower($val);
3086 return \in_array($val, self::OUTLINE_STYLES, \true) ? $val : null;
3087 }
3088 protected function _compute_outline_width(string $val)
3089 {
3090 return $this->compute_line_width($val, "outline_style");
3091 }
3092 /**
3093 * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-offset
3094 */
3095 protected function _compute_outline_offset(string $val)
3096 {
3097 return $this->compute_length($val);
3098 }
3099 /**
3100 * Compute `border-spacing` to two lengths of the form
3101 * `[horizontal, vertical]`.
3102 *
3103 * @link https://www.w3.org/TR/CSS21/tables.html#propdef-border-spacing
3104 */
3105 protected function _compute_border_spacing(string $val)
3106 {
3107 $val = \strtolower($val);
3108 $parts = $this->parse_property_value($val);
3109 $count = \count($parts);
3110 if ($count === 0 || $count > 2) {
3111 return null;
3112 }
3113 $h = $this->compute_length_positive($parts[0]);
3114 $v = isset($parts[1]) ? $this->compute_length_positive($parts[1]) : $h;
3115 if ($h === null || $v === null) {
3116 return null;
3117 }
3118 return [$h, $v];
3119 }
3120 /**
3121 * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-image
3122 */
3123 protected function _compute_list_style_image(string $val)
3124 {
3125 $parsed_val = $this->_stylesheet->resolve_url($val);
3126 if ($parsed_val === "none") {
3127 return "none";
3128 } else {
3129 return "url(\"" . \str_replace("\"", "\\\"", $parsed_val) . "\")";
3130 }
3131 }
3132 /**
3133 * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-type
3134 */
3135 protected function _compute_list_style_position(string $val)
3136 {
3137 $val = \strtolower($val);
3138 return $val === "inside" || $val === "outside" ? $val : null;
3139 }
3140 /**
3141 * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-type
3142 */
3143 protected function _compute_list_style_type(string $val)
3144 {
3145 $val = \strtolower($val);
3146 if ($val === "none") {
3147 return $val;
3148 }
3149 $ident = self::CSS_IDENTIFIER;
3150 return $val !== "default" && \preg_match("/^{$ident}\$/", $val) ? $val : null;
3151 }
3152 /**
3153 * Handle the `list-style` shorthand property.
3154 *
3155 * `[ list-style-position || list-style-image || list-style-type ]`
3156 *
3157 * @link https://www.w3.org/TR/css-lists-3/#list-style-property
3158 */
3159 protected function _set_list_style(string $value) : array
3160 {
3161 $components = $this->parse_property_value($value);
3162 $none = 0;
3163 $position = null;
3164 $image = null;
3165 $type = null;
3166 foreach ($components as $val) {
3167 $lower = \strtolower($val);
3168 // `none` can occur max 2 times (for image and type each)
3169 if ($none < 2 && $lower === "none") {
3170 $none++;
3171 } elseif ($position === null && ($lower === "inside" || $lower === "outside")) {
3172 $position = $lower;
3173 } elseif ($image === null && \strncmp($lower, "url(", 4) === 0) {
3174 $image = $val;
3175 } elseif ($type === null) {
3176 $type = $val;
3177 } else {
3178 // Duplicates are not allowed
3179 return [];
3180 }
3181 }
3182 // From the spec:
3183 // Using a value of `none` in the shorthand is potentially ambiguous, as
3184 // `none` is a valid value for both `list-style-image` and `list-style-type`.
3185 // To resolve this ambiguity, a value of `none` in the shorthand must be
3186 // applied to whichever of the two properties aren’t otherwise set by
3187 // the shorthand.
3188 if ($none === 2) {
3189 if ($image !== null || $type !== null) {
3190 return [];
3191 }
3192 $image = "none";
3193 $type = "none";
3194 } elseif ($none === 1) {
3195 if ($image !== null && $type !== null) {
3196 return [];
3197 }
3198 $image = $image ?? "none";
3199 $type = $type ?? "none";
3200 }
3201 return ["list_style_position" => $position, "list_style_image" => $image, "list_style_type" => $type];
3202 }
3203 /**
3204 * @param string $value
3205 * @param int $default
3206 * @param bool $sumDuplicates
3207 *
3208 * @return array|string|null
3209 */
3210 protected function compute_counter_prop(string $value, int $default, bool $sumDuplicates = \false)
3211 {
3212 $lower = \strtolower($value);
3213 if ($lower === "none") {
3214 return $lower;
3215 }
3216 $ident = self::CSS_IDENTIFIER;
3217 $integer = self::CSS_INTEGER;
3218 $counterDef = "({$ident})(?:\\s+({$integer}))?";
3219 $validationPattern = "/^{$counterDef}(\\s+{$counterDef})*\$/";
3220 if (!\preg_match($validationPattern, $value)) {
3221 return null;
3222 }
3223 \preg_match_all("/{$counterDef}/", $value, $matches, \PREG_SET_ORDER);
3224 $counters = [];
3225 foreach ($matches as $match) {
3226 $name = $match[1];
3227 if (!$this->isValidCounterName($name)) {
3228 return null;
3229 }
3230 $value = isset($match[2]) ? (int) $match[2] : $default;
3231 $counters[$name] = $sumDuplicates ? ($counters[$name] ?? 0) + $value : $value;
3232 }
3233 return $counters;
3234 }
3235 /**
3236 * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-increment
3237 */
3238 protected function _compute_counter_increment(string $val)
3239 {
3240 return $this->compute_counter_prop($val, 1, \true);
3241 }
3242 /**
3243 * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-reset
3244 */
3245 protected function _compute_counter_reset(string $val)
3246 {
3247 return $this->compute_counter_prop($val, 0);
3248 }
3249 /**
3250 * @link https://www.w3.org/TR/css-content-3/#quotes
3251 */
3252 protected function _compute_quotes(string $val)
3253 {
3254 $lower = \strtolower($val);
3255 // `auto` is resolved in the getter, so it can inherit as is
3256 if ($lower === "none" || $lower === "auto") {
3257 return $lower;
3258 }
3259 $components = $this->parse_property_value($val);
3260 $quotes = [];
3261 foreach ($components as $value) {
3262 if (\strncmp($value, '"', 1) !== 0 && \strncmp($value, "'", 1) !== 0) {
3263 return null;
3264 }
3265 $quotes[] = $this->_stylesheet->parse_string($value);
3266 }
3267 if ($quotes === [] || \count($quotes) % 2 !== 0) {
3268 return null;
3269 }
3270 return \array_chunk($quotes, 2);
3271 }
3272 /**
3273 * @link https://www.w3.org/TR/CSS21/generate.html#propdef-content
3274 * @link https://www.w3.org/TR/css-content-3/#propdef-content
3275 */
3276 protected function _compute_content(string $val)
3277 {
3278 $lower = \strtolower($val);
3279 if ($lower === "normal" || $lower === "none") {
3280 return $lower;
3281 }
3282 $components = $this->parse_property_value($val);
3283 $parts = [];
3284 if ($components === []) {
3285 return null;
3286 }
3287 foreach ($components as $value) {
3288 // String
3289 if (\strncmp($value, '"', 1) === 0 || \strncmp($value, "'", 1) === 0) {
3290 $parts[] = new StringPart($this->_stylesheet->parse_string($value));
3291 continue;
3292 }
3293 $lower = \strtolower($value);
3294 // Keywords
3295 if ($lower === "open-quote") {
3296 $parts[] = new OpenQuote();
3297 continue;
3298 } elseif ($lower === "close-quote") {
3299 $parts[] = new CloseQuote();
3300 continue;
3301 } elseif ($lower === "no-open-quote") {
3302 $parts[] = new NoOpenQuote();
3303 continue;
3304 } elseif ($lower === "no-close-quote") {
3305 $parts[] = new NoCloseQuote();
3306 continue;
3307 }
3308 // Functional components
3309 $pos = \strpos($lower, "(");
3310 if ($pos === \false) {
3311 return null;
3312 }
3313 // `parse_property_value` ensures that the value is of the form
3314 // `function(arguments)` at this point
3315 $function = \substr($lower, 0, $pos);
3316 $arguments = \trim(\substr($value, $pos + 1, -1));
3317 // attr()
3318 if ($function === "attr") {
3319 $attr = \strtolower($arguments);
3320 if ($attr === "") {
3321 return null;
3322 }
3323 $parts[] = new Attr($attr);
3324 } elseif ($function === "counter") {
3325 $ident = self::CSS_IDENTIFIER;
3326 if (!\preg_match("/^({$ident})(?:\\s*,\\s*({$ident}))?\$/", $arguments, $matches)) {
3327 return null;
3328 }
3329 $name = $matches[1];
3330 $type = isset($matches[2]) ? \strtolower($matches[2]) : "decimal";
3331 if (!$this->isValidCounterName($name) || !$this->isValidCounterStyleName($type)) {
3332 return null;
3333 }
3334 $parts[] = new Counter($name, $type);
3335 } elseif ($function === "counters") {
3336 $ident = self::CSS_IDENTIFIER;
3337 $string = self::CSS_STRING;
3338 if (!\preg_match("/^({$ident})\\s*,\\s*({$string})(?:\\s*,\\s*({$ident}))?\$/", $arguments, $matches)) {
3339 return null;
3340 }
3341 $name = $matches[1];
3342 $string = $this->_stylesheet->parse_string($matches[2]);
3343 $type = isset($matches[3]) ? \strtolower($matches[3]) : "decimal";
3344 if (!$this->isValidCounterName($name) || !$this->isValidCounterStyleName($type)) {
3345 return null;
3346 }
3347 $parts[] = new Counters($name, $string, $type);
3348 } elseif ($function === "url") {
3349 $url = $this->_stylesheet->parse_string($arguments);
3350 $parts[] = new Url($url);
3351 } else {
3352 return null;
3353 }
3354 }
3355 return $parts;
3356 }
3357 /**
3358 * @link https://www.w3.org/TR/css-page-3/#page-size-prop
3359 */
3360 protected function _compute_size(string $val)
3361 {
3362 $val = \strtolower($val);
3363 if ($val === "auto") {
3364 return $val;
3365 }
3366 $parts = $this->parse_property_value($val);
3367 $count = \count($parts);
3368 if ($count === 0 || $count > 3) {
3369 return null;
3370 }
3371 $size = null;
3372 $orientation = null;
3373 $lengths = [];
3374 foreach ($parts as $part) {
3375 if ($size === null && isset(CPDF::$PAPER_SIZES[$part])) {
3376 $size = $part;
3377 } elseif ($orientation === null && ($part === "portrait" || $part === "landscape")) {
3378 $orientation = $part;
3379 } else {
3380 $lengths[] = $part;
3381 }
3382 }
3383 if ($size !== null && $lengths !== []) {
3384 return null;
3385 }
3386 if ($size !== null) {
3387 // Standard paper size
3388 [$l1, $l2] = \array_slice(CPDF::$PAPER_SIZES[$size], 2, 2);
3389 } elseif ($lengths === []) {
3390 // Orientation only, use default paper size
3391 $dims = $this->_stylesheet->get_dompdf()->getPaperSize();
3392 [$l1, $l2] = \array_slice($dims, 2, 2);
3393 } else {
3394 // Custom paper size
3395 $l1 = $this->compute_length_positive($lengths[0]);
3396 $l2 = isset($lengths[1]) ? $this->compute_length_positive($lengths[1]) : $l1;
3397 if ($l1 === null || $l2 === null) {
3398 return null;
3399 }
3400 }
3401 if ($orientation === "portrait" && $l1 > $l2 || $orientation === "landscape" && $l2 > $l1) {
3402 return [$l2, $l1];
3403 }
3404 return [$l1, $l2];
3405 }
3406 /**
3407 * @link https://www.w3.org/TR/css-transforms-1/#transform-property
3408 */
3409 protected function _compute_transform(string $val)
3410 {
3411 $val = \strtolower($val);
3412 if ($val === "none") {
3413 return [];
3414 }
3415 $parts = $this->parse_property_value($val);
3416 $transforms = [];
3417 if ($parts === []) {
3418 return null;
3419 }
3420 foreach ($parts as $part) {
3421 if (!\preg_match("/^([a-z]+)\\((.+)\\)\$/s", $part, $matches)) {
3422 return null;
3423 }
3424 $name = $matches[1];
3425 $arguments = \trim($matches[2]);
3426 $values = $this->parse_property_value($arguments);
3427 $values = \array_values(\array_filter($values, function ($v) {
3428 return $v !== ",";
3429 }));
3430 $count = \count($values);
3431 if ($count === 0) {
3432 return null;
3433 }
3434 switch ($name) {
3435 // case "matrix":
3436 // if ($count !== 6) {
3437 // return null;
3438 // }
3439 // $values = array_map([$this, "compute_number"], $values);
3440 // break;
3441 // <length-percentage> units
3442 case "translate":
3443 if ($count > 2) {
3444 return null;
3445 }
3446 $values = [$this->compute_length_percentage($values[0]), isset($values[1]) ? $this->compute_length_percentage($values[1]) : 0.0];
3447 break;
3448 case "translatex":
3449 if ($count > 1) {
3450 return null;
3451 }
3452 $name = "translate";
3453 $values = [$this->compute_length_percentage($values[0]), 0.0];
3454 break;
3455 case "translatey":
3456 if ($count > 1) {
3457 return null;
3458 }
3459 $name = "translate";
3460 $values = [0.0, $this->compute_length_percentage($values[0])];
3461 break;
3462 // <number> units
3463 case "scale":
3464 if ($count > 2) {
3465 return null;
3466 }
3467 $v0 = $this->compute_number($values[0]);
3468 $v1 = isset($values[1]) ? $this->compute_number($values[1]) : $v0;
3469 $values = [$v0, $v1];
3470 break;
3471 case "scalex":
3472 if ($count > 1) {
3473 return null;
3474 }
3475 $name = "scale";
3476 $values = [$this->compute_number($values[0]), 1.0];
3477 break;
3478 case "scaley":
3479 if ($count > 1) {
3480 return null;
3481 }
3482 $name = "scale";
3483 $values = [1.0, $this->compute_number($values[0])];
3484 break;
3485 // <angle> units
3486 case "rotate":
3487 if ($count > 1) {
3488 return null;
3489 }
3490 $values = [$this->compute_angle_or_zero($values[0])];
3491 break;
3492 case "skew":
3493 if ($count > 2) {
3494 return null;
3495 }
3496 $values = [$this->compute_angle_or_zero($values[0]), isset($values[1]) ? $this->compute_angle_or_zero($values[1]) : 0.0];
3497 break;
3498 case "skewx":
3499 if ($count > 1) {
3500 return null;
3501 }
3502 $name = "skew";
3503 $values = [$this->compute_angle_or_zero($values[0]), 0.0];
3504 break;
3505 case "skewy":
3506 if ($count > 1) {
3507 return null;
3508 }
3509 $name = "skew";
3510 $values = [0.0, $this->compute_angle_or_zero($values[0])];
3511 break;
3512 default:
3513 return null;
3514 }
3515 foreach ($values as $v) {
3516 if ($v === null) {
3517 return null;
3518 }
3519 }
3520 $transforms[] = [$name, $values];
3521 }
3522 return $transforms;
3523 }
3524 /**
3525 * @link https://www.w3.org/TR/css-transforms-1/#transform-origin-property
3526 */
3527 protected function _compute_transform_origin(string $val)
3528 {
3529 $val = \strtolower($val);
3530 $parts = $this->parse_property_value($val);
3531 $count = \count($parts);
3532 if ($count === 0 || $count > 3) {
3533 return null;
3534 }
3535 $v1 = $parts[0];
3536 $v2 = $parts[1] ?? "center";
3537 [$x, $y] = $this->computeBackgroundPositionTransformOrigin($v1, $v2);
3538 $z = $count === 3 ? $this->compute_length($parts[2]) : 0.0;
3539 if ($x === null || $y === null || $z === null) {
3540 return null;
3541 }
3542 return [$x, $y, $z];
3543 }
3544 /**
3545 * @param string $val
3546 * @return string|null
3547 */
3548 protected function parse_image_resolution(string $val) : ?string
3549 {
3550 // If exif data could be get:
3551 // $re = '/^\s*(\d+|normal|auto)(?:\s*,\s*(\d+|normal))?\s*$/';
3552 $val = \strtolower($val);
3553 $re = '/^\\s*(\\d+|normal|auto)\\s*$/';
3554 if (!\preg_match($re, $val, $matches)) {
3555 return null;
3556 }
3557 return $matches[1];
3558 }
3559 /**
3560 * auto | normal | dpi
3561 */
3562 protected function _compute_background_image_resolution(string $val)
3563 {
3564 return $this->parse_image_resolution($val);
3565 }
3566 /**
3567 * auto | normal | dpi
3568 */
3569 protected function _compute_image_resolution(string $val)
3570 {
3571 return $this->parse_image_resolution($val);
3572 }
3573 /**
3574 * @link https://www.w3.org/TR/css-break-3/#propdef-orphans
3575 */
3576 protected function _compute_orphans(string $val)
3577 {
3578 return $this->compute_integer($val);
3579 }
3580 /**
3581 * @link https://www.w3.org/TR/css-break-3/#propdef-widows
3582 */
3583 protected function _compute_widows(string $val)
3584 {
3585 return $this->compute_integer($val);
3586 }
3587 /**
3588 * @link https://www.w3.org/TR/css-color-4/#propdef-opacity
3589 */
3590 protected function _compute_opacity(string $val)
3591 {
3592 $number = self::CSS_NUMBER;
3593 $pattern = "/^({$number})(%?)\$/";
3594 if (!\preg_match($pattern, $val, $matches)) {
3595 return null;
3596 }
3597 $v = (float) $matches[1];
3598 $percent = $matches[2] === "%";
3599 $opacity = $percent ? $v / 100 : $v;
3600 return \max(0.0, \min($opacity, 1.0));
3601 }
3602 /**
3603 * @link https://www.w3.org/TR/CSS21//visuren.html#propdef-z-index
3604 */
3605 protected function _compute_z_index(string $val)
3606 {
3607 $val = \strtolower($val);
3608 if ($val === "auto") {
3609 return $val;
3610 }
3611 return $this->compute_integer($val);
3612 }
3613 /**
3614 * @param FontMetrics $fontMetrics
3615 * @return $this
3616 */
3617 public function setFontMetrics(FontMetrics $fontMetrics)
3618 {
3619 $this->fontMetrics = $fontMetrics;
3620 return $this;
3621 }
3622 /**
3623 * @return FontMetrics
3624 */
3625 public function getFontMetrics()
3626 {
3627 return $this->fontMetrics;
3628 }
3629 /**
3630 * Generate a string representation of the Style
3631 *
3632 * This dumps the entire property array into a string via print_r. Useful
3633 * for debugging.
3634 *
3635 * @return string
3636 */
3637 /*DEBUGCSS print: see below additional debugging util*/
3638 public function __toString() : string
3639 {
3640 $parent_font_size = $this->parent_style ? $this->parent_style->font_size : self::$default_font_size;
3641 return \print_r(\array_merge(["parent_font_size" => $parent_font_size], $this->_props), \true);
3642 }
3643 /*DEBUGCSS*/
3644 public function debug_print() : void
3645 {
3646 $parent_font_size = $this->parent_style ? $this->parent_style->font_size : self::$default_font_size;
3647 print " parent_font_size:" . $parent_font_size . ";\n";
3648 print " Props [\n";
3649 print " specified [\n";
3650 foreach ($this->_props as $prop => $val) {
3651 print ' ' . $prop . ': ' . \preg_replace("/\r\n/", ' ', \print_r($val, \true));
3652 if (isset($this->_important_props[$prop])) {
3653 print ' !important';
3654 }
3655 print ";\n";
3656 }
3657 print " ]\n";
3658 print " computed [\n";
3659 foreach ($this->_props_computed as $prop => $val) {
3660 print ' ' . $prop . ': ' . \preg_replace("/\r\n/", ' ', \print_r($val, \true));
3661 print ";\n";
3662 }
3663 print " ]\n";
3664 print " cached [\n";
3665 foreach ($this->_props_used as $prop => $val) {
3666 print ' ' . $prop . ': ' . \preg_replace("/\r\n/", ' ', \print_r($val, \true));
3667 print ";\n";
3668 }
3669 print " ]\n";
3670 print " ]\n";
3671 }
3672 }
3673