Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.44% |
76 / 78 |
|
66.67% |
4 / 6 |
CRAP | |
0.00% |
0 / 1 |
| Transform | |
97.44% |
76 / 78 |
|
66.67% |
4 / 6 |
34 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| parse | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
8 | |||
| toMatrix | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| makeFunction | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
20 | |||
| parseNumberList | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| multiply | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Phpdftk\Svg\Value; |
| 6 | |
| 7 | use Phpdftk\Svg\Value\Transform\Matrix; |
| 8 | use Phpdftk\Svg\Value\Transform\Rotate; |
| 9 | use Phpdftk\Svg\Value\Transform\Scale; |
| 10 | use Phpdftk\Svg\Value\Transform\SkewX; |
| 11 | use Phpdftk\Svg\Value\Transform\SkewY; |
| 12 | use Phpdftk\Svg\Value\Transform\Translate; |
| 13 | |
| 14 | /** |
| 15 | * The parsed value of an SVG 2 `transform` attribute (SVG 2 §8.4) — an ordered |
| 16 | * list of `TransformFunction` operations. Compose them via `toMatrix()` to get |
| 17 | * the flat 3×2 affine matrix the painter feeds to the PDF `cm` operator. |
| 18 | * |
| 19 | * Composition is left-to-right per SVG 2: a list `[A, B, C]` produces matrix |
| 20 | * `A · B · C`, so `C` is applied to a point first, then `B`, then `A`. From |
| 21 | * the coordinate-system's perspective, the system is transformed in |
| 22 | * left-to-right order. |
| 23 | */ |
| 24 | final class Transform |
| 25 | { |
| 26 | /** @param list<TransformFunction> $functions */ |
| 27 | public function __construct(public readonly array $functions) {} |
| 28 | |
| 29 | /** |
| 30 | * Parse an SVG 2 transform-attribute string. Throws on malformed input — |
| 31 | * callers (typically `Element::transform()`) that want SVG's |
| 32 | * "invalid → ignored" semantics should catch and substitute null. |
| 33 | * |
| 34 | * @throws \InvalidArgumentException when the grammar isn't satisfied |
| 35 | */ |
| 36 | public static function parse(string $raw): self |
| 37 | { |
| 38 | $functions = []; |
| 39 | $offset = 0; |
| 40 | $len = strlen($raw); |
| 41 | while ($offset < $len) { |
| 42 | while ($offset < $len && (ctype_space($raw[$offset]) || $raw[$offset] === ',')) { |
| 43 | $offset++; |
| 44 | } |
| 45 | if ($offset >= $len) { |
| 46 | break; |
| 47 | } |
| 48 | if (preg_match('/\G([a-zA-Z]+)\s*\(/', $raw, $m, 0, $offset) !== 1) { |
| 49 | throw new \InvalidArgumentException( |
| 50 | sprintf('Malformed transform at offset %d: expected function name.', $offset), |
| 51 | ); |
| 52 | } |
| 53 | $name = $m[1]; |
| 54 | $offset += strlen($m[0]); |
| 55 | $close = strpos($raw, ')', $offset); |
| 56 | if ($close === false) { |
| 57 | throw new \InvalidArgumentException( |
| 58 | sprintf('Malformed transform: unterminated `%s(`.', $name), |
| 59 | ); |
| 60 | } |
| 61 | $args = self::parseNumberList(substr($raw, $offset, $close - $offset)); |
| 62 | $offset = $close + 1; |
| 63 | $functions[] = self::makeFunction($name, $args); |
| 64 | } |
| 65 | return new self($functions); |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Compose the function list and return the resulting affine matrix. |
| 70 | * Identity for an empty list. |
| 71 | * |
| 72 | * @return array{float, float, float, float, float, float} |
| 73 | */ |
| 74 | public function toMatrix(): array |
| 75 | { |
| 76 | $m = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; |
| 77 | foreach ($this->functions as $fn) { |
| 78 | $m = self::multiply($m, $fn->toMatrix()); |
| 79 | } |
| 80 | return $m; |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * @param list<float> $args |
| 85 | */ |
| 86 | private static function makeFunction(string $name, array $args): TransformFunction |
| 87 | { |
| 88 | return match ($name) { |
| 89 | 'matrix' => count($args) === 6 |
| 90 | ? new Matrix($args[0], $args[1], $args[2], $args[3], $args[4], $args[5]) |
| 91 | : throw new \InvalidArgumentException( |
| 92 | sprintf('matrix() requires 6 arguments, got %d', count($args)), |
| 93 | ), |
| 94 | 'translate' => match (count($args)) { |
| 95 | 1 => new Translate($args[0]), |
| 96 | 2 => new Translate($args[0], $args[1]), |
| 97 | default => throw new \InvalidArgumentException( |
| 98 | sprintf('translate() requires 1 or 2 arguments, got %d', count($args)), |
| 99 | ), |
| 100 | }, |
| 101 | 'scale' => match (count($args)) { |
| 102 | 1 => new Scale($args[0]), |
| 103 | 2 => new Scale($args[0], $args[1]), |
| 104 | default => throw new \InvalidArgumentException( |
| 105 | sprintf('scale() requires 1 or 2 arguments, got %d', count($args)), |
| 106 | ), |
| 107 | }, |
| 108 | 'rotate' => match (count($args)) { |
| 109 | 1 => new Rotate($args[0]), |
| 110 | 3 => new Rotate($args[0], $args[1], $args[2]), |
| 111 | default => throw new \InvalidArgumentException( |
| 112 | sprintf('rotate() requires 1 or 3 arguments, got %d', count($args)), |
| 113 | ), |
| 114 | }, |
| 115 | 'skewX' => count($args) === 1 |
| 116 | ? new SkewX($args[0]) |
| 117 | : throw new \InvalidArgumentException( |
| 118 | sprintf('skewX() requires 1 argument, got %d', count($args)), |
| 119 | ), |
| 120 | 'skewY' => count($args) === 1 |
| 121 | ? new SkewY($args[0]) |
| 122 | : throw new \InvalidArgumentException( |
| 123 | sprintf('skewY() requires 1 argument, got %d', count($args)), |
| 124 | ), |
| 125 | default => throw new \InvalidArgumentException( |
| 126 | sprintf('Unknown transform function `%s`.', $name), |
| 127 | ), |
| 128 | }; |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * @return list<float> |
| 133 | */ |
| 134 | private static function parseNumberList(string $raw): array |
| 135 | { |
| 136 | if (preg_match_all('/[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/', $raw, $m) === false) { |
| 137 | return []; |
| 138 | } |
| 139 | return array_values(array_map(static fn(string $v): float => (float) $v, $m[0])); |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Multiply two 3×2 affine matrices in SVG `[a,b,c,d,e,f]` order. |
| 144 | * |
| 145 | * @param array{float, float, float, float, float, float} $m1 |
| 146 | * @param array{float, float, float, float, float, float} $m2 |
| 147 | * @return array{float, float, float, float, float, float} |
| 148 | */ |
| 149 | private static function multiply(array $m1, array $m2): array |
| 150 | { |
| 151 | [$a1, $b1, $c1, $d1, $e1, $f1] = $m1; |
| 152 | [$a2, $b2, $c2, $d2, $e2, $f2] = $m2; |
| 153 | return [ |
| 154 | $a1 * $a2 + $c1 * $b2, |
| 155 | $b1 * $a2 + $d1 * $b2, |
| 156 | $a1 * $c2 + $c1 * $d2, |
| 157 | $b1 * $c2 + $d1 * $d2, |
| 158 | $a1 * $e2 + $c1 * $f2 + $e1, |
| 159 | $b1 * $e2 + $d1 * $f2 + $f1, |
| 160 | ]; |
| 161 | } |
| 162 | } |