1<?php
2/**
3 * DokuWiki Plugin svgEmbed (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Michael Bowers <restlessmind@gmail.com>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) {
11    die();
12}
13
14
15class syntax_plugin_svgembed extends DokuWiki_Syntax_Plugin
16{
17
18    /**
19     * Get the new parameters added to the syntax.
20     *
21     * @param string $match The text that matched the lexer pattern that we are inspecting
22     */
23    private function Get_New_Parameters($match, &$p) {
24        // Strip the opening and closing markup
25        $link = preg_replace(array('/^\{\{/', '/\}\}$/u'), '', $match);
26
27        // Split title from URL
28        $link = explode('|', $link, 2);
29
30        //remove aligning spaces
31        $link[0] = trim($link[0]);
32
33        //split into src and parameters (using the very last questionmark)
34        $pos = strrpos($link[0], '?');
35
36        if ($pos !== false) {
37            $param = substr($link[0], $pos + 1);
38
39            // Get units
40            $p['inResponsiveUnits'] = (preg_match('/units:(\%|vw)/i', $param, $local_match) > 0);
41            $p['responsiveUnits'] = ($p['inResponsiveUnits'] && count($local_match) > 1) ? $local_match[1] : NULL;
42
43            // Get declared CSS classes
44            unset($local_match);
45            $p['hasCssClasses'] = (preg_match_all('/class:(-?[_a-z]+[_a-z0-9-]*)/i', $param, $local_match) > 0);
46            $p['cssClasses'] = ($p['hasCssClasses'] && isset($local_match[1]) && count($local_match[1])) ? $local_match[1] : NULL;
47
48            // Get printing
49            if (preg_match_all('/(^|&)(print|print:(on|true|yes|1|off|false|no|0))(&|$)/i', $param, $local_match)) {
50                $p['print'] = in_array(strtolower($local_match[2][0]), array('print', 'print:on', 'print:true', 'print:yes', 'print:1'));
51            }
52            else {
53                $p['print'] = ($this->getConf('default_print') == '1');
54            }
55
56            // Re-parse width and height
57            $param = preg_replace('/class:(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)/i', '', $param);   // Remove the classes since they can have numbers embedded
58            if(preg_match('/(\d+)(x(\d+))?/i', $param, $size)) {
59                $p['width'] = (!empty($size[1]) ? $size[1] : null);
60                $p['height'] = (!empty($size[3]) ? $size[3] : null);
61            } else {
62                $p['width'] = null;
63                $p['height'] = null;
64            }
65        }
66    }
67
68
69    /**
70     * Figure out the pixel adjustment if an absolute measurement unit is given.
71     *
72     * @param string $value Dimension to analyze for unit value (cm|mm|Q|in|pc|pt|px)
73     */
74    private function Get_SVG_Unit_Adjustment($value) {
75        define('SVG_DPI', 96.0);
76
77        $matches = array();
78        $adjustment = 1;
79
80        if (preg_match('/(cm|mm|Q|in|pc|pt|px)/', $value, $matches) && count($matches)) {
81            switch ($matches[1]) {
82            // Don't bother checking for "px", we already set adjustment to 1, but we still
83            //   want to count it in the matches
84            case 'pt':
85                $adjustment = SVG_DPI / 72;
86                break;
87            case 'pc':
88                $adjustment = SVG_DPI / 6;
89                break;
90            case 'in':
91                $adjustment = SVG_DPI;
92                break;
93            case 'cm':
94                $adjustment = SVG_DPI / 2.54;
95                break;
96            case 'mm':
97                $adjustment = SVG_DPI / 25.4;
98                break;
99            case 'Q':
100                $adjustment = SVG_DPI / 101.6;
101                break;
102            }
103        }
104
105        return $adjustment;
106    }
107
108
109    /**
110     * @return string Syntax mode type
111     */
112    public function getType() {
113        return 'container';
114    }
115
116    /**
117     * @return string Paragraph type
118     */
119    public function getPType() {
120        return 'block';
121    }
122
123    /**
124     * @return int Sort order - Low numbers go before high numbers
125     */
126    public function getSort() {
127        // Run it before the standard media functionality
128        return 315;
129    }
130
131    /**
132     * Connect lookup pattern to lexer.
133     *
134     * @param string $mode Parser mode
135     */
136    public function connectTo($mode) {
137        // match everything the media component does, but short circuit into my code first
138        $this->Lexer->addSpecialPattern("\{\{(?:[^\}\>\<]|(?:\}[^\>\<\}]))+\}\}", $mode, 'plugin_svgembed');
139    }
140
141    /**
142     * Handle matches of the media syntax, overridden by this plugin
143     *
144     * @param string       $match   The match of the syntax
145     * @param int          $state   The state of the handler
146     * @param int          $pos     The position in the document
147     * @param Doku_Handler $handler The handler
148     *
149     * @return array Data for the renderer
150     */
151    public function handle($match, $state, $pos, Doku_Handler $handler) {
152        $p = Doku_Handler_Parse_Media($match);
153        $isSVG = preg_match('/\.svg$/i', trim($p['src']));
154        if ($isSVG)
155            $this->Get_New_Parameters($match, $p);
156
157        if (!$isSVG || $p['type'] != 'internalmedia') {
158            // If it's external media or not an SVG, perform the regular processing...
159            $handler->media($match, $state, $pos);
160            return false;
161        }
162        else {
163            // ...otherwise, feed into my renderer
164            return ($p);
165        }
166    }
167
168    /**
169     * Render xhtml output or metadata
170     *
171     * @param string        $mode     Renderer mode (supported modes: xhtml, metadata)
172     * @param Doku_Renderer $renderer The renderer
173     * @param array         $data     The data from the handler() function
174     *
175     * @return bool If rendering was successful.
176     */
177    public function render($mode, Doku_Renderer $renderer, $data) {
178
179        // If no data or we're not rendering XHTML, exit without handling
180        if (!$data)
181            return false;
182
183        if ($mode == 'xhtml') {
184            global $conf;
185
186            // Determine the maximum width allowed
187            if (isset($data['width'])) {
188                // Single width value specified?  Render with this width, but determine the file height and scale accordingly
189                $svg_max = $data['width'];
190            }
191            else {
192                // If a value is set, use that, else load the default value
193                $svg_max = isset($conf['plugin']['svgembed']['max_svg_width']) ?
194                             $conf['plugin']['svgembed']['max_svg_width'] :
195                             $this->getConf('max_svg_width');
196            }
197
198            // From here, it's basically a copy of the default renderer, but it inserts SVG with an embed tag rather than img tag.
199            $ret = '';
200            $hasdimensions = (isset($data['width']) && isset($data['height']));
201
202            // If both dimensions are not specified by the page then find them in the SVG file (if possible), and if not just pop out a default
203            if (!$hasdimensions) {
204                $svg_file = sprintf('%s%s', $conf['mediadir'], str_replace(':', '/', $data['src']));
205
206                if (file_exists($svg_file) && ($svg_fp = fopen($svg_file, 'r'))) {
207                    $svg_xml = simplexml_load_file($svg_file);
208
209                    // Find the amount to adjust the pixels for layout if a unit is involved; use the
210                    //   largest adjustment if they are mixed
211                    $svg_adjustment = max($this->Get_SVG_Unit_Adjustment($svg_xml->attributes()->width),
212                                          $this->Get_SVG_Unit_Adjustment($svg_xml->attributes()->height));
213
214                    $svg_width = round(floatval($svg_xml->attributes()->width) * $svg_adjustment);
215                    $svg_height = round(floatval($svg_xml->attributes()->height) * $svg_adjustment);
216
217                    if ($svg_width < 1 || $svg_height < 1) {
218                        if (isset($svg_xml->attributes()->viewBox)) {
219                            $svg_viewbox = preg_split('/[ ,]{1,}/', $svg_xml->attributes()->viewBox);
220                            $svg_width = round(floatval($svg_viewbox[2]));
221                            $svg_height = round(floatval($svg_viewbox[3]));
222                        }
223                    }
224
225                    if ($svg_width < 1 || $svg_height < 1) {
226                        $svg_width = isset($conf['plugin']['svgembed']['default_width']) ?
227                                       $conf['plugin']['svgembed']['default_width'] :
228                                       $this->getConf('default_width');
229                        $svg_height = isset($conf['plugin']['svgembed']['default_height']) ?
230                                        $conf['plugin']['svgembed']['default_height'] :
231                                        $this->getConf('default_height');
232                    }
233
234                    unset($svg_viewbox, $svg_xml);
235                    fclose($svg_fp);
236                }
237
238                // Make sure we're not exceeding the maximum width; if so, let's scale the SVG value to the maximum size
239                if ($svg_width > $svg_max) {
240                    $svg_height = round($svg_height * $svg_max / $svg_width);
241                    $svg_width = $svg_max;
242                }
243
244                $data['width'] = $svg_width;
245                $data['height'] = $svg_height;
246            }
247            else {
248                $svg_width = $data['width'];
249                $svg_height = $data['height'];
250            }
251
252            switch($data['align']) {
253                case 'center':
254                    $styleextra = "margin:auto";
255                    break;
256                case 'left':
257                case 'right':
258                    $styleextra = "float:" . urlencode($data['align']);
259                    break;
260                default:
261                    $styleextra = '';
262            }
263
264            $svgembed_md5 = sprintf('svgembed_%s', md5(ml($data['src'], $ml_array)));
265            $ret .= '<span style="display:block';
266
267            $spanunits = (isset($data['responsiveUnits'])) ? $data['responsiveUnits'] : 'px';
268
269            if (isset($data['width']))
270                $ret .= ";width:{$data['width']}{$spanunits}";
271
272            if (isset($data['height']))
273                $ret .= ";height:{$data['height']}{$spanunits}";
274
275            if (strlen($styleextra))
276                $ret .= ";{$styleextra}";
277
278            $ret .= '">';
279
280
281            $ml_array = array('cache' => $data['cache']);
282            if (!$data['inResponsiveUnits'])
283                $ml_array = array_merge($ml_array, array('w' => $data['width'], 'h' => $data['height']));
284
285            $properties = '"' . ml($data['src'], $ml_array) . '" class="media' . $data['align'] . '"';
286
287            if ($data['title']) {
288                $properties .= ' title="' . $data['title'] . '"';
289                $properties .= ' alt="' . $data['title'] . '"';
290            } else {
291                $properties .= ' alt=""';
292            }
293
294
295            if (!(is_null($data['width']) || is_null($data['height']))) {
296                $properties .= ' style="width:100%"';
297            }
298
299
300            // Add any extra specified classes to the objects
301            if ($data['hasCssClasses'] && count($data['cssClasses']) > 0) {
302                $additionalCssClasses = '';
303                foreach ($data['cssClasses'] as $newCssClass)
304                    $additionalCssClasses .= " " . $renderer->_xmlEntities($newCssClass);
305                $additionalCssClasses = trim($additionalCssClasses);
306
307                if (preg_match('/class="([^"]*)"/i', $properties, $pmatches)) {
308                    $properties = str_replace("class=\"{$pmatches[1]}\"", "class=\"{$pmatches[1]} {$additionalCssClasses}\"", $properties);
309                }
310
311                unset($additionalCssClasses, $newCssClass);
312            }
313
314            if (is_a($renderer, 'renderer_plugin_dw2pdf')) {
315                $ret .= "<img id=\"" . $svgembed_md5 . "\" src={$properties} />";
316            }
317            else {
318                $ret .= "<object id=\"" . $svgembed_md5 . "\" type=\"image/svg+xml\" data={$properties}><embed type=\"image/svg+xml\" src={$properties} /></object>";
319
320                unset($properties);
321
322                if ($data['print']) {
323                    $ret .= '<div class="svgprintbutton_table"><button type="submit" title="Print SVG" onClick="svgembed_printContent(\'' .
324                            urlencode(ml($data['src'], $ml_array)) . '\'); return false" onMouseOver="svgembed_onMouseOver(\'' .
325                            $svgembed_md5 . '\'); return false" ' . 'onMouseOut="svgembed_onMouseOut(\'' . $svgembed_md5 . '\'); return false"' .
326                            '>Print SVG</button></div>';
327                }
328            }
329
330            $ret .= '</span>';
331
332            $ret .= '<br />';
333
334            $renderer->doc .= $ret;
335        }
336
337        if ($mode == 'metadata') {
338            // Add metadata so the SVG is associated to the page
339            if ($data['type'] == 'internalmedia') {
340                global $ID;
341
342                $src = $data['src'];
343                list($src) = explode('#', $src, 2);
344
345                if (media_isexternal($src))
346                    return;
347
348                resolve_mediaid(getNS($ID), $src, $exists);
349                $renderer->meta['relation']['media'][$src] = $exists;
350            }
351        }
352
353        return true;
354    }
355}
356
357