From 1072ee525dad533b3be798f70f2f831f47df46e1 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Tue, 10 Jan 2017 17:31:34 +0100 Subject: [PATCH 1/5] first go at a dispatcher to dynamically recolor SVGs The script accepts the following parameters: svg - the SVG to load. Either an image in the img directory next to the script or a media file id. ACLs are checked f - wanted fill color s - wanted stroke color fh - wanted fill color on hover sh - wanted stroke color on hover Colors are to be given in hex in the following formats: RGB RRGGBB RRGGBBAA What's missing: * being able to define what is styled, currently hardcoded to 'path' elements only * caching - no need to do all the processing every time * background setting - that would require wrapping an additional or element around all content and style. I'm not sure how to do that best. * unit tests --- svg.php | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 svg.php diff --git a/svg.php b/svg.php new file mode 100644 index 0000000..10da30f --- /dev/null +++ b/svg.php @@ -0,0 +1,165 @@ +insertBefore( + $dom->ownerDocument->createElement($name, $value), + $dom->firstChild + ); + + return simplexml_import_dom($new, get_class($this)); + } +} + +/** + * Manage SVG recoloring + */ +class SVG { + + const IMGDIR = __DIR__ . '/img/'; + + /** @var SvgNode */ + protected $xml; + + /** + * 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)) { + // media files are ACL protected + if(auth_quickaclcheck($svg)) $this->abort(403); + $file = mediaFN($svg); + } + // check if media exists + if(!file_exists($file)) $this->abort(404); + + $this->xml = simplexml_load_file($file, SvgNode::class); + } + + /** + * Generate and output + */ + public function out() { + $this->setStyle(); + header('image/svg+xml'); + echo $this->xml->asXML(); + } + + /** + * Generate a style setting from the input variables + * + * @return string + */ + protected function makeStyle() { + global $INPUT; + + $element = 'path'; // FIXME configurable? + + $colors = array( + 's' => $this->fixColor($INPUT->str('s')), + 'f' => $this->fixColor($INPUT->str('f')), + 'sh' => $this->fixColor($INPUT->str('sh')), + 'fh' => $this->fixColor($INPUT->str('fh')), + ); + + $style = ''; + if($colors['s'] || $colors['f']) { + $style .= $element . '{'; + if($colors['s']) $style .= 'stroke:' . $colors['s'] . ';'; + if($colors['f']) $style .= 'fill:' . $colors['f'] . ';'; + $style .= '}'; + } + + if($colors['sh'] || $colors['fh']) { + $style .= $element . ':hover{'; + if($colors['sh']) $style .= 'stroke:' . $colors['sh'] . ';'; + if($colors['fh']) $style .= 'fill:' . $colors['fh'] . ';'; + $style .= '}'; + } + + return $style; + } + + /** + * Takes a hexadecimal color string in the following forms: + * + * RGB + * RRGGBB + * RRGGBBAA + * + * Converts it to rgba() form + * + * @param string $color + * @return string + */ + protected function fixColor($color) { + 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 { + return ''; + } + + return "rgba($r,$g,$b,$a)"; + } + + /** + * Apply the style to the SVG + */ + protected function setStyle() { + $defs = $this->xml->defs; + if(!$defs) { + $defs = $this->xml->prependChild('defs'); + } + $defs->addChild('style', $this->makeStyle()); + } + + /** + * Abort processing with given status code + * + * @param int $status + */ + protected function abort($status) { + http_status($status); + exit; + } + +} + +// main +$svg = new SVG(); +$svg->out(); + From 80d784e1de81a9bd4f2967a7d4fc39110dcb54d3 Mon Sep 17 00:00:00 2001 From: Michael Grosse Date: Wed, 11 Jan 2017 14:42:51 +0100 Subject: [PATCH 2/5] feat: add background-colors to SVG-dispatcher This extends the query with to parameters for the background: b - wanted background color bh - wanted background color on hover --- svg.php | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/svg.php b/svg.php index 10da30f..9e2d12c 100644 --- a/svg.php +++ b/svg.php @@ -24,6 +24,28 @@ class SvgNode extends \SimpleXMLElement { 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; + } } /** @@ -32,6 +54,7 @@ class SvgNode extends \SimpleXMLElement { class SVG { const IMGDIR = __DIR__ . '/img/'; + const BACKGROUNDCLASS = 'sprintdoc-background'; /** @var SvgNode */ protected $xml; @@ -62,6 +85,8 @@ class SVG { * Generate and output */ public function out() { + $g = $this->wrapChildren(); + $this->setBackground($g); $this->setStyle(); header('image/svg+xml'); echo $this->xml->asXML(); @@ -80,20 +105,31 @@ class SVG { $colors = array( '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')), ); - $style = ''; + if (empty($colors['b'])) { + $colors['b'] = $this->fixColor('00000000'); + } + + $style = 'g rect.' . self::BACKGROUNDCLASS . '{fill:' . $colors['b'] . ';}'; + + if($colors['bh']) { + $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $colors['bh'] . ';}'; + } + if($colors['s'] || $colors['f']) { - $style .= $element . '{'; + $style .= 'g ' . $element . '{'; if($colors['s']) $style .= 'stroke:' . $colors['s'] . ';'; if($colors['f']) $style .= 'fill:' . $colors['f'] . ';'; $style .= '}'; } if($colors['sh'] || $colors['fh']) { - $style .= $element . ':hover{'; + $style .= 'g:hover ' . $element . '{'; if($colors['sh']) $style .= 'stroke:' . $colors['sh'] . ';'; if($colors['fh']) $style .= 'fill:' . $colors['fh'] . ';'; $style .= '}'; @@ -136,6 +172,41 @@ class SVG { 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 setBackground(SvgNode $g) { + $attributes = $this->xml->attributes(); + $rect = $g->prependChild('rect'); + $rect->addAttribute('class', self::BACKGROUNDCLASS); + + $rect->addAttribute('x', '0'); + $rect->addAttribute('y', '0'); + $rect->addAttribute('height', $attributes['height']); + $rect->addAttribute('width', $attributes['width']); + return $rect; + } + + /** + * Wraps all elements of $this in a `` tag + * + * @return SvgNode + */ + protected function wrapChildren() { + $svgChildren = array(); + foreach ($this->xml->children() as $child) { + $svgChildren[] = $this->xml->removeChild($child); + } + $g = $this->xml->prependChild('g'); + foreach ($svgChildren as $child) { + $g->appendNode($child); + } + return $g; + } + /** * Apply the style to the SVG */ From 3ec07d58b7e4f7d33cfe896e8d5911829f58bcbe Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 12 Jan 2017 09:25:13 +0100 Subject: [PATCH 3/5] fixed auth check --- svg.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg.php b/svg.php index 10da30f..684b811 100644 --- a/svg.php +++ b/svg.php @@ -49,7 +49,7 @@ class SVG { $file = self::IMGDIR . $svg; if(!file_exists($file)) { // media files are ACL protected - if(auth_quickaclcheck($svg)) $this->abort(403); + if(auth_quickaclcheck($svg) < AUTH_READ) $this->abort(403); $file = mediaFN($svg); } // check if media exists From 4fd6492bc9b3ec7a7e4f4e8bb15ac2ba50328b48 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 12 Jan 2017 09:25:28 +0100 Subject: [PATCH 4/5] fixed content type header --- svg.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg.php b/svg.php index 684b811..f81b3c2 100644 --- a/svg.php +++ b/svg.php @@ -63,7 +63,7 @@ class SVG { */ public function out() { $this->setStyle(); - header('image/svg+xml'); + header('Content-Type: image/svg+xml'); echo $this->xml->asXML(); } From 24ab1f725c47400c809211b7ebf1197468d8252d Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 19 Jan 2017 18:30:37 +0100 Subject: [PATCH 5/5] add caching and fix wrapping --- svg.php | 145 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 89 insertions(+), 56 deletions(-) diff --git a/svg.php b/svg.php index f79382e..f1bc30f 100644 --- a/svg.php +++ b/svg.php @@ -46,6 +46,37 @@ class SvgNode extends \SimpleXMLElement { $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); + } } /** @@ -56,8 +87,7 @@ class SVG { const IMGDIR = __DIR__ . '/img/'; const BACKGROUNDCLASS = 'sprintdoc-background'; - /** @var SvgNode */ - protected $xml; + protected $file; /** * SVG constructor @@ -78,31 +108,51 @@ class SVG { // check if media exists if(!file_exists($file)) $this->abort(404); - $this->xml = simplexml_load_file($file, SvgNode::class); + $this->file = $file; } /** * Generate and output */ public function out() { - $g = $this->wrapChildren(); - $this->setBackground($g); - $this->setStyle(); + $file = $this->file; + $params = $this->getParameters(); + header('Content-Type: image/svg+xml'); - echo $this->xml->asXML(); + $cachekey = md5($file . serialize($params)); + $cache = new \cache($cachekey, '.svg'); + $cache->_event = 'SVG_CACHE'; + + http_cached($cache->cache, $cache->useCache(array('files' => array($file, __FILE__)))); + http_cached_finish($cache->cache, $this->generateSVG($file, $params)); } /** - * Generate a style setting from the input variables + * Generate a new SVG based on the input file and the parameters * - * @return string + * @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 makeStyle() { + 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(); + } + + /** + * Get the supported parameters from request + * + * @return array + */ + protected function getParameters() { global $INPUT; - $element = 'path'; // FIXME configurable? - - $colors = array( + $params = array( 's' => $this->fixColor($INPUT->str('s')), 'f' => $this->fixColor($INPUT->str('f')), 'b' => $this->fixColor($INPUT->str('b')), @@ -111,27 +161,39 @@ class SVG { 'bh' => $this->fixColor($INPUT->str('bh')), ); - if (empty($colors['b'])) { - $colors['b'] = $this->fixColor('00000000'); + 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:' . $colors['b'] . ';}'; + $style = 'g rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['b'] . ';}'; - if($colors['bh']) { - $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $colors['bh'] . ';}'; + if($params['bh']) { + $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['bh'] . ';}'; } - if($colors['s'] || $colors['f']) { + if($params['s'] || $params['f']) { $style .= 'g ' . $element . '{'; - if($colors['s']) $style .= 'stroke:' . $colors['s'] . ';'; - if($colors['f']) $style .= 'fill:' . $colors['f'] . ';'; + if($params['s']) $style .= 'stroke:' . $params['s'] . ';'; + if($params['f']) $style .= 'fill:' . $params['f'] . ';'; $style .= '}'; } - if($colors['sh'] || $colors['fh']) { + if($params['sh'] || $params['fh']) { $style .= 'g:hover ' . $element . '{'; - if($colors['sh']) $style .= 'stroke:' . $colors['sh'] . ';'; - if($colors['fh']) $style .= 'fill:' . $colors['fh'] . ';'; + if($params['sh']) $style .= 'stroke:' . $params['sh'] . ';'; + if($params['fh']) $style .= 'fill:' . $params['fh'] . ';'; $style .= '}'; } @@ -178,46 +240,17 @@ class SVG { * @param SvgNode $g * @return SvgNode */ - protected function setBackground(SvgNode $g) { - $attributes = $this->xml->attributes(); + 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', $attributes['height']); - $rect->addAttribute('width', $attributes['width']); + $rect->addAttribute('height', '100%'); + $rect->addAttribute('width', '100%'); return $rect; } - /** - * Wraps all elements of $this in a `` tag - * - * @return SvgNode - */ - protected function wrapChildren() { - $svgChildren = array(); - foreach ($this->xml->children() as $child) { - $svgChildren[] = $this->xml->removeChild($child); - } - $g = $this->xml->prependChild('g'); - foreach ($svgChildren as $child) { - $g->appendNode($child); - } - return $g; - } - - /** - * Apply the style to the SVG - */ - protected function setStyle() { - $defs = $this->xml->defs; - if(!$defs) { - $defs = $this->xml->prependChild('defs'); - } - $defs->addChild('style', $this->makeStyle()); - } - /** * Abort processing with given status code *