xref: /plugin/dev/SVGIcon.php (revision 1a23d1dba84779f6c148b47f8352f1ea9cc5b271)
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