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); |
|
|
|
|
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
|
|
|
|