Files
dokuwiki-plugin-dev/SVGIcon.php
2021-11-30 21:57:46 +01:00

230 lines
6.5 KiB
PHP

<?php
namespace dokuwiki\plugin\dev;
use dokuwiki\HTTP\DokuHTTPClient;
use splitbrain\phpcli\CLI;
/**
* Download and clean SVG icons
*/
class SVGIcon
{
const SOURCES = [
'mdi' => "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;
}
}