xref: /template/sprintdoc/svg.php (revision 24ab1f725c47400c809211b7ebf1197468d8252d)
1<?php
2
3namespace dokuwiki\template\sprintdoc;
4
5if(!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__) . '/../../../');
6require_once(DOKU_INC . 'inc/init.php');
7
8/**
9 * Custom XML node that allows prepending
10 */
11class SvgNode extends \SimpleXMLElement {
12    /**
13     * @param string $name Name of the new node
14     * @param null|string $value
15     * @return SvgNode
16     */
17    public function prependChild($name, $value = null) {
18        $dom = dom_import_simplexml($this);
19
20        $new = $dom->insertBefore(
21            $dom->ownerDocument->createElement($name, $value),
22            $dom->firstChild
23        );
24
25        return simplexml_import_dom($new, get_class($this));
26    }
27
28    /**
29     * @param \SimpleXMLElement $node the node to be added
30     * @return \SimpleXMLElement
31     */
32    public function appendNode(\SimpleXMLElement $node) {
33        $dom = dom_import_simplexml($this);
34        $domNode = dom_import_simplexml($node);
35
36        $newNode = $dom->appendChild($domNode);
37        return simplexml_import_dom($newNode, get_class($this));
38    }
39
40    /**
41     * @param \SimpleXMLElement $node the child to remove
42     * @return \SimpleXMLElement
43     */
44    public function removeChild(\SimpleXMLElement $node) {
45        $dom = dom_import_simplexml($node);
46        $dom->parentNode->removeChild($dom);
47        return $node;
48    }
49
50    /**
51     * Wraps all elements of $this in a `<g>` tag
52     *
53     * @return SvgNode
54     */
55    public function groupChildren() {
56        $dom = dom_import_simplexml($this);
57
58        $g = $dom->ownerDocument->createElement('g');
59        while ($dom->childNodes->length > 0) {
60            $child = $dom->childNodes->item(0);
61            $dom->removeChild($child);
62            $g->appendChild($child);
63        }
64        $g = $dom->appendChild($g);
65
66        return simplexml_import_dom($g, get_class($this));
67    }
68
69    /**
70     * Add new style definitions to this element
71     * @param string $style
72     */
73    public function addStyle($style) {
74        $defs = $this->defs;
75        if(!$defs) {
76            $defs = $this->prependChild('defs');
77        }
78        $defs->addChild('style', $style);
79    }
80}
81
82/**
83 * Manage SVG recoloring
84 */
85class SVG {
86
87    const IMGDIR = __DIR__ . '/img/';
88    const BACKGROUNDCLASS = 'sprintdoc-background';
89
90    protected $file;
91
92    /**
93     * SVG constructor
94     */
95    public function __construct() {
96        global $INPUT;
97
98        $svg = cleanID($INPUT->str('svg'));
99        if(blank($svg)) $this->abort(404);
100
101        // try local file first
102        $file = self::IMGDIR . $svg;
103        if(!file_exists($file)) {
104            // media files are ACL protected
105            if(auth_quickaclcheck($svg) < AUTH_READ) $this->abort(403);
106            $file = mediaFN($svg);
107        }
108        // check if media exists
109        if(!file_exists($file)) $this->abort(404);
110
111        $this->file = $file;
112    }
113
114    /**
115     * Generate and output
116     */
117    public function out() {
118        $file = $this->file;
119        $params = $this->getParameters();
120
121        header('Content-Type: image/svg+xml');
122        $cachekey = md5($file . serialize($params));
123        $cache = new \cache($cachekey, '.svg');
124        $cache->_event = 'SVG_CACHE';
125
126        http_cached($cache->cache, $cache->useCache(array('files' => array($file, __FILE__))));
127        http_cached_finish($cache->cache, $this->generateSVG($file, $params));
128    }
129
130    /**
131     * Generate a new SVG based on the input file and the parameters
132     *
133     * @param string $file the SVG file to load
134     * @param array $params the parameters as returned by getParameters()
135     * @return string the new XML contents
136     */
137    protected function generateSVG($file, $params) {
138        /** @var SvgNode $xml */
139        $xml = simplexml_load_file($file, SvgNode::class);
140        $xml->addStyle($this->makeStyle($params));
141        $this->createBackground($xml);
142        $xml->groupChildren();
143
144        return $xml->asXML();
145    }
146
147    /**
148     * Get the supported parameters from request
149     *
150     * @return array
151     */
152    protected function getParameters() {
153        global $INPUT;
154
155        $params = array(
156            's' => $this->fixColor($INPUT->str('s')),
157            'f' => $this->fixColor($INPUT->str('f')),
158            'b' => $this->fixColor($INPUT->str('b')),
159            'sh' => $this->fixColor($INPUT->str('sh')),
160            'fh' => $this->fixColor($INPUT->str('fh')),
161            'bh' => $this->fixColor($INPUT->str('bh')),
162        );
163
164        return $params;
165    }
166
167    /**
168     * Generate a style setting from the input variables
169     *
170     * @param array $params associative array with the given parameters
171     * @return string
172     */
173    protected function makeStyle($params) {
174        $element = 'path'; // FIXME configurable?
175
176        if(empty($params['b'])) {
177            $params['b'] = $this->fixColor('00000000');
178        }
179
180        $style = 'g rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['b'] . ';}';
181
182        if($params['bh']) {
183            $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['bh'] . ';}';
184        }
185
186        if($params['s'] || $params['f']) {
187            $style .= 'g ' . $element . '{';
188            if($params['s']) $style .= 'stroke:' . $params['s'] . ';';
189            if($params['f']) $style .= 'fill:' . $params['f'] . ';';
190            $style .= '}';
191        }
192
193        if($params['sh'] || $params['fh']) {
194            $style .= 'g:hover ' . $element . '{';
195            if($params['sh']) $style .= 'stroke:' . $params['sh'] . ';';
196            if($params['fh']) $style .= 'fill:' . $params['fh'] . ';';
197            $style .= '}';
198        }
199
200        return $style;
201    }
202
203    /**
204     * Takes a hexadecimal color string in the following forms:
205     *
206     * RGB
207     * RRGGBB
208     * RRGGBBAA
209     *
210     * Converts it to rgba() form
211     *
212     * @param string $color
213     * @return string
214     */
215    protected function fixColor($color) {
216        if(preg_match('/^([0-9a-f])([0-9a-f])([0-9a-f])$/i', $color, $m)) {
217            $r = hexdec($m[1] . $m[1]);
218            $g = hexdec($m[2] . $m[2]);
219            $b = hexdec($m[3] . $m[3]);
220            $a = hexdec('ff');
221        } elseif(preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i', $color, $m)) {
222            $r = hexdec($m[1]);
223            $g = hexdec($m[2]);
224            $b = hexdec($m[3]);
225            if(isset($m[4])) {
226                $a = hexdec($m[4]);
227            } else {
228                $a = hexdec('ff');
229            }
230        } else {
231            return '';
232        }
233
234        return "rgba($r,$g,$b,$a)";
235    }
236
237    /**
238     * sets a rectangular background of the size of the svg/this itself
239     *
240     * @param SvgNode $g
241     * @return SvgNode
242     */
243    protected function createBackground(SvgNode $g) {
244        $rect = $g->prependChild('rect');
245        $rect->addAttribute('class', self::BACKGROUNDCLASS);
246
247        $rect->addAttribute('x', '0');
248        $rect->addAttribute('y', '0');
249        $rect->addAttribute('height', '100%');
250        $rect->addAttribute('width', '100%');
251        return $rect;
252    }
253
254    /**
255     * Abort processing with given status code
256     *
257     * @param int $status
258     */
259    protected function abort($status) {
260        http_status($status);
261        exit;
262    }
263
264}
265
266// main
267$svg = new SVG();
268$svg->out();
269
270