Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.70% covered (success)
90.70%
39 / 43
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Gradient
90.70% covered (success)
90.70%
39 / 43
50.00% covered (danger)
50.00%
3 / 6
25.50
0.00% covered (danger)
0.00%
0 / 1
 gradientUnits
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 gradientTransform
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 spreadMethod
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 href
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 stops
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveStops
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
7.02
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Svg\Gradient;
6
7use Phpdftk\Svg\Element;
8use Phpdftk\Svg\SvgDocument;
9use Phpdftk\Svg\Value\Transform;
10
11/**
12 * Shared base for SVG 2 §13 gradients (`<linearGradient>`,
13 * `<radialGradient>`). Carries the attributes both forms share —
14 * coordinate units, optional transform, spread method, and the `href`
15 * chain used to inherit colour stops from another gradient.
16 *
17 * The stop-inheritance walk is the most subtle piece: per SVG 2 §13.4,
18 * if a gradient has its own `<stop>` children they win; otherwise the
19 * `href` chain is walked at access time. Cycles (`href="#a"` → `#b` →
20 * `#a`) are broken via a visited set.
21 */
22abstract class Gradient extends Element
23{
24    /**
25     * `gradientUnits` — coordinate-system mode for the gradient's
26     * geometric attributes (`x1`/`y1`/... or `cx`/`cy`/...). Default
27     * `objectBoundingBox` per SVG 2 §13.6.5.
28     *
29     * @return 'userSpaceOnUse'|'objectBoundingBox'
30     */
31    public function gradientUnits(): string
32    {
33        $raw = $this->getAttribute('gradientUnits');
34        if ($raw === null) {
35            return 'objectBoundingBox';
36        }
37        return match (trim($raw)) {
38            'userSpaceOnUse' => 'userSpaceOnUse',
39            default => 'objectBoundingBox',
40        };
41    }
42
43    /**
44     * `gradientTransform` — an additional transformation applied to the
45     * gradient's coordinate system. Null when absent or malformed (the
46     * SVG 2 "invalid → ignored" semantics that 3C established for
47     * `transform`).
48     */
49    public function gradientTransform(): ?Transform
50    {
51        $raw = $this->getAttribute('gradientTransform');
52        if ($raw === null || trim($raw) === '') {
53            return null;
54        }
55        try {
56            return Transform::parse($raw);
57        } catch (\InvalidArgumentException) {
58            return null;
59        }
60    }
61
62    /**
63     * `spreadMethod` — how the gradient extends past its declared range.
64     * Default `pad` per SVG 2 §13.6.5.
65     *
66     * @return 'pad'|'reflect'|'repeat'
67     */
68    public function spreadMethod(): string
69    {
70        $raw = $this->getAttribute('spreadMethod');
71        if ($raw === null) {
72            return 'pad';
73        }
74        return match (trim($raw)) {
75            'reflect' => 'reflect',
76            'repeat' => 'repeat',
77            default => 'pad',
78        };
79    }
80
81    /**
82     * Referenced gradient id (without the leading `#`), or null. Reads
83     * `href` per SVG 2 §13.4 then falls back to `xlink:href`. External
84     * references return null — same intra-document-only posture as
85     * `Use_`.
86     */
87    public function href(): ?string
88    {
89        $raw = $this->getAttribute('href')
90            ?? $this->getAttribute('xlink:href');
91        if ($raw === null) {
92            return null;
93        }
94        $trimmed = trim($raw);
95        if (!str_starts_with($trimmed, '#')) {
96            return null;
97        }
98        $id = substr($trimmed, 1);
99        return $id === '' ? null : $id;
100    }
101
102    /**
103     * Effective colour stops, walking the `href` chain when this
104     * gradient has none of its own (SVG 2 §13.4). Cycles in the chain
105     * (e.g. `#a` → `#b` → `#a`) are broken by a visited set.
106     *
107     * @return list<Stop>
108     */
109    public function stops(SvgDocument $doc): array
110    {
111        return $this->resolveStops($doc, []);
112    }
113
114    /**
115     * @param array<string, true> $visited element-id set, used to break href cycles
116     * @return list<Stop>
117     */
118    private function resolveStops(SvgDocument $doc, array $visited): array
119    {
120        $own = [];
121        foreach ($this->children as $child) {
122            if ($child instanceof Stop) {
123                $own[] = $child;
124            }
125        }
126        if ($own !== []) {
127            return $own;
128        }
129
130        $href = $this->href();
131        if ($href === null || isset($visited[$href])) {
132            return [];
133        }
134        $referent = $doc->findById($href);
135        if (!$referent instanceof self) {
136            return [];
137        }
138
139        $visited[$href] = true;
140        return $referent->resolveStops($doc, $visited);
141    }
142}