xref: /template/sprintdoc/svg.php (revision 4ac10cf3be8e2e469f501800b9e04453d6737427)
1<?php
2
3namespace dokuwiki\template\sprintdoc;
4
5define('NOSESSION', 1);
6if(!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__) . '/../../../');
7require_once(DOKU_INC . 'inc/init.php');
8
9/**
10 * Custom XML node that allows prepending
11 */
12class SvgNode extends \SimpleXMLElement {
13    /**
14     * @param string $name Name of the new node
15     * @param null|string $value
16     * @return SvgNode
17     */
18    public function prependChild($name, $value = null) {
19        $dom = dom_import_simplexml($this);
20
21        $new = $dom->insertBefore(
22            $dom->ownerDocument->createElement($name, $value),
23            $dom->firstChild
24        );
25
26        return simplexml_import_dom($new, get_class($this));
27    }
28
29    /**
30     * @param \SimpleXMLElement $node the node to be added
31     * @return \SimpleXMLElement
32     */
33    public function appendNode(\SimpleXMLElement $node) {
34        $dom = dom_import_simplexml($this);
35        $domNode = dom_import_simplexml($node);
36
37        $newNode = $dom->appendChild($domNode);
38        return simplexml_import_dom($newNode, get_class($this));
39    }
40
41    /**
42     * @param \SimpleXMLElement $node the child to remove
43     * @return \SimpleXMLElement
44     */
45    public function removeChild(\SimpleXMLElement $node) {
46        $dom = dom_import_simplexml($node);
47        $dom->parentNode->removeChild($dom);
48        return $node;
49    }
50
51    /**
52     * Wraps all elements of $this in a `<g>` tag
53     *
54     * @return SvgNode
55     */
56    public function groupChildren() {
57        $dom = dom_import_simplexml($this);
58
59        $g = $dom->ownerDocument->createElement('g');
60        while($dom->childNodes->length > 0) {
61            $child = $dom->childNodes->item(0);
62            $dom->removeChild($child);
63            $g->appendChild($child);
64        }
65        $g = $dom->appendChild($g);
66
67        return simplexml_import_dom($g, get_class($this));
68    }
69
70    /**
71     * Add new style definitions to this element
72     * @param string $style
73     */
74    public function addStyle($style) {
75        $defs = $this->defs;
76        if(!$defs) {
77            $defs = $this->prependChild('defs');
78        }
79        $defs->addChild('style', $style);
80    }
81}
82
83/**
84 * Manage SVG recoloring
85 */
86class SVG {
87
88    const IMGDIR = __DIR__ . '/img/';
89    const BACKGROUNDCLASS = 'sprintdoc-background';
90    const CDNBASE = 'https://cdn.rawgit.com/Templarian/MaterialDesign/master/icons/svg/';
91
92    protected $file;
93    protected $replacements;
94
95    /**
96     * SVG constructor
97     */
98    public function __construct() {
99        global $INPUT;
100
101        $svg = cleanID($INPUT->str('svg'));
102        if(blank($svg)) $this->abort(404);
103
104        // try local file first
105        $file = self::IMGDIR . $svg;
106        if(!file_exists($file)) {
107            // try media file
108            $file = mediaFN($svg);
109            if(file_exists($file)) {
110                // media files are ACL protected
111                if(auth_quickaclcheck($svg) < AUTH_READ) $this->abort(403);
112            } else {
113                // get it from material design icons
114                $file = getCacheName($svg, '.svg');
115                if (!file_exists($file)) {
116                    io_download(self::CDNBASE . $svg, $file);
117                }
118            }
119
120        }
121        // check if media exists
122        if(!file_exists($file)) $this->abort(404);
123
124        $this->file = $file;
125    }
126
127    /**
128     * Generate and output
129     */
130    public function out() {
131        global $conf;
132        $file = $this->file;
133        $params = $this->getParameters();
134
135        header('Content-Type: image/svg+xml');
136        $cachekey = md5($file . serialize($params) . $conf['template'] . filemtime(__FILE__));
137        $cache = new \cache($cachekey, '.svg');
138        $cache->_event = 'SVG_CACHE';
139
140        http_cached($cache->cache, $cache->useCache(array('files' => array($file, __FILE__))));
141        if($params['e']) {
142            $content = $this->embedSVG($file);
143        } else {
144            $content = $this->generateSVG($file, $params);
145        }
146        http_cached_finish($cache->cache, $content);
147    }
148
149    /**
150     * Generate a new SVG based on the input file and the parameters
151     *
152     * @param string $file the SVG file to load
153     * @param array $params the parameters as returned by getParameters()
154     * @return string the new XML contents
155     */
156    protected function generateSVG($file, $params) {
157        /** @var SvgNode $xml */
158        $xml = simplexml_load_file($file, SvgNode::class);
159        $xml->addStyle($this->makeStyle($params));
160        $this->createBackground($xml);
161        $xml->groupChildren();
162
163        return $xml->asXML();
164    }
165
166    /**
167     * Return the absolute minimum path definition for direct embedding
168     *
169     * No styles will be applied. They have to be done in CSS
170     *
171     * @param string $file the SVG file to load
172     * @return string the new XML contents
173     */
174    protected function embedSVG($file) {
175        /** @var SvgNode $xml */
176        $xml = simplexml_load_file($file, SvgNode::class);
177
178        $def = hsc((string) $xml->path['d']);
179        $w = hsc($xml['width']);
180        $h = hsc($xml['height']);
181        $v = hsc($xml['viewBox']);
182
183        return "<svg width=\"$w\" height=\"$h\" viewBox=\"$v\"><path d=\"$def\" /></svg>";
184    }
185
186    /**
187     * Get the supported parameters from request
188     *
189     * @return array
190     */
191    protected function getParameters() {
192        global $INPUT;
193
194        $params = array(
195            'e' => $INPUT->bool('e', false),
196            's' => $this->fixColor($INPUT->str('s')),
197            'f' => $this->fixColor($INPUT->str('f')),
198            'b' => $this->fixColor($INPUT->str('b')),
199            'sh' => $this->fixColor($INPUT->str('sh')),
200            'fh' => $this->fixColor($INPUT->str('fh')),
201            'bh' => $this->fixColor($INPUT->str('bh')),
202        );
203
204        return $params;
205    }
206
207    /**
208     * Generate a style setting from the input variables
209     *
210     * @param array $params associative array with the given parameters
211     * @return string
212     */
213    protected function makeStyle($params) {
214        $element = 'path'; // FIXME configurable?
215
216        if(empty($params['b'])) {
217            $params['b'] = $this->fixColor('00000000');
218        }
219
220        $style = 'g rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['b'] . ';}';
221
222        if($params['bh']) {
223            $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['bh'] . ';}';
224        }
225
226        if($params['s'] || $params['f']) {
227            $style .= 'g ' . $element . '{';
228            if($params['s']) $style .= 'stroke:' . $params['s'] . ';';
229            if($params['f']) $style .= 'fill:' . $params['f'] . ';';
230            $style .= '}';
231        }
232
233        if($params['sh'] || $params['fh']) {
234            $style .= 'g:hover ' . $element . '{';
235            if($params['sh']) $style .= 'stroke:' . $params['sh'] . ';';
236            if($params['fh']) $style .= 'fill:' . $params['fh'] . ';';
237            $style .= '}';
238        }
239
240        return $style;
241    }
242
243    /**
244     * Takes a hexadecimal color string in the following forms:
245     *
246     * RGB
247     * RRGGBB
248     * RRGGBBAA
249     *
250     * Converts it to rgba() form.
251     *
252     * Alternatively takes a replacement name from the current template's style.ini
253     *
254     * @param string $color
255     * @return string
256     */
257    protected function fixColor($color) {
258        if($color === '') return '';
259        if(preg_match('/^([0-9a-f])([0-9a-f])([0-9a-f])$/i', $color, $m)) {
260            $r = hexdec($m[1] . $m[1]);
261            $g = hexdec($m[2] . $m[2]);
262            $b = hexdec($m[3] . $m[3]);
263            $a = hexdec('ff');
264        } elseif(preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i', $color, $m)) {
265            $r = hexdec($m[1]);
266            $g = hexdec($m[2]);
267            $b = hexdec($m[3]);
268            if(isset($m[4])) {
269                $a = hexdec($m[4]);
270            } else {
271                $a = hexdec('ff');
272            }
273        } else {
274            if(is_null($this->replacements)) $this->initReplacements();
275            if(isset($this->replacements[$color])) {
276                return $this->replacements[$color];
277            }
278            if(isset($this->replacements['__' . $color . '__'])) {
279                return $this->replacements['__' . $color . '__'];
280            }
281            return '';
282        }
283
284        return "rgba($r,$g,$b,$a)";
285    }
286
287    /**
288     * sets a rectangular background of the size of the svg/this itself
289     *
290     * @param SvgNode $g
291     * @return SvgNode
292     */
293    protected function createBackground(SvgNode $g) {
294        $rect = $g->prependChild('rect');
295        $rect->addAttribute('class', self::BACKGROUNDCLASS);
296
297        $rect->addAttribute('x', '0');
298        $rect->addAttribute('y', '0');
299        $rect->addAttribute('height', '100%');
300        $rect->addAttribute('width', '100%');
301        return $rect;
302    }
303
304    /**
305     * Abort processing with given status code
306     *
307     * @param int $status
308     */
309    protected function abort($status) {
310        http_status($status);
311        exit;
312    }
313
314    /**
315     * Initialize the available replacement patterns
316     *
317     * Loads the style.ini from the template (and various local locations)
318     * via a core function only available through some hack.
319     */
320    protected function initReplacements() {
321        global $conf;
322        if (!class_exists('\dokuwiki\StyleUtils')) {
323            // Pre-Greebo Compatibility
324
325            define('SIMPLE_TEST', 1); // hacky shit
326            include DOKU_INC . 'lib/exe/css.php';
327            $ini = css_styleini($conf['template']);
328            $this->replacements = $ini['replacements'];
329            return;
330        }
331
332        $stuleUtils = new \dokuwiki\StyleUtils();
333        $ini = $stuleUtils->cssStyleini('sprintdoc');
334        $this->replacements = $ini['replacements'];
335    }
336}
337
338// main
339$svg = new SVG();
340$svg->out();
341
342
343