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