dokuwiki-template-sprintdoc.../svg.php
Andreas Gohr 9fd3d99b8f directly embed SVGs for better styling
Now the SVGs are loaded via AJAX and are directly embedded into the
HTML. This allows for styling them via CSS and we can apply proper hover
styles.

The svg dispatcher was extended with an embed option (boolean parameter
'e') which will return an absolute minimum svg with absolutely no
styles.
2017-02-13 16:19:43 +01:00

328 lines
9.2 KiB
PHP

<?php
namespace dokuwiki\template\sprintdoc;
if(!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__) . '/../../../');
require_once(DOKU_INC . 'inc/init.php');
/**
* Custom XML node that allows prepending
*/
class SvgNode extends \SimpleXMLElement {
/**
* @param string $name Name of the new node
* @param null|string $value
* @return SvgNode
*/
public function prependChild($name, $value = null) {
$dom = dom_import_simplexml($this);
$new = $dom->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 `<g>` 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://cdn.rawgit.com/Templarian/MaterialDesign/master/icons/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');
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() {
$file = $this->file;
$params = $this->getParameters();
header('Content-Type: image/svg+xml');
$cachekey = md5($file . serialize($params) . filemtime(__FILE__));
$cache = new \cache($cachekey, '.svg');
$cache->_event = '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']);
$h = hsc($xml['height']);
$v = hsc($xml['viewBox']);
return "<svg width=\"$w\" height=\"$h\" viewBox=\"$v\"><path d=\"$def\" /></svg>";
}
/**
* 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, $ini = true) {
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($ini) {
if(!$this->replacements) $this->initReplacements();
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;
define('SIMPLE_TEST', 1); // hacky shit
include DOKU_INC . 'lib/exe/css.php';
$ini = css_styleini($conf['tpl']);
$this->replacements = $ini['replacements'];
}
}
// main
$svg = new SVG();
$svg->out();