diff --git a/svg.php b/svg.php new file mode 100644 index 0000000..f1bc30f --- /dev/null +++ b/svg.php @@ -0,0 +1,269 @@ +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'; + + protected $file; + + /** + * 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) < AUTH_READ) $this->abort(403); + $file = mediaFN($svg); + } + // check if media exists + if(!file_exists($file)) $this->abort(404); + + $this->file = $file; + } + + /** + * Generate and output + */ + public function out() { + $file = $this->file; + $params = $this->getParameters(); + + header('Content-Type: image/svg+xml'); + $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 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(); + } + + /** + * Get the supported parameters from request + * + * @return array + */ + protected function getParameters() { + global $INPUT; + + $params = 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')), + ); + + 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 + * + * @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)"; + } + + /** + * 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; + } + +} + +// main +$svg = new SVG(); +$svg->out(); +