Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
96.88% |
31 / 32 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
| Paint | |
96.88% |
31 / 32 |
|
50.00% |
1 / 2 |
20 | |
0.00% |
0 / 1 |
| parse | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
12 | |||
| urlToken | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
8.06 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Phpdftk\Svg\Value; |
| 6 | |
| 7 | use Phpdftk\Svg\Value\Paint\ContextPaint; |
| 8 | use Phpdftk\Svg\Value\Paint\CurrentColor; |
| 9 | use Phpdftk\Svg\Value\Paint\None_; |
| 10 | use Phpdftk\Svg\Value\Paint\SolidColor; |
| 11 | use Phpdftk\Svg\Value\Paint\Url; |
| 12 | |
| 13 | /** |
| 14 | * Parsed SVG paint value (SVG 2 ยง13.2 grammar): |
| 15 | * |
| 16 | * <paint> = none | <color> | <url> [ none | <color> ]? |
| 17 | * | context-fill | context-stroke | currentColor |
| 18 | * |
| 19 | * The implementation set is closed: `Paint\None_`, `Paint\CurrentColor`, |
| 20 | * `Paint\SolidColor`, `Paint\Url`, `Paint\ContextPaint`. The painter |
| 21 | * pattern-matches on the concrete type to choose the right PDF emit |
| 22 | * path (fill/stroke colour vs gradient/pattern reference). |
| 23 | * |
| 24 | * Color parsing delegates to `Phpdftk\Svg\Value\Color::parse()` which in |
| 25 | * turn produces a `Phpdftk\Color\ColorInterface` instance โ the SVG package |
| 26 | * depends on `phpdftk/color` for the typed colour model. |
| 27 | */ |
| 28 | abstract class Paint |
| 29 | { |
| 30 | /** |
| 31 | * Parse an SVG paint attribute value. Returns null on absent / empty / |
| 32 | * malformed input โ SVG 2's "invalid โ ignored" semantics. |
| 33 | */ |
| 34 | public static function parse(string $raw): ?self |
| 35 | { |
| 36 | $trimmed = trim($raw); |
| 37 | if ($trimmed === '') { |
| 38 | return null; |
| 39 | } |
| 40 | if (strcasecmp($trimmed, 'none') === 0) { |
| 41 | return new None_(); |
| 42 | } |
| 43 | if (strcasecmp($trimmed, 'currentColor') === 0) { |
| 44 | return new CurrentColor(); |
| 45 | } |
| 46 | // SVG 2 ยง13.2.1 โ defer to the element that referenced this |
| 47 | // subtree (a `<marker>`'s shape, a `<use>`). Resolved by the |
| 48 | // painter, which is the only place that knows the reference |
| 49 | // chain. |
| 50 | if (strcasecmp($trimmed, 'context-fill') === 0) { |
| 51 | return ContextPaint::fill(); |
| 52 | } |
| 53 | if (strcasecmp($trimmed, 'context-stroke') === 0) { |
| 54 | return ContextPaint::stroke(); |
| 55 | } |
| 56 | // `url(#id) [fallback]` โ the fallback is itself a paint, but |
| 57 | // restricted by SVG 2 to `none | <color>`. We re-enter parse() |
| 58 | // on the tail and reject anything that comes back as a Url. |
| 59 | if (preg_match('/^url\(([^)]*)\)\s*(.*)$/is', $trimmed, $m) === 1) { |
| 60 | $reference = self::urlToken($m[1]); |
| 61 | if ($reference === null || !str_starts_with($reference, '#')) { |
| 62 | // Either malformed, or an EXTERNAL reference. Neither |
| 63 | // is a local paint-server id, and reading one as if it |
| 64 | // were would resolve it against the wrong document. |
| 65 | return null; |
| 66 | } |
| 67 | $fallback = trim($m[2]); |
| 68 | $fallbackPaint = $fallback === '' ? null : self::parse($fallback); |
| 69 | if ($fallbackPaint instanceof Url) { |
| 70 | // url(#a) url(#b) isn't a legal SVG fallback chain. |
| 71 | $fallbackPaint = null; |
| 72 | } |
| 73 | return new Url(substr($reference, 1), $fallbackPaint); |
| 74 | } |
| 75 | $color = Color::parse($trimmed); |
| 76 | return $color === null ? null : new SolidColor($color); |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * The URL inside a `url(โฆ)`, with its quotes removed. |
| 81 | * |
| 82 | * CSS Values 4 ยง4.5.1 and SVG 2 ยง16.2: whitespace is allowed on |
| 83 | * both sides of the value, and inside a quoted form it is stripped |
| 84 | * along with the quotes โ `url(' #green ')` references `#green`. |
| 85 | * Only a MATCHED pair of quotes is a string; a lone quote is a |
| 86 | * parse error, and swallowing it would turn malformed author input |
| 87 | * into a silently different reference. |
| 88 | * |
| 89 | * Whitespace INSIDE the value survives, deliberately. `url(' # red |
| 90 | * ')` names the fragment `# red`, which matches no element, so the |
| 91 | * paint falls back โ exactly what the spec asks for and what |
| 92 | * "helpfully" collapsing it to `#red` would break. |
| 93 | */ |
| 94 | private static function urlToken(string $raw): ?string |
| 95 | { |
| 96 | $value = trim($raw); |
| 97 | $quote = $value[0] ?? ''; |
| 98 | if ($quote === '"' || $quote === "'") { |
| 99 | if (strlen($value) < 2 || !str_ends_with($value, $quote)) { |
| 100 | return null; |
| 101 | } |
| 102 | $value = substr($value, 1, -1); |
| 103 | } elseif (str_contains($value, '"') || str_contains($value, "'")) { |
| 104 | // An unquoted url-token may not contain a quote character. |
| 105 | return null; |
| 106 | } |
| 107 | $value = trim($value); |
| 108 | return $value === '' ? null : $value; |
| 109 | } |
| 110 | } |