insertBefore( $dom->ownerDocument->createElement($name, $value), $dom->firstChild ); return simplexml_import_dom($new, get_class($this)); } /** * @param \SimpleXMLElement $node the node to be added * @return \SimpleXMLElement */ public function appendNode(\SimpleXMLElement $node) { $dom = dom_import_simplexml($this); $domNode = dom_import_simplexml($node); $newNode = $dom->appendChild($domNode); return simplexml_import_dom($newNode, get_class($this)); } /** * @param \SimpleXMLElement $node the child to remove * @return \SimpleXMLElement */ public function removeChild(\SimpleXMLElement $node) { $dom = dom_import_simplexml($node); $dom->parentNode->removeChild($dom); return $node; } /** * Wraps all elements of $this in a `` tag * * @return SvgNode */ public function groupChildren() { $dom = dom_import_simplexml($this); $g = $dom->ownerDocument->createElement('g'); while($dom->childNodes->length > 0) { $child = $dom->childNodes->item(0); $dom->removeChild($child); $g->appendChild($child); } $g = $dom->appendChild($g); return simplexml_import_dom($g, get_class($this)); } /** * Add new style definitions to this element * @param string $style */ public function addStyle($style) { $defs = $this->defs; if(!$defs) { $defs = $this->prependChild('defs'); } $defs->addChild('style', $style); } } /** * Manage SVG recoloring */ class SVG { const IMGDIR = __DIR__ . '/img/'; const BACKGROUNDCLASS = 'sprintdoc-background'; const CDNBASE = 'https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/'; protected $file; protected $replacements; /** * SVG constructor */ public function __construct() { global $INPUT; $svg = cleanID($INPUT->str('svg')); if(blank($svg)) $this->abort(404); // try local file first $file = self::IMGDIR . $svg; if(!file_exists($file)) { // try media file $file = mediaFN($svg); if(file_exists($file)) { // media files are ACL protected if(auth_quickaclcheck($svg) < AUTH_READ) $this->abort(403); } else { // get it from material design icons $file = getCacheName($svg, '.svg'); if (!file_exists($file)) { io_download(self::CDNBASE . $svg, $file); } } } // check if media exists if(!file_exists($file)) $this->abort(404); $this->file = $file; } /** * Generate and output */ public function out() { global $conf; $file = $this->file; $params = $this->getParameters(); header('Content-Type: image/svg+xml'); $cachekey = md5($file . serialize($params) . $conf['template'] . filemtime(__FILE__)); $cache = new \dokuwiki\Cache\Cache($cachekey, '.svg'); $cache->setEvent('SVG_CACHE'); http_cached($cache->cache, $cache->useCache(array('files' => array($file, __FILE__)))); if($params['e']) { $content = $this->embedSVG($file); } else { $content = $this->generateSVG($file, $params); } http_cached_finish($cache->cache, $content); } /** * Generate a new SVG based on the input file and the parameters * * @param string $file the SVG file to load * @param array $params the parameters as returned by getParameters() * @return string the new XML contents */ protected function generateSVG($file, $params) { /** @var SvgNode $xml */ $xml = simplexml_load_file($file, SvgNode::class); $xml->addStyle($this->makeStyle($params)); $this->createBackground($xml); $xml->groupChildren(); return $xml->asXML(); } /** * Return the absolute minimum path definition for direct embedding * * No styles will be applied. They have to be done in CSS * * @param string $file the SVG file to load * @return string the new XML contents */ protected function embedSVG($file) { /** @var SvgNode $xml */ $xml = simplexml_load_file($file, SvgNode::class); $def = hsc((string) $xml->path['d']); $w = hsc($xml['width'] ?? '100%'); $h = hsc($xml['height'] ?? '100%'); $v = hsc($xml['viewBox']); // if viewbox is not defined, construct it from width and height, if available if (empty($v) && !empty($w) && !empty($h)) { $v = hsc("0 0 $w $h"); } return ""; } /** * Get the supported parameters from request * * @return array */ protected function getParameters() { global $INPUT; $params = array( 'e' => $INPUT->bool('e', false), 's' => $this->fixColor($INPUT->str('s')), 'f' => $this->fixColor($INPUT->str('f')), 'b' => $this->fixColor($INPUT->str('b')), 'sh' => $this->fixColor($INPUT->str('sh')), 'fh' => $this->fixColor($INPUT->str('fh')), 'bh' => $this->fixColor($INPUT->str('bh')), ); return $params; } /** * Generate a style setting from the input variables * * @param array $params associative array with the given parameters * @return string */ protected function makeStyle($params) { $element = 'path'; // FIXME configurable? if(empty($params['b'])) { $params['b'] = $this->fixColor('00000000'); } $style = 'g rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['b'] . ';}'; if($params['bh']) { $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['bh'] . ';}'; } if($params['s'] || $params['f']) { $style .= 'g ' . $element . '{'; if($params['s']) $style .= 'stroke:' . $params['s'] . ';'; if($params['f']) $style .= 'fill:' . $params['f'] . ';'; $style .= '}'; } if($params['sh'] || $params['fh']) { $style .= 'g:hover ' . $element . '{'; if($params['sh']) $style .= 'stroke:' . $params['sh'] . ';'; if($params['fh']) $style .= 'fill:' . $params['fh'] . ';'; $style .= '}'; } return $style; } /** * Takes a hexadecimal color string in the following forms: * * RGB * RRGGBB * RRGGBBAA * * Converts it to rgba() form. * * Alternatively takes a replacement name from the current template's style.ini * * @param string $color * @return string */ protected function fixColor($color) { if($color === '') return ''; if(preg_match('/^([0-9a-f])([0-9a-f])([0-9a-f])$/i', $color, $m)) { $r = hexdec($m[1] . $m[1]); $g = hexdec($m[2] . $m[2]); $b = hexdec($m[3] . $m[3]); $a = hexdec('ff'); } elseif(preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i', $color, $m)) { $r = hexdec($m[1]); $g = hexdec($m[2]); $b = hexdec($m[3]); if(isset($m[4])) { $a = hexdec($m[4]); } else { $a = hexdec('ff'); } } else { if(is_null($this->replacements)) $this->initReplacements(); if(isset($this->replacements[$color])) { return $this->replacements[$color]; } if(isset($this->replacements['__' . $color . '__'])) { return $this->replacements['__' . $color . '__']; } return ''; } return "rgba($r,$g,$b,$a)"; } /** * sets a rectangular background of the size of the svg/this itself * * @param SvgNode $g * @return SvgNode */ protected function createBackground(SvgNode $g) { $rect = $g->prependChild('rect'); $rect->addAttribute('class', self::BACKGROUNDCLASS); $rect->addAttribute('x', '0'); $rect->addAttribute('y', '0'); $rect->addAttribute('height', '100%'); $rect->addAttribute('width', '100%'); return $rect; } /** * Abort processing with given status code * * @param int $status */ protected function abort($status) { http_status($status); exit; } /** * Initialize the available replacement patterns * * Loads the style.ini from the template (and various local locations) * via a core function only available through some hack. */ protected function initReplacements() { global $conf; if (!class_exists('\dokuwiki\StyleUtils')) { // Pre-Greebo Compatibility define('SIMPLE_TEST', 1); // hacky shit include DOKU_INC . 'lib/exe/css.php'; $ini = css_styleini($conf['template']); $this->replacements = $ini['replacements']; return; } $stuleUtils = new \dokuwiki\StyleUtils(); $ini = $stuleUtils->cssStyleini('sprintdoc'); $this->replacements = $ini['replacements']; } } // main $svg = new SVG(); $svg->out();