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