Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.24% covered (warning)
88.24%
45 / 51
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
LengthResolver
88.24% covered (warning)
88.24%
45 / 51
33.33% covered (danger)
33.33%
1 / 3
33.67
0.00% covered (danger)
0.00%
0 / 1
 clampPx
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 toPx
91.18% covered (success)
91.18%
31 / 34
0.00% covered (danger)
0.00%
0 / 1
24.40
 resolveValue
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
4.43
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Css\Cascade;
6
7use Phpdftk\Css\Value\Length;
8use Phpdftk\Css\Value\LengthUnit;
9use Phpdftk\Css\Value\Percentage;
10use Phpdftk\Css\Value\Value;
11
12/**
13 * Converts relative-unit `Length` values into absolute `px` Lengths against
14 * a `LengthContext`. Used by the cascade in its computed-value pass.
15 *
16 * Mapping per CSS Values 4 §6:
17 *  - Absolute (px, pt, pc, cm, mm, q, in) — converted directly via the
18 *    CSS canonical relations (1in = 96px, 1pt = 96/72 px, etc.).
19 *  - em / rem / ex / ch / lh / rlh — multiplied against the appropriate
20 *    font-size reference from the context.
21 *  - vw / vh / vmin / vmax / svw / svh / lvw / lvh / dvw / dvh — viewport
22 *    references (the small/large/dynamic variants collapse onto the same
23 *    print-medium viewport since there's no UI chrome to subtract).
24 *  - cqw / cqh / cqi / cqb / cqmin / cqmax — nearest size-query
25 *    container's content-box dimensions (CSS Containment 3 §6).
26 *    Resolves to 0 when no size container is in scope.
27 *  - Percentage — multiplied against the context's `percentageBasis`.
28 */
29final class LengthResolver
30{
31    /**
32     * Maximum absolute pixel value that layout will operate on,
33     * mirroring browser conventions: Blink caps at `LayoutUnit::Max`
34     * (~16.7M CSS px from the int32 fixed-point representation),
35     * WebKit clamps `kFixedPointDenominator * INT_MAX / ...` to the
36     * same neighbourhood. Beyond this:
37     *
38     *  - float-precision is gone (mantissa is 24 bits, so values
39     *    above 2^24 lose integer accuracy);
40     *  - layout math becomes meaningless to a reader;
41     *  - downstream code sized to these dimensions (content streams,
42     *    column-balance arrays, paint-region rects) allocates
43     *    gigabytes and OOMs on adversarial CSS like
44     *    `padding: 2880804336vmax 854269137% 347744005in 2487922492pt`
45     *    or `aspect-ratio: 1/0.00000000000001`.
46     *
47     * Authored CSS values stay untouched on the parsed Length /
48     * Percentage objects; this constant only bounds the floats that
49     * enter layout via {@see toPx()} / {@see resolveValue()}.
50     *
51     * Reference: WPT crashtests + `*-crash.html` fixtures (1,012
52     * corpus-wide as of WPT @ 2026-06-08). See phpdftk/phpdftk#28.
53     */
54    public const MAX_PX = 16777216.0;   // 2^24
55
56    /**
57     * Clamp a resolved pixel value into the safe layout range.
58     * NaN collapses to 0 (CSS Values 4 §6 treats undefined-typed
59     * results as the property's initial value, and the call sites
60     * here would otherwise propagate NaN through arithmetic until
61     * a comparison fails). ±Inf clamps to ±{@see MAX_PX}.
62     */
63    public static function clampPx(float $px): float
64    {
65        if (is_nan($px)) {
66            return 0.0;
67        }
68        if ($px > self::MAX_PX) {
69            return self::MAX_PX;
70        }
71        if ($px < -self::MAX_PX) {
72            return -self::MAX_PX;
73        }
74        return $px;
75    }
76
77    /**
78     * 1 inch = 96 CSS pixels (CSS Values 4 §6.2). Derived conversions:
79     *  1 pt = 96 / 72  ≈ 1.3333 px
80     *  1 pc = 16       px
81     *  1 cm = 96 / 2.54 ≈ 37.7953 px
82     *  1 mm = 96 / 25.4 ≈ 3.7795 px
83     *  1 Q  = 96 / 101.6 ≈ 0.9449 px
84     */
85    public static function toPx(Length $length, LengthContext $ctx): float
86    {
87        $v = $length->value;
88        $px = match ($length->unit) {
89            LengthUnit::Px => $v,
90            LengthUnit::Pt => $v * (96.0 / 72.0),
91            LengthUnit::Pc => $v * 16.0,
92            LengthUnit::Cm => $v * (96.0 / 2.54),
93            LengthUnit::Mm => $v * (96.0 / 25.4),
94            LengthUnit::Q => $v * (96.0 / 101.6),
95            LengthUnit::In => $v * 96.0,
96            LengthUnit::Em => $v * $ctx->currentFontSize,
97            LengthUnit::Rem => $v * $ctx->rootFontSize,
98            // CSS Values 4 §6.1.1 — `ex` and `ch` resolve against the
99            // first available font's metrics. LengthContext carries
100            // ratios (defaulting to 0.5em); layout code with access to
101            // the resolved font passes the real ratios via
102            // {@see LengthContext::withFontMetrics}.
103            LengthUnit::Ex => $v * $ctx->currentFontSize * $ctx->xHeightRatio,
104            LengthUnit::Ch => $v * $ctx->currentFontSize * $ctx->chWidthRatio,
105            LengthUnit::Cap => $v * $ctx->currentFontSize * $ctx->capHeightRatio,
106            LengthUnit::Lh, LengthUnit::Rlh => $v * $ctx->currentFontSize * 1.2,
107            LengthUnit::Vw, LengthUnit::Svw, LengthUnit::Lvw, LengthUnit::Dvw
108                => $v * ($ctx->viewportWidth / 100.0),
109            LengthUnit::Vh, LengthUnit::Svh, LengthUnit::Lvh, LengthUnit::Dvh
110                => $v * ($ctx->viewportHeight / 100.0),
111            LengthUnit::Vmin
112                => $v * (min($ctx->viewportWidth, $ctx->viewportHeight) / 100.0),
113            LengthUnit::Vmax
114                => $v * (max($ctx->viewportWidth, $ctx->viewportHeight) / 100.0),
115            LengthUnit::Vi => $v * ($ctx->viewportWidth / 100.0),  // assumes horizontal-tb
116            LengthUnit::Vb => $v * ($ctx->viewportHeight / 100.0),
117            // CSS Containment 3 §6 — container-relative units resolve
118            // against the nearest size-query container's content box.
119            // `cqw` / `cqi` use the inline size; `cqh` / `cqb` the
120            // block size; `cqmin` / `cqmax` the smaller / larger of
121            // the two. When no size container is in scope, the
122            // container sizes default to 0 (per §6.3 spec fallback).
123            LengthUnit::Cqw, LengthUnit::Cqi
124                => $v * ($ctx->containerInlineSize / 100.0),
125            LengthUnit::Cqh, LengthUnit::Cqb
126                => $v * ($ctx->containerBlockSize / 100.0),
127            LengthUnit::Cqmin
128                => $v * (min($ctx->containerInlineSize, $ctx->containerBlockSize) / 100.0),
129            LengthUnit::Cqmax
130                => $v * (max($ctx->containerInlineSize, $ctx->containerBlockSize) / 100.0),
131        };
132        return self::clampPx($px);
133    }
134
135    /**
136     * Resolve a Value into an absolute-pixel Length when possible. Returns
137     * the original value untouched if it's not a Length or Percentage.
138     * Percentage requires a non-zero `percentageBasis` in the context; when
139     * the basis is unknown the value is left as a Percentage for layout
140     * to resolve later.
141     */
142    public static function resolveValue(Value $value, LengthContext $ctx): Value
143    {
144        if ($value instanceof Length) {
145            return new Length(self::toPx($value, $ctx), LengthUnit::Px);
146        }
147        if ($value instanceof Percentage) {
148            if ($ctx->percentageBasis === 0.0) {
149                return $value;
150            }
151            return new Length(
152                self::clampPx($value->value / 100.0 * $ctx->percentageBasis),
153                LengthUnit::Px,
154            );
155        }
156        return $value;
157    }
158}