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