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