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