1<?php 2/** 3 * Ditaa-Plugin: Converts Ascii-Flowcharts into a png-File 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Dennis Ploeger <develop [at] dieploegers [dot] de> 7 * @author Christoph Mertins <c [dot] mertins [at] gmail [dot] com> 8 * @author Gerry Weißbach / i-net software <tools [at] inetsoftware [dot] de> 9 * @author Christian Marg <marg@rz.tu-clausthal.de> 10 * @author Andreas Gohr <andi@splitbrain.org> 11 */ 12 13if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/'); 14if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 15require_once(DOKU_PLUGIN . 'syntax.php'); 16 17/** 18 * Class syntax_plugin_ditaa 19 */ 20class syntax_plugin_ditaa extends DokuWiki_Syntax_Plugin { 21 22 /** 23 * What about paragraphs? 24 */ 25 public function getPType() { 26 return 'normal'; 27 } 28 29 /** 30 * What kind of syntax are we? 31 */ 32 public function getType() { 33 return 'substition'; 34 } 35 36 /** 37 * Where to sort in? 38 */ 39 public function getSort() { 40 return 200; 41 } 42 43 /** 44 * Connect pattern to lexer 45 * 46 * @param string $mode 47 */ 48 public function connectTo($mode) { 49 $this->Lexer->addSpecialPattern('<ditaa.*?>\n.*?\n</ditaa>', $mode, 'plugin_ditaa'); 50 } 51 52 /** 53 * Stores all infor about the diagram in two files. One is the actual ditaa data, the other 54 * contains the options. 55 * 56 * @param string $match The text matched by the patterns 57 * @param int $state The lexer state for the match 58 * @param int $pos The character position of the matched text 59 * @param Doku_Handler $handler The Doku_Handler object 60 * @return bool|array Return an array with all data you want to use in render, false don't add an instruction 61 */ 62 public function handle($match, $state, $pos, Doku_Handler $handler) { 63 $info = $this->getInfo(); 64 65 // prepare default data 66 $return = array( 67 'width' => 0, 68 'height' => 0, 69 'antialias' => true, 70 'edgesep' => true, 71 'round' => false, 72 'shadow' => true, 73 'scale' => 1, 74 'align' => '', 75 'version' => $info['date'], //force rebuild of images on update 76 ); 77 78 // prepare input 79 $lines = explode("\n", $match); 80 $conf = array_shift($lines); 81 array_pop($lines); 82 83 // match config options 84 if(preg_match('/\b(left|center|right)\b/i', $conf, $match)) $return['align'] = $match[1]; 85 if(preg_match('/\b(\d+)x(\d+)\b/', $conf, $match)) { 86 $return['width'] = $match[1]; 87 $return['height'] = $match[2]; 88 } 89 if(preg_match('/\b(\d+(\.\d+)?)X\b/', $conf, $match)) $return['scale'] = $match[1]; 90 if(preg_match('/\bwidth=([0-9]+)\b/i', $conf, $match)) $return['width'] = $match[1]; 91 if(preg_match('/\bheight=([0-9]+)\b/i', $conf, $match)) $return['height'] = $match[1]; 92 // match boolean toggles 93 if(preg_match_all('/\b(no)?(antialias|edgesep|round|shadow)\b/i', $conf, $matches, PREG_SET_ORDER)) { 94 foreach($matches as $match) { 95 $return[$match[2]] = !$match[1]; 96 } 97 } 98 99 $input = join("\n", $lines); 100 $return['md5'] = md5($input); // we only pass a hash around 101 102 // store input for later use in _imagefile() 103 io_saveFile(getCacheName($return['md5'], '.ditaa.txt'), $input); 104 io_saveFile(getCacheName($return['md5'], '.ditaa.cfg'), serialize($return)); 105 106 return $return; 107 } 108 109 /** 110 * Output the image 111 * 112 * @param string $format output format being rendered 113 * @param Doku_Renderer $R the current renderer object 114 * @param array $data data created by handler() 115 * @return boolean rendered correctly? 116 */ 117 public function render($format, Doku_Renderer $R, $data) { 118 global $ID; 119 if($format == 'xhtml') { 120 // Only use the md5 key 121 $img = ml($ID, array('ditaa' => $data['md5'])); 122 $R->doc .= '<img src="' . $img . '" class="media' . $data['align'] . '" alt=""'; 123 if($data['width']) $R->doc .= ' width="' . $data['width'] . '"'; 124 if($data['height']) $R->doc .= ' height="' . $data['height'] . '"'; 125 if($data['align'] == 'right') $R->doc .= ' align="right"'; 126 if($data['align'] == 'left') $R->doc .= ' align="left"'; 127 $R->doc .= '/>'; 128 return true; 129 } else if($format == 'odt') { 130 $src = $this->_imgfile($data['md5']); 131 /** @var renderer_plugin_odt $R */ 132 $R->_odtAddImage($src, $data['width'], $data['height'], $data['align']); 133 return true; 134 } 135 return false; 136 } 137 138 /** 139 * Return path to the rendered image on our local system 140 * 141 * @param string $md5 MD5 of the input data, used to identify the cache files 142 * @return false|string path to file or fals on error 143 */ 144 public function _imgfile($md5) { 145 $file_cfg = getCacheName($md5, '.ditaa.cfg'); // configs 146 $file_txt = getCacheName($md5, '.ditaa.txt'); // input 147 $file_png = getCacheName($md5, '.ditaa.png'); // ouput 148 149 if(!file_exists($file_cfg) || !file_exists($file_txt)) { 150 return false; 151 } 152 $data = unserialize(io_readFile($file_cfg, false)); 153 154 // file does not exist or is outdated 155 if(@filemtime($file_png) < filemtime($file_cfg)) { 156 157 if($this->getConf('java')) { 158 $ok = $this->_runJava($data, $file_txt, $file_png); 159 } else { 160 $ok = $this->_runGo($data, $file_txt, $file_png); 161 #$ok = $this->_remote($data, $in, $cache); 162 } 163 if(!$ok) return false; 164 165 clearstatcache($file_png); 166 } 167 168 // resized version 169 if($data['width']) { 170 $file_png = media_resize_image($file_png, 'png', $data['width'], $data['height']); 171 } 172 173 // something went wrong, we're missing the file 174 if(!file_exists($file_png)) return false; 175 176 return $file_png; 177 } 178 179 /** 180 * Render the output remotely at ditaa.org 181 * 182 * @deprecated ditaa.org is no longer available, so this defunct 183 * @param array $data The config settings 184 * @param string $in Path to the ditaa input file (txt) 185 * @param string $out Path to the output file (PNG) 186 * @return bool true if the image was created, false otherwise 187 */ 188 protected function _remote($data, $in, $out) { 189 global $conf; 190 191 if(!file_exists($in)) { 192 if($conf['debug']) { 193 dbglog($in, 'no such ditaa input file'); 194 } 195 return false; 196 } 197 198 $http = new DokuHTTPClient(); 199 $http->timeout = 30; 200 201 $pass = array(); 202 $pass['scale'] = $data['scale']; 203 $pass['timeout'] = 25; 204 $pass['grid'] = io_readFile($in); 205 if(!$data['antialias']) $pass['A'] = 'on'; 206 if(!$data['shadow']) $pass['S'] = 'on'; 207 if($data['round']) $pass['r'] = 'on'; 208 if(!$data['edgesep']) $pass['E'] = 'on'; 209 210 $img = $http->post('http://ditaa.org/ditaa/render', $pass); 211 if(!$img) return false; 212 213 return io_saveFile($out, $img); 214 } 215 216 /** 217 * Run the ditaa Java program 218 * 219 * @param array $data The config settings 220 * @param string $in Path to the ditaa input file (txt) 221 * @param string $out Path to the output file (PNG) 222 * @return bool true if the image was created, false otherwise 223 */ 224 protected function _runJava($data, $in, $out) { 225 global $conf; 226 227 if(!file_exists($in)) { 228 if($conf['debug']) { 229 dbglog($in, 'no such ditaa input file'); 230 } 231 return false; 232 } 233 234 $cmd = $this->getConf('java'); 235 $cmd .= ' -Djava.awt.headless=true -Dfile.encoding=UTF-8 -jar'; 236 $cmd .= ' ' . escapeshellarg(dirname(__FILE__) . '/ditaa/ditaa0_9.jar'); //ditaa jar 237 $cmd .= ' --encoding UTF-8'; 238 $cmd .= ' ' . escapeshellarg($in); //input 239 $cmd .= ' ' . escapeshellarg($out); //output 240 $cmd .= ' -s ' . escapeshellarg($data['scale']); 241 if(!$data['antialias']) $cmd .= ' -A'; 242 if(!$data['shadow']) $cmd .= ' -S'; 243 if($data['round']) $cmd .= ' -r'; 244 if(!$data['edgesep']) $cmd .= ' -E'; 245 246 exec($cmd, $output, $error); 247 248 if($error != 0) { 249 if($conf['debug']) { 250 dbglog(join("\n", $output), 'ditaa command failed: ' . $cmd); 251 } 252 return false; 253 } 254 255 return true; 256 } 257 258 /** 259 * Run the ditaa Go program 260 * 261 * @param array $data The config settings - currently not used because the Go relase supports no options 262 * @param string $in Path to the ditaa input file (txt) 263 * @param string $out Path to the output file (PNG) 264 * @return bool true if the image was created, false otherwise 265 */ 266 protected function _runGo($data, $in, $out) { 267 global $conf; 268 269 if(!file_exists($in)) { 270 if($conf['debug']) { 271 dbglog($in, 'no such ditaa input file'); 272 } 273 return false; 274 } 275 276 $cmd = $this->getLocalBinary(); 277 if(!$cmd) return false; 278 $cmd .= ' ' . escapeshellarg($in); //input 279 $cmd .= ' ' . escapeshellarg($out); //output 280 281 exec($cmd, $output, $error); 282 283 if($error != 0) { 284 if($conf['debug']) { 285 dbglog(join("\n", $output), 'ditaa command failed: ' . $cmd); 286 } 287 return false; 288 } 289 290 return true; 291 } 292 293 /** 294 * Detects the platform of the PHP host and constructs the appropriate binary name 295 * 296 * @return false|string 297 */ 298 protected function getBinaryName() { 299 $ext = ''; 300 301 $os = php_uname('s'); 302 if(preg_match('/darwin/i', $os)) { 303 $os = 'darwin'; 304 } elseif(preg_match('/win/i', $os)) { 305 $os = 'windows'; 306 $ext = '.exe'; 307 } elseif(preg_match('/linux/i', $os)) { 308 $os = 'linux'; 309 } elseif(preg_match('/freebsd/i', $os)) { 310 $os = 'freebsd'; 311 } elseif(preg_match('/openbsd/i', $os)) { 312 $os = 'openbsd'; 313 } elseif(preg_match('/netbsd/i', $os)) { 314 $os = 'netbsd'; 315 } elseif(preg_match('/(solaris|netbsd)/i', $os)) { 316 $os = 'freebsd'; 317 } else { 318 return false; 319 } 320 321 $arch = php_uname('m'); 322 if($arch == 'x86_64') { 323 $arch = 'amd64'; 324 } elseif(preg_match('/arm/i', $arch)) { 325 $arch = 'amd'; 326 } else { 327 $arch = '386'; 328 } 329 330 return "ditaa-$os-$arch$ext"; 331 } 332 333 /** 334 * Returns the local binary to use 335 * 336 * Downloads it if necessary 337 * 338 * @return bool|string 339 */ 340 protected function getLocalBinary() { 341 global $conf; 342 343 $bin = $this->getBinaryName(); 344 if(!$bin) return false; 345 346 // check distributed files first 347 if(file_exists(__DIR__ . '/ditaa/' . $bin)) { 348 return __DIR__ . '/ditaa/' . $bin; 349 } 350 351 $info = $this->getInfo(); 352 $cache = getCacheName($info['date'], ".$bin"); 353 354 if(file_exists($cache)) return $cache; 355 356 $url = 'https://github.com/akavel/ditaa/releases/download/g1.0.0/' . $bin; 357 if(io_download($url, $cache, false, '', 0)) { 358 @chmod($cache, $conf['dmode']); 359 return $cache; 360 } 361 362 return false; 363 } 364} 365 366