You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

347 lines
9.9 KiB

  1. <?php
  2. namespace dokuwiki\template\sprintdoc;
  3. if(!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__) . '/../../../');
  4. require_once(DOKU_INC . 'inc/init.php');
  5. /**
  6. * Custom XML node that allows prepending
  7. */
  8. class SvgNode extends \SimpleXMLElement {
  9. /**
  10. * @param string $name Name of the new node
  11. * @param null|string $value
  12. * @return SvgNode
  13. */
  14. public function prependChild($name, $value = null) {
  15. $dom = dom_import_simplexml($this);
  16. $new = $dom->insertBefore(
  17. $dom->ownerDocument->createElement($name, $value),
  18. $dom->firstChild
  19. );
  20. return simplexml_import_dom($new, get_class($this));
  21. }
  22. /**
  23. * @param \SimpleXMLElement $node the node to be added
  24. * @return \SimpleXMLElement
  25. */
  26. public function appendNode(\SimpleXMLElement $node) {
  27. $dom = dom_import_simplexml($this);
  28. $domNode = dom_import_simplexml($node);
  29. $newNode = $dom->appendChild($domNode);
  30. return simplexml_import_dom($newNode, get_class($this));
  31. }
  32. /**
  33. * @param \SimpleXMLElement $node the child to remove
  34. * @return \SimpleXMLElement
  35. */
  36. public function removeChild(\SimpleXMLElement $node) {
  37. $dom = dom_import_simplexml($node);
  38. $dom->parentNode->removeChild($dom);
  39. return $node;
  40. }
  41. /**
  42. * Wraps all elements of $this in a `<g>` tag
  43. *
  44. * @return SvgNode
  45. */
  46. public function groupChildren() {
  47. $dom = dom_import_simplexml($this);
  48. $g = $dom->ownerDocument->createElement('g');
  49. while($dom->childNodes->length > 0) {
  50. $child = $dom->childNodes->item(0);
  51. $dom->removeChild($child);
  52. $g->appendChild($child);
  53. }
  54. $g = $dom->appendChild($g);
  55. return simplexml_import_dom($g, get_class($this));
  56. }
  57. /**
  58. * Add new style definitions to this element
  59. * @param string $style
  60. */
  61. public function addStyle($style) {
  62. $defs = $this->defs;
  63. if(!$defs) {
  64. $defs = $this->prependChild('defs');
  65. }
  66. $defs->addChild('style', $style);
  67. }
  68. }
  69. /**
  70. * Manage SVG recoloring
  71. */
  72. class SVG {
  73. const IMGDIR = __DIR__ . '/img/';
  74. const BACKGROUNDCLASS = 'sprintdoc-background';
  75. const CDNBASE = 'https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/';
  76. protected $file;
  77. protected $replacements;
  78. /**
  79. * SVG constructor
  80. */
  81. public function __construct() {
  82. global $INPUT;
  83. $svg = cleanID($INPUT->str('svg'));
  84. if(blank($svg)) $this->abort(404);
  85. // try local file first
  86. $file = self::IMGDIR . $svg;
  87. if(!file_exists($file)) {
  88. // try media file
  89. $file = mediaFN($svg);
  90. if(file_exists($file)) {
  91. // media files are ACL protected
  92. if(auth_quickaclcheck($svg) < AUTH_READ) $this->abort(403);
  93. } else {
  94. // get it from material design icons
  95. $file = getCacheName($svg, '.svg');
  96. if (!file_exists($file)) {
  97. io_download(self::CDNBASE . $svg, $file);
  98. }
  99. }
  100. }
  101. // check if media exists
  102. if(!file_exists($file)) $this->abort(404);
  103. $this->file = $file;
  104. }
  105. /**
  106. * Generate and output
  107. */
  108. public function out() {
  109. global $conf;
  110. $file = $this->file;
  111. $params = $this->getParameters();
  112. header('Content-Type: image/svg+xml');
  113. $cachekey = md5($file . serialize($params) . $conf['template'] . filemtime(__FILE__));
  114. $cache = new \dokuwiki\Cache\Cache($cachekey, '.svg');
  115. $cache->setEvent('SVG_CACHE');
  116. http_cached($cache->cache, $cache->useCache(array('files' => array($file, __FILE__))));
  117. if($params['e']) {
  118. $content = $this->embedSVG($file);
  119. } else {
  120. $content = $this->generateSVG($file, $params);
  121. }
  122. http_cached_finish($cache->cache, $content);
  123. }
  124. /**
  125. * Generate a new SVG based on the input file and the parameters
  126. *
  127. * @param string $file the SVG file to load
  128. * @param array $params the parameters as returned by getParameters()
  129. * @return string the new XML contents
  130. */
  131. protected function generateSVG($file, $params) {
  132. /** @var SvgNode $xml */
  133. $xml = simplexml_load_file($file, SvgNode::class);
  134. $xml->addStyle($this->makeStyle($params));
  135. $this->createBackground($xml);
  136. $xml->groupChildren();
  137. return $xml->asXML();
  138. }
  139. /**
  140. * Return the absolute minimum path definition for direct embedding
  141. *
  142. * No styles will be applied. They have to be done in CSS
  143. *
  144. * @param string $file the SVG file to load
  145. * @return string the new XML contents
  146. */
  147. protected function embedSVG($file) {
  148. /** @var SvgNode $xml */
  149. $xml = simplexml_load_file($file, SvgNode::class);
  150. $def = hsc((string) $xml->path['d']);
  151. $w = hsc($xml['width'] ?? '100%');
  152. $h = hsc($xml['height'] ?? '100%');
  153. $v = hsc($xml['viewBox']);
  154. // if viewbox is not defined, construct it from width and height, if available
  155. if (empty($v) && !empty($w) && !empty($h)) {
  156. $v = hsc("0 0 $w $h");
  157. }
  158. return "<svg viewBox=\"$v\"><path d=\"$def\" /></svg>";
  159. }
  160. /**
  161. * Get the supported parameters from request
  162. *
  163. * @return array
  164. */
  165. protected function getParameters() {
  166. global $INPUT;
  167. $params = array(
  168. 'e' => $INPUT->bool('e', false),
  169. 's' => $this->fixColor($INPUT->str('s')),
  170. 'f' => $this->fixColor($INPUT->str('f')),
  171. 'b' => $this->fixColor($INPUT->str('b')),
  172. 'sh' => $this->fixColor($INPUT->str('sh')),
  173. 'fh' => $this->fixColor($INPUT->str('fh')),
  174. 'bh' => $this->fixColor($INPUT->str('bh')),
  175. );
  176. return $params;
  177. }
  178. /**
  179. * Generate a style setting from the input variables
  180. *
  181. * @param array $params associative array with the given parameters
  182. * @return string
  183. */
  184. protected function makeStyle($params) {
  185. $element = 'path'; // FIXME configurable?
  186. if(empty($params['b'])) {
  187. $params['b'] = $this->fixColor('00000000');
  188. }
  189. $style = 'g rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['b'] . ';}';
  190. if($params['bh']) {
  191. $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['bh'] . ';}';
  192. }
  193. if($params['s'] || $params['f']) {
  194. $style .= 'g ' . $element . '{';
  195. if($params['s']) $style .= 'stroke:' . $params['s'] . ';';
  196. if($params['f']) $style .= 'fill:' . $params['f'] . ';';
  197. $style .= '}';
  198. }
  199. if($params['sh'] || $params['fh']) {
  200. $style .= 'g:hover ' . $element . '{';
  201. if($params['sh']) $style .= 'stroke:' . $params['sh'] . ';';
  202. if($params['fh']) $style .= 'fill:' . $params['fh'] . ';';
  203. $style .= '}';
  204. }
  205. return $style;
  206. }
  207. /**
  208. * Takes a hexadecimal color string in the following forms:
  209. *
  210. * RGB
  211. * RRGGBB
  212. * RRGGBBAA
  213. *
  214. * Converts it to rgba() form.
  215. *
  216. * Alternatively takes a replacement name from the current template's style.ini
  217. *
  218. * @param string $color
  219. * @return string
  220. */
  221. protected function fixColor($color) {
  222. if($color === '') return '';
  223. if(preg_match('/^([0-9a-f])([0-9a-f])([0-9a-f])$/i', $color, $m)) {
  224. $r = hexdec($m[1] . $m[1]);
  225. $g = hexdec($m[2] . $m[2]);
  226. $b = hexdec($m[3] . $m[3]);
  227. $a = hexdec('ff');
  228. } elseif(preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i', $color, $m)) {
  229. $r = hexdec($m[1]);
  230. $g = hexdec($m[2]);
  231. $b = hexdec($m[3]);
  232. if(isset($m[4])) {
  233. $a = hexdec($m[4]);
  234. } else {
  235. $a = hexdec('ff');
  236. }
  237. } else {
  238. if(is_null($this->replacements)) $this->initReplacements();
  239. if(isset($this->replacements[$color])) {
  240. return $this->replacements[$color];
  241. }
  242. if(isset($this->replacements['__' . $color . '__'])) {
  243. return $this->replacements['__' . $color . '__'];
  244. }
  245. return '';
  246. }
  247. return "rgba($r,$g,$b,$a)";
  248. }
  249. /**
  250. * sets a rectangular background of the size of the svg/this itself
  251. *
  252. * @param SvgNode $g
  253. * @return SvgNode
  254. */
  255. protected function createBackground(SvgNode $g) {
  256. $rect = $g->prependChild('rect');
  257. $rect->addAttribute('class', self::BACKGROUNDCLASS);
  258. $rect->addAttribute('x', '0');
  259. $rect->addAttribute('y', '0');
  260. $rect->addAttribute('height', '100%');
  261. $rect->addAttribute('width', '100%');
  262. return $rect;
  263. }
  264. /**
  265. * Abort processing with given status code
  266. *
  267. * @param int $status
  268. */
  269. protected function abort($status) {
  270. http_status($status);
  271. exit;
  272. }
  273. /**
  274. * Initialize the available replacement patterns
  275. *
  276. * Loads the style.ini from the template (and various local locations)
  277. * via a core function only available through some hack.
  278. */
  279. protected function initReplacements() {
  280. global $conf;
  281. if (!class_exists('\dokuwiki\StyleUtils')) {
  282. // Pre-Greebo Compatibility
  283. define('SIMPLE_TEST', 1); // hacky shit
  284. include DOKU_INC . 'lib/exe/css.php';
  285. $ini = css_styleini($conf['template']);
  286. $this->replacements = $ini['replacements'];
  287. return;
  288. }
  289. $stuleUtils = new \dokuwiki\StyleUtils();
  290. $ini = $stuleUtils->cssStyleini('sprintdoc');
  291. $this->replacements = $ini['replacements'];
  292. }
  293. }
  294. // main
  295. $svg = new SVG();
  296. $svg->out();