Passed
Branch master (53ff77)
by Mr.
05:27 queued 02:43
created

SvgInline::getSvgSize()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 10
c 1
b 0
f 0
nc 9
nop 0
dl 0
loc 17
rs 9.2222
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
16
use function array_pad;
17
use function explode;
18
use function round;
19
20
/**
21
 * SvgInline provides a quick and easy way to access icons.
22
 */
23
class SvgInline implements SvgInlineInterface
24
{
25
    /** @var array Values for converting various units to pixels */
26
    private const PIXEL_MAP = [
27
        'px' => 1,
28
        'em' => 16,
29
        'ex' => 16 / 2,
30
        'pt' => 16 / 12,
31
        'pc' => 16,
32
        'in' => 16 * 6,
33
        'cm' => 16 / (2.54 / 6),
34
        'mm' => 16 / (25.4 / 6),
35
    ];
36
37
    /** @var Aliases Object used to resolve aliases */
38
    protected Aliases $aliases;
39
40
    /** @var array Class property */
41
    protected array $class;
42
43
    /** @var string Backup icon in case requested icon cannot be found */
44
    protected string $fallbackIcon;
45
46
    /** @var string Color of the icon. Set to empty string to disable this attribute */
47
    protected string $fill;
48
49
    /** @var array additional properties for the icon not set with Options */
50
    protected array $svgProperties;
51
52
    /** $var ContainerInterface $container */
53
    private ContainerInterface $container;
54
55
    /** @var IconInterface icon properties */
56
    private Object $icon;
57
58
    /** @var DOMDocument SVG file */
59
    private DOMDocument $svg;
60
61
    /** @var DOMElement SVG */
62
    private DOMElement $svgElement;
63
64
    /**
65
     * @param Aliases $aliases
66
     * @return $this
67
     */
68
    public function __construct(Aliases $aliases, ContainerInterface $container)
69
    {
70
        $this->aliases = $aliases;
71
        $this->container = $container;
72
        return $this;
73
    }
74
75
    /**
76
     * Magic function, sets icon properties.
77
     *
78
     * @param string $name  property name
79
     * @param array  $value property value
80
     * @return self updated object
81
     */
82
    public function __call(string $name, $value): self
83
    {
84
        $function = 'set' . ucfirst($name);
85
        $this->icon->$function($value[0]);
86
        return $this;
87
    }
88
89
    /**
90
     * Magic function, returns the SVG string.
91
     *
92
     * @return string SVG data
93
     */
94
    public function __toString(): string
95
    {
96
        libxml_clear_errors();
97
        libxml_use_internal_errors(true);
98
        $this->svg = new DOMDocument();
99
100
        $this->loadSvg();
101
        $this->setSvgSize();
102
        $this->setSvgProperties();
103
        $this->setSvgAttributes();
104
105
        return $this->svg->saveXML($this->svgElement);
106
    }
107
108
    /**
109
     * Sets the Bootstrap Icon
110
     *
111
     * @param string $name name of the icon
112
     * @return SvgInlineBootstrapInterface component object
113
     */
114
    public function bootstrap(string $name): SvgInlineBootstrapInterface
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 SvgInlineFontAwesomeInterface component object
128
     */
129
    public function fai(string $name, ?string $style = null): SvgInlineFontAwesomeInterface
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 self component object
142
     */
143
    public function file(string $file): self
144
    {
145
        $this->icon = new Icon();
146
        $fileName = $this->aliases->get($file);
147
        $this->icon->setName($fileName);
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->getName();
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
     * @see $fallbackIcon
171
     * @param string $value
172
     * @return void
173
     */
174
    public function setFallbackIcon(string $value): void
175
    {
176
        $this->fallbackIcon = $this->aliases->get($value);
177
    }
178
179
    /**
180
     * @see $fill
181
     * @param string $value
182
     * @return void
183
     */
184
    public function setFill(string $value): void
185
    {
186
        $this->fill = $value;
187
    }
188
189
    /**
190
     * Determines size of the SVG element.
191
     *
192
     * @return void
193
     */
194
    protected function setSvgSize(): void
195
    {
196
        [$xStart, $yStart, $xEnd, $yEnd] = array_pad(explode(' ', $this->svgElement->getAttribute('viewBox')), -4, 0);
197
        $this->svgProperties['width'] = $this->getPixelValue($this->svgElement->getAttribute('width'));
198
        $this->svgProperties['height'] = $this->getPixelValue($this->svgElement->getAttribute('height'));
199
200
        $svgWidth = ($this->svgElement->hasAttribute('viewBox'))
201
            ? (int) $xEnd - (int) $xStart
202
            : $this->svgProperties['width'];
203
204
        $svgHeight = ($this->svgElement->hasAttribute('viewBox'))
205
            ? (int) $yEnd - (int) $yStart
206
            : $this->svgProperties['height'];
207
208
        $width = $this->icon->get('width');
209
        $height = $this->icon->get('height');
210
        if ($width || $height) {
211
            $this->svgProperties['width'] = $width ?? round($height * $svgWidth / $svgHeight);
212
            $this->svgProperties['height'] = $height ?? round($width * $svgHeight / $svgWidth);
213
        }
214
    }
215
216
    /**
217
     * Converts various sizes to pixels.
218
     *
219
     * @param string $size
220
     * @return int
221
     */
222
    private function getPixelValue(string $size): int
223
    {
224
        $trimmedSize = trim($size);
225
        $value = (int) $trimmedSize;
226
        $unit = substr($trimmedSize, -2);
227
228
        if (isset(self::PIXEL_MAP[$unit])) {
229
            $trimmedSize = $value * self::PIXEL_MAP[$unit];
230
        }
231
232
        return (int) round((float) $trimmedSize);
233
    }
234
235
    /**
236
     * Removes nodes from a DOMDocument
237
     *
238
     * @return void
239
     */
240
    private function removeDomNodes(DOMDocument $dom, string $expression): void
241
    {
242
        $xpath = new DOMXPath($dom);
243
        while ($node = $xpath->query($expression)->item(0)) {
244
            if ($node->parentNode) {
245
                $node->parentNode->removeChild($node);
246
            }
247
        }
248
    }
249
250
    /**
251
     * Adds the properties to the SVG.
252
     *
253
     * @return void
254
     */
255
    private function setSvgAttributes(): void
256
    {
257
        $title = $this->icon->get('title');
258
        if ($title) {
259
            $titleElement = $this->svg->createElement('title', $title);
260
            $this->svgElement->insertBefore($titleElement, $this->svgElement->firstChild);
261
        }
262
263
        foreach ($this->svgProperties as $key => $value) {
264
            $this->svgElement->removeAttribute($key);
265
            if (!empty($value)) {
266
                $this->svgElement->setAttribute($key, (string) $value);
267
            }
268
        }
269
    }
270
271
    /**
272
     * Prepares the values to be set on the SVG.
273
     *
274
     * @return void
275
     */
276
    private function setSvgProperties(): void
277
    {
278
        $this->svgProperties['aria-hidden'] = 'true';
279
        $this->svgProperties['role'] = 'img';
280
        $this->svgProperties['id'] = $this->icon->get('id');
281
        $this->svgProperties['class'] = $this->class['class'];
282
283
        $css = $this->icon->get('css');
284
        if (is_array($css)) {
285
            $this->svgProperties['style'] = Html::cssStyleFromArray($css);
286
        }
287
288
        $this->svgProperties['fill'] = $this->icon->get('fill') ?? $this->fill;
289
    }
290
}
291