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