1<?php 2 3namespace dokuwiki\plugin\icon; 4 5use dokuwiki\HTTP\HTTPClient; 6use dokuwiki\StyleUtils; 7 8class SVG 9{ 10 const SOURCES = [ 11 'mdi' => "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/%s.svg", 12 'fab' => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/brands/%s.svg", 13 'fas' => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/solid/%s.svg", 14 'fa' => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/regular/%s.svg", 15 'twbs' => "https://raw.githubusercontent.com/twbs/icons/main/icons/%s.svg", 16 ]; 17 18 protected $file; 19 protected $svg; 20 21 protected $url = ''; 22 protected $color = 'currentColor'; 23 protected $width = 'auto'; 24 protected $height = '1.2em'; 25 26 public function __construct($source, $icon) 27 { 28 if (!isset(self::SOURCES[$source])) throw new \RuntimeException('Unknown icon source'); 29 $this->url = sprintf(self::SOURCES[$source], $icon); 30 } 31 32 public function getFile() 33 { 34 $cache = getCacheName( 35 join("\n", [$this->url, $this->color, $this->width, $this->height]), 36 '.icon.svg' 37 ); 38 39 if (!file_exists($cache) || filemtime($cache) < filemtime(__FILE__)) { 40 $http = new HTTPClient(); 41 $svg = $http->get($this->url); 42 if (!$svg) throw new \RuntimeException('Failed to download SVG: ' . $http->error); 43 $svg = $this->processSVG($svg); 44 io_saveFile($cache, $svg); 45 } 46 47 return $cache; 48 } 49 50 /** 51 * Set the intrinsic width 52 * 53 * @param string $width 54 * @return void 55 */ 56 public function setWidth($width) 57 { 58 // these are fine 59 if ($width == 'auto' || $width == '') { 60 $this->width = $width; 61 return; 62 } 63 64 // we accept numbers and units only 65 if (preg_match('/^\d*\.?\d+(px|em|ex|pt|in|pc|mm|cm|rem|vh|vw)?$/', $width)) { 66 $this->width = $width; 67 return; 68 } 69 70 $this->width = 'auto'; // fall back to default 71 } 72 73 /** 74 * Set the intrinsic height 75 * 76 * @param string $height 77 * @return void 78 */ 79 public function setHeight($height) 80 { 81 // these are fine 82 if ($height == 'auto' || $height == '') { 83 $this->height = $height; 84 return; 85 } 86 87 // we accept numbers and units only 88 if (preg_match('/^\d*\.?\d+(px|em|ex|pt|in|pc|mm|cm|rem|vh|vw)?$/', $height)) { 89 $this->height = $height; 90 return; 91 } 92 93 $this->height = '1.3em'; // fall back to default 94 } 95 96 /** 97 * Set the fill color to use 98 * 99 * Can be a hex color or a style.ini replacement 100 * 101 * @param string $color 102 * @return void 103 */ 104 public function setColor($color) 105 { 106 // these are fine 107 if ($color == 'currentColor' || $color == '') { 108 $this->color = $color; 109 return; 110 } 111 112 // hex colors are easy too 113 if (self::isHexColor($color)) { 114 $this->color = '#' . ltrim($color, '#'); 115 return; 116 } 117 // see if the given color is an ini replacement 118 $styleUtil = new StyleUtils(); 119 $ini = $styleUtil->cssStyleini(); 120 if (isset($ini['replacements'][$color]) && self::isHexColor($ini['replacements'][$color])) { 121 $this->color = '#' . ltrim($ini['replacements'][$color], '#'); 122 return; 123 } 124 if (isset($ini['replacements']["__{$color}__"]) && self::isHexColor($ini['replacements']["__{$color}__"])) { 125 $this->color = '#' . ltrim($ini['replacements']["__{$color}__"], '#'); 126 return; 127 } 128 129 $this->color = 'currentColor'; 130 } 131 132 /** 133 * Check if the given value is a hex color 134 * 135 * @link https://stackoverflow.com/a/53330328 136 * @param string $color 137 * @return bool 138 */ 139 public static function isHexColor($color) 140 { 141 return (bool)preg_match('/^#?(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$/i', $color); 142 } 143 144 /** 145 * Minify SVG and apply transofrmations 146 * 147 * @param string $svgdata 148 * @return string 149 */ 150 protected function processSVG($svgdata) 151 { 152 // strip namespace declarations FIXME is there a cleaner way? 153 $svgdata = preg_replace('/\sxmlns(:.*?)?="(.*?)"/', '', $svgdata); 154 155 $dom = new \DOMDocument(); 156 $dom->loadXML($svgdata, LIBXML_NOBLANKS); 157 $dom->formatOutput = false; 158 $dom->preserveWhiteSpace = false; 159 160 $svg = $dom->getElementsByTagName('svg')->item(0); 161 162 // prefer viewbox over width/height 163 if (!$svg->hasAttribute('viewBox')) { 164 $w = $svg->getAttribute('width'); 165 $h = $svg->getAttribute('height'); 166 if ($w && $h) { 167 $svg->setAttribute('viewBox', "0 0 $w $h"); 168 } 169 } 170 171 // remove unwanted attributes from root 172 $this->removeAttributes($svg, ['viewBox']); 173 174 // remove unwanted attributes from primitives 175 foreach ($dom->getElementsByTagName('path') as $elem) { 176 $this->removeAttributes($elem, ['d']); 177 } 178 foreach ($dom->getElementsByTagName('rect') as $elem) { 179 $this->removeAttributes($elem, ['x', 'y', 'rx', 'ry']); 180 } 181 foreach ($dom->getElementsByTagName('circle') as $elem) { 182 $this->removeAttributes($elem, ['cx', 'cy', 'r']); 183 } 184 foreach ($dom->getElementsByTagName('ellipse') as $elem) { 185 $this->removeAttributes($elem, ['cx', 'cy', 'rx', 'ry']); 186 } 187 foreach ($dom->getElementsByTagName('line') as $elem) { 188 $this->removeAttributes($elem, ['x1', 'x2', 'y1', 'y2']); 189 } 190 foreach ($dom->getElementsByTagName('polyline') as $elem) { 191 $this->removeAttributes($elem, ['points']); 192 } 193 foreach ($dom->getElementsByTagName('polygon') as $elem) { 194 $this->removeAttributes($elem, ['points']); 195 } 196 197 // remove comments see https://stackoverflow.com/a/60420210 198 $xpath = new \DOMXPath($dom); 199 for ($els = $xpath->query('//comment()'), $i = $els->length - 1; $i >= 0; $i--) { 200 $els->item($i)->parentNode->removeChild($els->item($i)); 201 } 202 203 // readd namespace 204 $svg->setAttribute('xmlns', 'http://www.w3.org/2000/svg'); 205 206 if ($this->color) $svg->setAttribute('fill', $this->color); 207 if ($this->width) $svg->setAttribute('width', $this->width); 208 if ($this->height) $svg->setAttribute('height', $this->height); 209 210 $svgdata = $dom->saveXML($svg); 211 return $svgdata; 212 } 213 214 /** 215 * Remove all attributes except the given keepers 216 * 217 * @param \DOMNode $element 218 * @param string[] $keep 219 */ 220 protected function removeAttributes($element, $keep) 221 { 222 $attributes = $element->attributes; 223 for ($i = $attributes->length - 1; $i >= 0; $i--) { 224 $name = $attributes->item($i)->name; 225 if (in_array($name, $keep)) continue; 226 $element->removeAttribute($name); 227 } 228 } 229 230} 231