11a23d1dbSAndreas Gohr<?php 21a23d1dbSAndreas Gohr 31a23d1dbSAndreas Gohrnamespace dokuwiki\plugin\dev; 41a23d1dbSAndreas Gohr 51a23d1dbSAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient; 61a23d1dbSAndreas Gohruse splitbrain\phpcli\CLI; 71a23d1dbSAndreas Gohr 81a23d1dbSAndreas Gohr/** 91a23d1dbSAndreas Gohr * Download and clean SVG icons 101a23d1dbSAndreas Gohr */ 111a23d1dbSAndreas Gohrclass SVGIcon 121a23d1dbSAndreas Gohr{ 131a23d1dbSAndreas Gohr 141a23d1dbSAndreas Gohr const SOURCES = [ 151a23d1dbSAndreas Gohr 'mdi' => "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/%s.svg", 161a23d1dbSAndreas Gohr 'fab' => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/brands/%s.svg", 171a23d1dbSAndreas Gohr 'fas' => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/solid/%s.svg", 181a23d1dbSAndreas Gohr 'fa' => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/regular/%s.svg", 191a23d1dbSAndreas Gohr 'twbs' => "https://raw.githubusercontent.com/twbs/icons/main/icons/%s.svg", 201a23d1dbSAndreas Gohr ]; 211a23d1dbSAndreas Gohr 22*92738407SAndreas Gohr /** @var CLI for logging */ 231a23d1dbSAndreas Gohr protected $logger; 241a23d1dbSAndreas Gohr 25*92738407SAndreas Gohr /** @var bool keep the SVG namespace for when the image is not used in embed? */ 26*92738407SAndreas Gohr protected $keepns = false; 27*92738407SAndreas Gohr 281a23d1dbSAndreas Gohr /** 291a23d1dbSAndreas Gohr * @throws \Exception 301a23d1dbSAndreas Gohr */ 311a23d1dbSAndreas Gohr public function __construct(CLI $logger) 321a23d1dbSAndreas Gohr { 331a23d1dbSAndreas Gohr $this->logger = $logger; 341a23d1dbSAndreas Gohr } 351a23d1dbSAndreas Gohr 361a23d1dbSAndreas Gohr /** 37*92738407SAndreas Gohr * Call before cleaning to keep the SVG namespace 38*92738407SAndreas Gohr * 39*92738407SAndreas Gohr * @param bool $keep 40*92738407SAndreas Gohr */ 41*92738407SAndreas Gohr public function keepNamespace($keep = true) 42*92738407SAndreas Gohr { 43*92738407SAndreas Gohr $this->keepns = $keep; 44*92738407SAndreas Gohr } 45*92738407SAndreas Gohr 46*92738407SAndreas Gohr /** 471a23d1dbSAndreas Gohr * Download and save a remote icon 481a23d1dbSAndreas Gohr * 491a23d1dbSAndreas Gohr * @param string $ident prefixed name of the icon 501a23d1dbSAndreas Gohr * @param string $save 511a23d1dbSAndreas Gohr * @return bool 521a23d1dbSAndreas Gohr * @throws \Exception 531a23d1dbSAndreas Gohr */ 541a23d1dbSAndreas Gohr public function downloadRemoteIcon($ident, $save = '') 551a23d1dbSAndreas Gohr { 561a23d1dbSAndreas Gohr $icon = $this->remoteIcon($ident); 571a23d1dbSAndreas Gohr $svgdata = $this->fetchSVG($icon['url']); 581a23d1dbSAndreas Gohr $svgdata = $this->cleanSVG($svgdata); 591a23d1dbSAndreas Gohr 601a23d1dbSAndreas Gohr if (!$save) { 611a23d1dbSAndreas Gohr $save = $icon['name'] . '.svg'; 621a23d1dbSAndreas Gohr } 631a23d1dbSAndreas Gohr 641a23d1dbSAndreas Gohr io_makeFileDir($save); 651a23d1dbSAndreas Gohr $ok = io_saveFile($save, $svgdata); 661a23d1dbSAndreas Gohr if ($ok) $this->logger->success('saved ' . $save); 671a23d1dbSAndreas Gohr return $ok; 681a23d1dbSAndreas Gohr } 691a23d1dbSAndreas Gohr 701a23d1dbSAndreas Gohr /** 711a23d1dbSAndreas Gohr * Clean an existing SVG file 721a23d1dbSAndreas Gohr * 731a23d1dbSAndreas Gohr * @param string $file 741a23d1dbSAndreas Gohr * @return bool 751a23d1dbSAndreas Gohr * @throws \Exception 761a23d1dbSAndreas Gohr */ 771a23d1dbSAndreas Gohr public function cleanSVGFile($file) 781a23d1dbSAndreas Gohr { 791a23d1dbSAndreas Gohr $svgdata = io_readFile($file, false); 801a23d1dbSAndreas Gohr if (!$svgdata) { 811a23d1dbSAndreas Gohr throw new \Exception('Failed to read ' . $file); 821a23d1dbSAndreas Gohr } 831a23d1dbSAndreas Gohr 841a23d1dbSAndreas Gohr $svgdata = $this->cleanSVG($svgdata); 851a23d1dbSAndreas Gohr $ok = io_saveFile($file, $svgdata); 861a23d1dbSAndreas Gohr if ($ok) $this->logger->success('saved ' . $file); 871a23d1dbSAndreas Gohr return $ok; 881a23d1dbSAndreas Gohr } 891a23d1dbSAndreas Gohr 901a23d1dbSAndreas Gohr /** 911a23d1dbSAndreas Gohr * Get info about an icon from a known remote repository 921a23d1dbSAndreas Gohr * 931a23d1dbSAndreas Gohr * @param string $ident prefixed name of the icon 941a23d1dbSAndreas Gohr * @return array 951a23d1dbSAndreas Gohr * @throws \Exception 961a23d1dbSAndreas Gohr */ 971a23d1dbSAndreas Gohr public function remoteIcon($ident) 981a23d1dbSAndreas Gohr { 991a23d1dbSAndreas Gohr if (strpos($ident, ':')) { 1001a23d1dbSAndreas Gohr list($prefix, $name) = explode(':', $ident); 1011a23d1dbSAndreas Gohr } else { 1021a23d1dbSAndreas Gohr $prefix = 'mdi'; 1031a23d1dbSAndreas Gohr $name = $ident; 1041a23d1dbSAndreas Gohr } 1051a23d1dbSAndreas Gohr if (!isset(self::SOURCES[$prefix])) { 1061a23d1dbSAndreas Gohr throw new \Exception("Unknown prefix $prefix"); 1071a23d1dbSAndreas Gohr } 1081a23d1dbSAndreas Gohr 1091a23d1dbSAndreas Gohr $url = sprintf(self::SOURCES[$prefix], $name); 1101a23d1dbSAndreas Gohr 1111a23d1dbSAndreas Gohr return [ 1121a23d1dbSAndreas Gohr 'prefix' => $prefix, 1131a23d1dbSAndreas Gohr 'name' => $name, 1141a23d1dbSAndreas Gohr 'url' => $url, 1151a23d1dbSAndreas Gohr ]; 1161a23d1dbSAndreas Gohr } 1171a23d1dbSAndreas Gohr 1181a23d1dbSAndreas Gohr /** 119*92738407SAndreas Gohr * Minify SVG 1201a23d1dbSAndreas Gohr * 1211a23d1dbSAndreas Gohr * @param string $svgdata 1221a23d1dbSAndreas Gohr * @return string 1231a23d1dbSAndreas Gohr */ 1241a23d1dbSAndreas Gohr protected function cleanSVG($svgdata) 1251a23d1dbSAndreas Gohr { 1261a23d1dbSAndreas Gohr $old = strlen($svgdata); 1271a23d1dbSAndreas Gohr 1281a23d1dbSAndreas Gohr // strip namespace declarations FIXME is there a cleaner way? 1291a23d1dbSAndreas Gohr $svgdata = preg_replace('/\sxmlns(:.*?)?="(.*?)"/', '', $svgdata); 1301a23d1dbSAndreas Gohr 1311a23d1dbSAndreas Gohr $dom = new \DOMDocument(); 1321a23d1dbSAndreas Gohr $dom->loadXML($svgdata, LIBXML_NOBLANKS); 1331a23d1dbSAndreas Gohr $dom->formatOutput = false; 1341a23d1dbSAndreas Gohr $dom->preserveWhiteSpace = false; 1351a23d1dbSAndreas Gohr 1361a23d1dbSAndreas Gohr $svg = $dom->getElementsByTagName('svg')->item(0); 1371a23d1dbSAndreas Gohr 1381a23d1dbSAndreas Gohr // prefer viewbox over width/height 1391a23d1dbSAndreas Gohr if (!$svg->hasAttribute('viewBox')) { 1401a23d1dbSAndreas Gohr $w = $svg->getAttribute('width'); 1411a23d1dbSAndreas Gohr $h = $svg->getAttribute('height'); 1421a23d1dbSAndreas Gohr if ($w && $h) { 1431a23d1dbSAndreas Gohr $svg->setAttribute('viewBox', "0 0 $w $h"); 1441a23d1dbSAndreas Gohr } 1451a23d1dbSAndreas Gohr } 1461a23d1dbSAndreas Gohr 1471a23d1dbSAndreas Gohr // remove unwanted attributes from root 1481a23d1dbSAndreas Gohr $this->removeAttributes($svg, ['viewBox']); 1491a23d1dbSAndreas Gohr 1501a23d1dbSAndreas Gohr // remove unwanted attributes from primitives 1511a23d1dbSAndreas Gohr foreach ($dom->getElementsByTagName('path') as $elem) { 1521a23d1dbSAndreas Gohr $this->removeAttributes($elem, ['d']); 1531a23d1dbSAndreas Gohr } 1541a23d1dbSAndreas Gohr foreach ($dom->getElementsByTagName('rect') as $elem) { 1551a23d1dbSAndreas Gohr $this->removeAttributes($elem, ['x', 'y', 'rx', 'ry']); 1561a23d1dbSAndreas Gohr } 1571a23d1dbSAndreas Gohr foreach ($dom->getElementsByTagName('circle') as $elem) { 1581a23d1dbSAndreas Gohr $this->removeAttributes($elem, ['cx', 'cy', 'r']); 1591a23d1dbSAndreas Gohr } 1601a23d1dbSAndreas Gohr foreach ($dom->getElementsByTagName('ellipse') as $elem) { 1611a23d1dbSAndreas Gohr $this->removeAttributes($elem, ['cx', 'cy', 'rx', 'ry']); 1621a23d1dbSAndreas Gohr } 1631a23d1dbSAndreas Gohr foreach ($dom->getElementsByTagName('line') as $elem) { 1641a23d1dbSAndreas Gohr $this->removeAttributes($elem, ['x1', 'x2', 'y1', 'y2']); 1651a23d1dbSAndreas Gohr } 1661a23d1dbSAndreas Gohr foreach ($dom->getElementsByTagName('polyline') as $elem) { 1671a23d1dbSAndreas Gohr $this->removeAttributes($elem, ['points']); 1681a23d1dbSAndreas Gohr } 1691a23d1dbSAndreas Gohr foreach ($dom->getElementsByTagName('polygon') as $elem) { 1701a23d1dbSAndreas Gohr $this->removeAttributes($elem, ['points']); 1711a23d1dbSAndreas Gohr } 1721a23d1dbSAndreas Gohr 1731a23d1dbSAndreas Gohr // remove comments see https://stackoverflow.com/a/60420210 1741a23d1dbSAndreas Gohr $xpath = new \DOMXPath($dom); 1751a23d1dbSAndreas Gohr for ($els = $xpath->query('//comment()'), $i = $els->length - 1; $i >= 0; $i--) { 1761a23d1dbSAndreas Gohr $els->item($i)->parentNode->removeChild($els->item($i)); 1771a23d1dbSAndreas Gohr } 1781a23d1dbSAndreas Gohr 179*92738407SAndreas Gohr // readd namespace if not meant for embedding 180*92738407SAndreas Gohr if ($this->keepns) { 181*92738407SAndreas Gohr $svg->setAttribute('xmlns', 'http://www.w3.org/2000/svg'); 182*92738407SAndreas Gohr } 183*92738407SAndreas Gohr 1841a23d1dbSAndreas Gohr $svgdata = $dom->saveXML($svg); 1851a23d1dbSAndreas Gohr $new = strlen($svgdata); 1861a23d1dbSAndreas Gohr 1871a23d1dbSAndreas Gohr $this->logger->info(sprintf('Minified SVG %d bytes -> %d bytes (%.2f%%)', $old, $new, $new * 100 / $old)); 1881a23d1dbSAndreas Gohr if ($new > 2048) { 1891a23d1dbSAndreas Gohr $this->logger->warning('%d bytes is still too big for standard inlineSVG() limit!'); 1901a23d1dbSAndreas Gohr } 1911a23d1dbSAndreas Gohr return $svgdata; 1921a23d1dbSAndreas Gohr } 1931a23d1dbSAndreas Gohr 1941a23d1dbSAndreas Gohr /** 1951a23d1dbSAndreas Gohr * Remove all attributes except the given keepers 1961a23d1dbSAndreas Gohr * 1971a23d1dbSAndreas Gohr * @param \DOMNode $element 1981a23d1dbSAndreas Gohr * @param string[] $keep 1991a23d1dbSAndreas Gohr */ 2001a23d1dbSAndreas Gohr protected function removeAttributes($element, $keep) 2011a23d1dbSAndreas Gohr { 2021a23d1dbSAndreas Gohr $attributes = $element->attributes; 2031a23d1dbSAndreas Gohr for ($i = $attributes->length - 1; $i >= 0; $i--) { 2041a23d1dbSAndreas Gohr $name = $attributes->item($i)->name; 2051a23d1dbSAndreas Gohr if (in_array($name, $keep)) continue; 2061a23d1dbSAndreas Gohr $element->removeAttribute($name); 2071a23d1dbSAndreas Gohr } 2081a23d1dbSAndreas Gohr } 2091a23d1dbSAndreas Gohr 2101a23d1dbSAndreas Gohr /** 2111a23d1dbSAndreas Gohr * Fetch the content from the given URL 2121a23d1dbSAndreas Gohr * 2131a23d1dbSAndreas Gohr * @param string $url 2141a23d1dbSAndreas Gohr * @return string 2151a23d1dbSAndreas Gohr * @throws \Exception 2161a23d1dbSAndreas Gohr */ 2171a23d1dbSAndreas Gohr protected function fetchSVG($url) 2181a23d1dbSAndreas Gohr { 2191a23d1dbSAndreas Gohr $http = new DokuHTTPClient(); 2201a23d1dbSAndreas Gohr $svg = $http->get($url); 2211a23d1dbSAndreas Gohr 2221a23d1dbSAndreas Gohr if (!$svg) { 2231a23d1dbSAndreas Gohr throw new \Exception("Failed to download $url: " . $http->status . ' ' . $http->error); 2241a23d1dbSAndreas Gohr } 2251a23d1dbSAndreas Gohr 2261a23d1dbSAndreas Gohr return $svg; 2271a23d1dbSAndreas Gohr } 2281a23d1dbSAndreas Gohr 2291a23d1dbSAndreas Gohr} 230