xref: /template/sprintdoc/svg.php (revision 5a65cb36d80b446f0ef323905030edd2b868f4ad)
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/**
52 * Manage SVG recoloring
53 */
54class SVG {
55
56    const IMGDIR = __DIR__ . '/img/';
57    const BACKGROUNDCLASS = 'sprintdoc-background';
58
59    /** @var SvgNode */
60    protected $xml;
61
62    /**
63     * SVG constructor
64     */
65    public function __construct() {
66        global $INPUT;
67
68        $svg = cleanID($INPUT->str('svg'));
69        if(blank($svg)) $this->abort(404);
70
71        // try local file first
72        $file = self::IMGDIR . $svg;
73        if(!file_exists($file)) {
74            // media files are ACL protected
75            if(auth_quickaclcheck($svg) < AUTH_READ) $this->abort(403);
76            $file = mediaFN($svg);
77        }
78        // check if media exists
79        if(!file_exists($file)) $this->abort(404);
80
81        $this->xml = simplexml_load_file($file, SvgNode::class);
82    }
83
84    /**
85     * Generate and output
86     */
87    public function out() {
88        $g = $this->wrapChildren();
89        $this->setBackground($g);
90        $this->setStyle();
91        header('Content-Type: image/svg+xml');
92        echo $this->xml->asXML();
93    }
94
95    /**
96     * Generate a style setting from the input variables
97     *
98     * @return string
99     */
100    protected function makeStyle() {
101        global $INPUT;
102
103        $element = 'path'; // FIXME configurable?
104
105        $colors = array(
106            's' => $this->fixColor($INPUT->str('s')),
107            'f' => $this->fixColor($INPUT->str('f')),
108            'b' => $this->fixColor($INPUT->str('b')),
109            'sh' => $this->fixColor($INPUT->str('sh')),
110            'fh' => $this->fixColor($INPUT->str('fh')),
111            'bh' => $this->fixColor($INPUT->str('bh')),
112        );
113
114        if (empty($colors['b'])) {
115            $colors['b'] = $this->fixColor('00000000');
116        }
117
118        $style = 'g rect.' . self::BACKGROUNDCLASS . '{fill:' . $colors['b'] . ';}';
119
120        if($colors['bh']) {
121            $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $colors['bh'] . ';}';
122        }
123
124        if($colors['s'] || $colors['f']) {
125            $style .= 'g ' . $element . '{';
126            if($colors['s']) $style .= 'stroke:' . $colors['s'] . ';';
127            if($colors['f']) $style .= 'fill:' . $colors['f'] . ';';
128            $style .= '}';
129        }
130
131        if($colors['sh'] || $colors['fh']) {
132            $style .= 'g:hover ' . $element . '{';
133            if($colors['sh']) $style .= 'stroke:' . $colors['sh'] . ';';
134            if($colors['fh']) $style .= 'fill:' . $colors['fh'] . ';';
135            $style .= '}';
136        }
137
138        return $style;
139    }
140
141    /**
142     * Takes a hexadecimal color string in the following forms:
143     *
144     * RGB
145     * RRGGBB
146     * RRGGBBAA
147     *
148     * Converts it to rgba() form
149     *
150     * @param string $color
151     * @return string
152     */
153    protected function fixColor($color) {
154        if(preg_match('/^([0-9a-f])([0-9a-f])([0-9a-f])$/i', $color, $m)) {
155            $r = hexdec($m[1] . $m[1]);
156            $g = hexdec($m[2] . $m[2]);
157            $b = hexdec($m[3] . $m[3]);
158            $a = hexdec('ff');
159        } elseif(preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i', $color, $m)) {
160            $r = hexdec($m[1]);
161            $g = hexdec($m[2]);
162            $b = hexdec($m[3]);
163            if(isset($m[4])) {
164                $a = hexdec($m[4]);
165            } else {
166                $a = hexdec('ff');
167            }
168        } else {
169            return '';
170        }
171
172        return "rgba($r,$g,$b,$a)";
173    }
174
175    /**
176     * sets a rectangular background of the size of the svg/this itself
177     *
178     * @param SvgNode $g
179     * @return SvgNode
180     */
181    protected function setBackground(SvgNode $g) {
182        $attributes = $this->xml->attributes();
183        $rect = $g->prependChild('rect');
184        $rect->addAttribute('class', self::BACKGROUNDCLASS);
185
186        $rect->addAttribute('x', '0');
187        $rect->addAttribute('y', '0');
188        $rect->addAttribute('height', $attributes['height']);
189        $rect->addAttribute('width', $attributes['width']);
190        return $rect;
191    }
192
193    /**
194     * Wraps all elements of $this in a `<g>` tag
195     *
196     * @return SvgNode
197     */
198    protected function wrapChildren() {
199        $svgChildren = array();
200        foreach ($this->xml->children() as $child) {
201            $svgChildren[] = $this->xml->removeChild($child);
202        }
203        $g = $this->xml->prependChild('g');
204        foreach ($svgChildren as $child) {
205            $g->appendNode($child);
206        }
207        return $g;
208    }
209
210    /**
211     * Apply the style to the SVG
212     */
213    protected function setStyle() {
214        $defs = $this->xml->defs;
215        if(!$defs) {
216            $defs = $this->xml->prependChild('defs');
217        }
218        $defs->addChild('style', $this->makeStyle());
219    }
220
221    /**
222     * Abort processing with given status code
223     *
224     * @param int $status
225     */
226    protected function abort($status) {
227        http_status($status);
228        exit;
229    }
230
231}
232
233// main
234$svg = new SVG();
235$svg->out();
236
237