"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; } }