Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
20 / 24
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
TextPositioningElement
83.33% covered (warning)
83.33%
20 / 24
62.50% covered (warning)
62.50%
5 / 8
18.34
0.00% covered (danger)
0.00%
0 / 1
 x
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 y
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dx
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rotate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 textLength
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 lengthAdjust
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 parseNumberList
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Svg\Text;
6
7use Phpdftk\Svg\Element;
8
9/**
10 * Shared base for elements that carry SVG 2 §11.6 text positioning
11 * attributes (`<text>` and `<tspan>`). Each of `x`, `y`, `dx`, `dy`, and
12 * `rotate` accepts a list of numbers — one value per glyph — so the
13 * accessors return a `list<float>` even when the attribute is a single
14 * number. An absent attribute → empty list.
15 *
16 * `textLength` and `lengthAdjust` round out the v1 subset; SVG 2's
17 * `<textPath>` is deliberately out of scope (the parser falls back to
18 * `GenericElement`).
19 */
20abstract class TextPositioningElement extends Element
21{
22    /** @return list<float> */
23    public function x(): array
24    {
25        return self::parseNumberList($this->attributes['x'] ?? null);
26    }
27
28    /** @return list<float> */
29    public function y(): array
30    {
31        return self::parseNumberList($this->attributes['y'] ?? null);
32    }
33
34    /** @return list<float> */
35    public function dx(): array
36    {
37        return self::parseNumberList($this->attributes['dx'] ?? null);
38    }
39
40    /** @return list<float> */
41    public function dy(): array
42    {
43        return self::parseNumberList($this->attributes['dy'] ?? null);
44    }
45
46    /**
47     * Per-glyph rotation in degrees (SVG 2 §11.6). One value applies to all
48     * subsequent glyphs; multiple values apply per-glyph, last value
49     * persisting for trailing glyphs.
50     *
51     * @return list<float>
52     */
53    public function rotate(): array
54    {
55        return self::parseNumberList($this->attributes['rotate'] ?? null);
56    }
57
58    /**
59     * `textLength` — the user-space length the text should occupy. SVG 2
60     * §11.7. Null when absent; values are non-negative lengths.
61     */
62    public function textLength(): ?float
63    {
64        $raw = $this->attributes['textLength'] ?? null;
65        if ($raw === null) {
66            return null;
67        }
68        if (preg_match('/^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/', $raw, $m) !== 1) {
69            return null;
70        }
71        $value = (float) $m[1];
72        return $value < 0.0 ? null : $value;
73    }
74
75    /**
76     * `lengthAdjust` — `spacing` (default) stretches inter-glyph gaps;
77     * `spacingAndGlyphs` also stretches each glyph. SVG 2 §11.7.
78     */
79    public function lengthAdjust(): ?string
80    {
81        $raw = $this->attributes['lengthAdjust'] ?? null;
82        if ($raw === null) {
83            return null;
84        }
85        $value = trim($raw);
86        return match ($value) {
87            'spacing', 'spacingAndGlyphs' => $value,
88            default => null,
89        };
90    }
91
92    /**
93     * @return list<float>
94     */
95    private static function parseNumberList(?string $raw): array
96    {
97        if ($raw === null || trim($raw) === '') {
98            return [];
99        }
100        if (preg_match_all('/[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/', $raw, $m) === false) {
101            return [];
102        }
103        return array_values(array_map(static fn(string $v): float => (float) $v, $m[0]));
104    }
105}