-
Notifications
You must be signed in to change notification settings - Fork 2
/
SVGIcon.php
229 lines (197 loc) · 6.5 KB
/
SVGIcon.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
<?php
namespace dokuwiki\plugin\dev;
use dokuwiki\HTTP\DokuHTTPClient;
use splitbrain\phpcli\CLI;
/**
* Download and clean SVG icons
*/
class SVGIcon
{
const SOURCES = [
'mdi' => "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/%s.svg",
'fab' => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/brands/%s.svg",
'fas' => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/solid/%s.svg",
'fa' => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/regular/%s.svg",
'twbs' => "https://raw.githubusercontent.com/twbs/icons/main/icons/%s.svg",
];
/** @var CLI for logging */
protected $logger;
/** @var bool keep the SVG namespace for when the image is not used in embed? */
protected $keepns = false;
/**
* @throws \Exception
*/
public function __construct(CLI $logger)
{
$this->logger = $logger;
}
/**
* Call before cleaning to keep the SVG namespace
*
* @param bool $keep
*/
public function keepNamespace($keep = true)
{
$this->keepns = $keep;
}
/**
* Download and save a remote icon
*
* @param string $ident prefixed name of the icon
* @param string $save
* @return bool
* @throws \Exception
*/
public function downloadRemoteIcon($ident, $save = '')
{
$icon = $this->remoteIcon($ident);
$svgdata = $this->fetchSVG($icon['url']);
$svgdata = $this->cleanSVG($svgdata);
if (!$save) {
$save = $icon['name'] . '.svg';
}
io_makeFileDir($save);
$ok = io_saveFile($save, $svgdata);
if ($ok) $this->logger->success('saved ' . $save);
return $ok;
}
/**
* Clean an existing SVG file
*
* @param string $file
* @return bool
* @throws \Exception
*/
public function cleanSVGFile($file)
{
$svgdata = io_readFile($file, false);
if (!$svgdata) {
throw new \Exception('Failed to read ' . $file);
}
$svgdata = $this->cleanSVG($svgdata);
$ok = io_saveFile($file, $svgdata);
if ($ok) $this->logger->success('saved ' . $file);
return $ok;
}
/**
* Get info about an icon from a known remote repository
*
* @param string $ident prefixed name of the icon
* @return array
* @throws \Exception
*/
public function remoteIcon($ident)
{
if (strpos($ident, ':')) {
list($prefix, $name) = explode(':', $ident);
} else {
$prefix = 'mdi';
$name = $ident;
}
if (!isset(self::SOURCES[$prefix])) {
throw new \Exception("Unknown prefix $prefix");
}
$url = sprintf(self::SOURCES[$prefix], $name);
return [
'prefix' => $prefix,
'name' => $name,
'url' => $url,
];
}
/**
* Minify SVG
*
* @param string $svgdata
* @return string
*/
protected function cleanSVG($svgdata)
{
$old = strlen($svgdata);
// strip namespace declarations FIXME is there a cleaner way?
$svgdata = preg_replace('/\sxmlns(:.*?)?="(.*?)"/', '', $svgdata);
$dom = new \DOMDocument();
$dom->loadXML($svgdata, LIBXML_NOBLANKS);
$dom->formatOutput = false;
$dom->preserveWhiteSpace = false;
$svg = $dom->getElementsByTagName('svg')->item(0);
// prefer viewbox over width/height
if (!$svg->hasAttribute('viewBox')) {
$w = $svg->getAttribute('width');
$h = $svg->getAttribute('height');
if ($w && $h) {
$svg->setAttribute('viewBox', "0 0 $w $h");
}
}
// remove unwanted attributes from root
$this->removeAttributes($svg, ['viewBox']);
// remove unwanted attributes from primitives
foreach ($dom->getElementsByTagName('path') as $elem) {
$this->removeAttributes($elem, ['d']);
}
foreach ($dom->getElementsByTagName('rect') as $elem) {
$this->removeAttributes($elem, ['x', 'y', 'rx', 'ry']);
}
foreach ($dom->getElementsByTagName('circle') as $elem) {
$this->removeAttributes($elem, ['cx', 'cy', 'r']);
}
foreach ($dom->getElementsByTagName('ellipse') as $elem) {
$this->removeAttributes($elem, ['cx', 'cy', 'rx', 'ry']);
}
foreach ($dom->getElementsByTagName('line') as $elem) {
$this->removeAttributes($elem, ['x1', 'x2', 'y1', 'y2']);
}
foreach ($dom->getElementsByTagName('polyline') as $elem) {
$this->removeAttributes($elem, ['points']);
}
foreach ($dom->getElementsByTagName('polygon') as $elem) {
$this->removeAttributes($elem, ['points']);
}
// remove comments see https://stackoverflow.com/a/60420210
$xpath = new \DOMXPath($dom);
for ($els = $xpath->query('//comment()'), $i = $els->length - 1; $i >= 0; $i--) {
$els->item($i)->parentNode->removeChild($els->item($i));
}
// readd namespace if not meant for embedding
if ($this->keepns) {
$svg->setAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
$svgdata = $dom->saveXML($svg);
$new = strlen($svgdata);
$this->logger->info(sprintf('Minified SVG %d bytes -> %d bytes (%.2f%%)', $old, $new, $new * 100 / $old));
if ($new > 2048) {
$this->logger->warning('%d bytes is still too big for standard inlineSVG() limit!');
}
return $svgdata;
}
/**
* Remove all attributes except the given keepers
*
* @param \DOMNode $element
* @param string[] $keep
*/
protected function removeAttributes($element, $keep)
{
$attributes = $element->attributes;
for ($i = $attributes->length - 1; $i >= 0; $i--) {
$name = $attributes->item($i)->name;
if (in_array($name, $keep)) continue;
$element->removeAttribute($name);
}
}
/**
* Fetch the content from the given URL
*
* @param string $url
* @return string
* @throws \Exception
*/
protected function fetchSVG($url)
{
$http = new DokuHTTPClient();
$svg = $http->get($url);
if (!$svg) {
throw new \Exception("Failed to download $url: " . $http->status . ' ' . $http->error);
}
return $svg;
}
}