Issues (1)

src/SvgInline.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace YiiRocks\SvgInline;
6
7
use DOMDocument;
8
use DOMElement;
9
use DOMXPath;
10
use Psr\Container\ContainerInterface;
11
use YiiRocks\SvgInline\Bootstrap\SvgInlineBootstrapInterface;
12
use YiiRocks\SvgInline\FontAwesome\SvgInlineFontAwesomeInterface;
13
use Yiisoft\Aliases\Aliases;
14
use Yiisoft\Html\Html;
15
use Yiisoft\Html\NoEncodeStringableInterface;
16
17
use function explode;
18
use function libxml_clear_errors;
19
use function libxml_use_internal_errors;
20
use function round;
21
use function ucfirst;
22
23
/**
24
 * SvgInline provides a quick and easy way to access icons.
25
 */
26
class SvgInline implements NoEncodeStringableInterface, SvgInlineInterface
27
{
28
    /** @var array Values for converting various units to pixels */
29
    private const PIXEL_MAP = [
30
        'px' => 1,
31
        'em' => 16,
32
        'ex' => 16 / 2,
33
        'pt' => 16 / 12,
34
        'pc' => 16,
35
        'in' => 16 * 6,
36
        'cm' => 16 / (2.54 / 6),
37
        'mm' => 16 / (25.4 / 6),
38
    ];
39
40
    /** @var Aliases Object used to resolve aliases */
41
    protected Aliases $aliases;
42
43
    /** @var array Class property */
44
    protected array $class;
45
46
    /** @var string Backup icon in case requested icon cannot be found */
47
    protected string $fallbackIcon;
48
49
    /** @var string Color of the icon. Set to empty string to disable this attribute */
50
    protected string $fill;
51
52
    /** @var int height of the svg */
53
    protected int $svgHeight;
54
55
    /** @var array additional properties for the icon not set with Options */
56
    protected array $svgProperties;
57
58
    /** @var int width of the svg */
59
    protected int $svgWidth;
60
61
    /** $var ContainerInterface $container */
62
    private ContainerInterface $container;
63
64
    /** @var IconInterface icon properties */
65
    private IconInterface $icon;
66
67
    /** @var DOMDocument SVG file */
68
    private DOMDocument $svg;
69
70
    /** @var DOMElement SVG */
71
    private DOMElement $svgElement;
72
73
    /**
74
     * @param Aliases $aliases
75
     * @param ContainerInterface $container
76
     */
77
    public function __construct(Aliases $aliases, ContainerInterface $container)
78
    {
79
        $this->aliases = $aliases;
80
        $this->container = $container;
81
    }
82
83
    /**
84
     * Magic function, sets icon properties.
85
     *
86
     * @param string $name  property name
87
     * @param array  $value property value
88
     * @return self updated object
89
     */
90
    public function __call(string $name, $value): SvgInlineInterface
91
    {
92
        $new = clone $this;
93
        $function = 'set' . ucfirst($name);
94
        $new->icon->$function($value[0]);
95
        return $new;
96
    }
97
98
    /**
99
     * Magic function, call render to return the SVG string.
100
     *
101
     * @return string SVG data
102
     */
103
    public function __toString(): string
104
    {
105
        return $this->render();
106
    }
107
108
    /**
109
     * Sets the Bootstrap Icon
110
     *
111
     * @param string $name name of the icon
112
     * @return SvgInlineInterface component object
113
     */
114
    public function bootstrap(string $name): SvgInlineInterface
115
    {
116
        $bootstrap = $this->container->get(SvgInlineBootstrapInterface::class);
117
        $bootstrap->icon = $bootstrap->name($name);
118
119
        return $bootstrap;
120
    }
121
122
    /**
123
     * Sets the Font Awesome Icon
124
     *
125
     * @param string $name name of the icon
126
     * $param null|string $style style of the icon
127
     * @return SvgInlineInterface component object
128
     */
129
    public function fai(string $name, ?string $style = null): SvgInlineInterface
130
    {
131
        $fai = $this->container->get(SvgInlineFontAwesomeInterface::class);
132
        $fai->icon = $fai->name($name, $style);
133
134
        return $fai;
135
    }
136
137
    /**
138
     * Sets the filename
139
     *
140
     * @param string $file name of the icon, or filename
141
     * @return SvgInlineInterface component object
142
     */
143
    public function file(string $file): SvgInlineInterface
144
    {
145
        $this->icon = new Icon();
146
        $fileName = $this->aliases->get($file);
147
        $this->icon->setName($fileName);
0 ignored issues
show
The method setName() does not exist on YiiRocks\SvgInline\IconInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to YiiRocks\SvgInline\IconInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

147
        $this->icon->/** @scrutinizer ignore-call */ 
148
                     setName($fileName);
Loading history...
148
149
        return $this;
150
    }
151
152
    /**
153
     * Load Font Awesome SVG file. Falls back to default if not found.
154
     *
155
     * @see $fallbackIcon
156
     */
157
    public function loadSvg(): void
158
    {
159
        $iconFile = $this->icon->get('name');
160
        if (!$this->svg->load($iconFile, LIBXML_NOBLANKS)) {
161
            $this->svg->load($this->fallbackIcon, LIBXML_NOBLANKS);
162
        }
163
164
        $this->removeDomNodes($this->svg, '//comment()');
165
        $this->svgElement = $this->svg->getElementsByTagName('svg')->item(0);
166
        $this->class = ['class' => $this->icon->get('class')];
167
    }
168
169
    /**
170
     * Returns the SVG string.
171
     *
172
     * @return string SVG data
173
     */
174
    public function render(): string
175
    {
176
        libxml_clear_errors();
177
        libxml_use_internal_errors(true);
178
        $this->svg = new DOMDocument();
179
180
        $this->loadSvg();
181
        $this->setSvgSize();
182
        $this->setSvgProperties();
183
        $this->setSvgAttributes();
184
185
        return $this->svg->saveXML($this->svgElement);
186
    }
187
188
    /**
189
     * @see $fallbackIcon
190
     * @param string $value
191
     * @return void
192
     */
193
    public function setFallbackIcon(string $value): void
194
    {
195
        $this->fallbackIcon = $this->aliases->get($value);
196
    }
197
198
    /**
199
     * @see $fill
200
     * @param string $value
201
     * @return void
202
     */
203
    public function setFill(string $value): void
204
    {
205
        $this->fill = $value;
206
    }
207
208
    /**
209
     * Determines size of the SVG element.
210
     *
211
     * @return void
212
     */
213
    protected function setSvgSize(): void
214
    {
215
        $this->svgWidth = $this->getPixelValue($this->svgElement->getAttribute('width'));
216
        $this->svgHeight = $this->getPixelValue($this->svgElement->getAttribute('height'));
217
        $this->svgProperties['width'] = $this->svgWidth;
218
        $this->svgProperties['height'] = $this->svgHeight;
219
220
        if ($this->svgElement->hasAttribute('viewBox')) {
221
            [$xStart, $yStart, $xEnd, $yEnd] = explode(' ', $this->svgElement->getAttribute('viewBox'));
222
            $this->svgWidth = (int) $xEnd - (int) $xStart;
223
            $this->svgHeight = (int) $yEnd - (int) $yStart;
224
225
            $this->svgElement->removeAttribute('width');
226
            $this->svgElement->removeAttribute('height');
227
            unset($this->svgProperties['width'], $this->svgProperties['height']);
228
        }
229
230
        $width = $this->icon->get('width');
231
        $height = $this->icon->get('height');
232
        if ($width || $height) {
233
            $this->svgProperties['width'] = $width ?? round($height * $this->svgWidth / $this->svgHeight);
234
            $this->svgProperties['height'] = $height ?? round($width * $this->svgHeight / $this->svgWidth);
235
        }
236
    }
237
238
    /**
239
     * Converts various sizes to pixels.
240
     *
241
     * @param string $size
242
     * @return int
243
     */
244
    private function getPixelValue(string $size): int
245
    {
246
        $trimmedSize = trim($size);
247
        $value = (int) $trimmedSize;
248
        $unit = substr($trimmedSize, -2);
249
250
        if (isset(self::PIXEL_MAP[$unit])) {
251
            $trimmedSize = $value * self::PIXEL_MAP[$unit];
252
        }
253
254
        return (int) round((float) $trimmedSize);
255
    }
256
257
    /**
258
     * Removes nodes from a DOMDocument
259
     *
260
     * @return void
261
     */
262
    private function removeDomNodes(DOMDocument $dom, string $expression): void
263
    {
264
        $xpath = new DOMXPath($dom);
265
        while ($node = $xpath->query($expression)->item(0)) {
266
            if ($node->parentNode) {
267
                $node->parentNode->removeChild($node);
268
            }
269
        }
270
    }
271
272
    /**
273
     * Adds the properties to the SVG.
274
     *
275
     * @return void
276
     */
277
    private function setSvgAttributes(): void
278
    {
279
        $titleElement = $this->svg->createElement('title', $this->icon->getTitle());
280
        $this->svgElement->insertBefore($titleElement, $this->svgElement->firstChild);
281
282
        foreach ($this->svgProperties as $key => $value) {
283
            $this->svgElement->removeAttribute($key);
284
            if (!empty($value)) {
285
                $this->svgElement->setAttribute($key, (string) $value);
286
            }
287
        }
288
    }
289
290
    /**
291
     * Prepares the values to be set on the SVG.
292
     *
293
     * @return void
294
     */
295
    private function setSvgProperties(): void
296
    {
297
        $this->svgProperties['aria-hidden'] = 'true';
298
        $this->svgProperties['role'] = 'img';
299
        $this->svgProperties['id'] = $this->icon->get('id');
300
        $this->svgProperties['class'] = $this->class['class'];
301
302
        $css = $this->icon->get('css');
303
        if (is_array($css)) {
304
            $this->svgProperties['style'] = Html::cssStyleFromArray($css);
305
        }
306
307
        $this->svgProperties['fill'] = $this->icon->get('fill') ?? $this->fill;
308
    }
309
}
310