1<?php
2/**
3 * Go diagrams for Dokuwiki
4 *
5 * This was ported from the GoDiag MediWiki extension which itself uses code
6 * and input syntax from from Sensei's Library
7 *
8 * @author     Andreas Gohr <andi@splitbrain.org>
9 *
10 * @link       http://meta.wikimedia.org/wiki/Go_diagrams
11 * @author     Stanislav Traykov
12 *
13 * @link       http://senseis.xmp.net/files/sltxt2png.php.txt published
14 * @author     Arno Hollosi <ahollosi@xmp.net>
15 * @author     Morten Pahle <morten@pahle.org.uk>
16 */
17
18if(!defined('DOKU_INC')) die();
19require_once(DOKU_PLUGIN.'syntax.php');
20
21/**
22 * The godiag plugin class
23 */
24class syntax_plugin_godiag extends DokuWiki_Syntax_Plugin {
25
26    var $dgm;       // holds diagram data
27    var $seqno = 1; // if the same diagram is included more than once on a page,
28                    // we need this to construct a unique image map id
29
30    /**
31     * Map syntax keywords to internal draw functions
32     */
33    var $functab = array(
34            ',' => 'draw_hoshi',
35            'O' => 'draw_white',
36            'X' => 'draw_black',
37            'C' => 'draw_circle',
38            'S' => 'draw_square',
39            'B' => 'draw_black_circle',
40            'W' => 'draw_white_circle',
41            '#' => 'draw_black_square',
42            '@' => 'draw_white_square',
43            '_' => 'draw_wipe');
44
45    /**
46     * Style settings for the generated diagram
47     */
48    var $style = array(
49            'board_max'         => 40,  // max size of go board
50            'sgf_comment'       => 'GoDiag Plugin for DokuWiki',
51            'sgf_link_txt'      => 'SGF',
52            'ttfont'            => 'Vera.ttf',            // true type font
53            'ttfont_sz'         => 10,  // font size in px (roughly half of line_sp)
54            'line_sp'           => 22,  // spacing between two lines
55            'edge_sp'           => 14,  // spacing on edge of board
56            'line_begin'        => 4,   // line begin (if not edge)
57            'coord_sp'          => 20,  // spacing for coordinates (2 * font_sz or more)
58            'stone_radius'      => 11,  // (line_sp / 2 works nice)
59            'mark_radius'       => 5,   // radius of circle mark (about half of stone radius)
60            'mark_sqheight'     => 8,   // height of square mark (somewhat less than stone radius)
61            'link_sqheight'     => 21,  // height of link highlight (line_sp - 1)
62            'hoshi_radius'      => 3,                       // star point radius
63            'goban_acolor'      => array (242, 180, 101),   // background color in RGB
64            'black_acolor'      => array(0, 0, 0),
65            'white_acolor'      => array(255, 255, 255),
66            'white_rim_acolor'  => array(70, 70, 70),
67            'mark_acolor'       => array(244, 0, 0),
68            'link_acolor_alpha' => array(10, 50, 255, 96),  // R G B alpha (=transparency)
69            'line_acolor'       => array(0, 0, 0),
70            'string_acolor'     => array(0, 0, 0));
71
72    /**
73     * Regular expression to parse hints for creating SGF files
74     */
75    var $hintre = '/(\d|10) (?:at|on) (\d)/';
76
77    /**
78     * Constructor. Initializes the diagram styles and localization
79     */
80    function syntax_plugin_godiag() {
81        //set correct path to font file
82        $this->style['ttfont'] = dirname(__FILE__).'/Vera.ttf';
83    }
84
85    /**
86     * What kind of syntax are we?
87     */
88    function getType(){
89        return 'substition';
90    }
91
92    /**
93     * What HTML type are we?
94     */
95    function getPType(){
96        return 'block';
97    }
98
99    /**
100     * Where to sort in?
101     */
102    function getSort(){
103        return 160;
104    }
105
106    /**
107     * Connect pattern to lexer
108     */
109    function connectTo($mode) {
110        $this->Lexer->addSpecialPattern('<go>.*?</go>',$mode,'plugin_godiag');
111    }
112
113
114    /**
115     * Create output
116     */
117    function render($mode, Doku_Renderer $renderer, $data) {
118        if($mode != 'xhtml') return false;
119
120        // check for errors first
121        if($data['error']){
122            $renderer->doc .= '<div class="error">Go Diagram plugin error: '.$data['error'].'</div>';
123            return;
124        }
125
126
127        // create proper image map and attribute for IMG tag
128        $map_id = 'godiag__' . $this->seqno++ . $md5hash_png;
129        if($data['imap_html']) {
130            $data['imap_html'] = "<map id=\"$map_id\" name=\"$map_id\">" . $data['imap_html'] . '</map>';
131            $godiag_imap_imgs = "usemap=\"#$map_id\"";
132        } else {
133            $godiag_imap_imgs = '';
134        }
135
136        // now we have the HTML to be returned if everything went OK FIXME
137        $sgf_href = DOKU_BASE.'lib/plugins/godiag/fetch.php?f='.$data['md5hash_sgf'].'&amp;t=sgf';
138        $png_href = DOKU_BASE.'lib/plugins/godiag/fetch.php?f='.$data['md5hash_png'].'&amp;t=png';
139
140        $renderer->doc .= '<div class="godiag-' . $data['divclass'] . '">';
141        $renderer->doc .= $data['imap_html'];
142        $renderer->doc .= '<div class="godiagi" style="width:'.$data['width'].'px;">';
143        $renderer->doc .= '<img class="godiagimg" src="'.$png_href.'" alt="go diagram" '.$godiag_imap_imgs.'/>';
144        $renderer->doc .= '<div class="godiagheading">';
145        $renderer->doc .= hsc($data['heading']).' ';
146        $renderer->doc .= '<a href="'.$sgf_href.'" title="'.$this->getLang('sgfdownload').'">[SGF]</a>';
147        $renderer->doc .= '</div></div></div>';
148
149        if($data['break']) {
150            $return_str .= '<br class="godiag-' . $data['divclass'] . '"/>';
151        }
152
153        $renderer->doc .= $return_str;
154        return true;
155    }
156
157
158    /**
159     * Handle the match.
160     *
161     * Most work is done here, like parsing the syntax and creating the image and SGF file
162     */
163    function handle($match, $state, $pos, Doku_Handler $handler){
164        $sourceandlinks = trim(substr($match,4,-5));
165
166
167        // diagram specific things
168        $this->dgm = array (
169                'dia' => array(),
170                'edge_top' => false,
171                'edge_bottom' => false,
172                'edge_left' => false,
173                'edge_right' => false,
174                'black_first' => true,
175                'coord_markers' => false,
176                'offset_x' => 0,
177                'offset_y' => 0,
178                'board_size' => 19,
179                'gridh' => 0,
180                'gridv' => 0,
181                'imap_html' => '',
182                'imappings' => array(),
183                'divclass' => 'right', // enclose HTML in div class="godiag-$whatever"
184                'break' => false);      // append br style="clear: left|right" to HTML
185
186        // this will be concatenated to so we can compute a hash
187        $str_for_hash='';
188
189        // strip empty space and final newline
190        $sourceandlinks=preg_replace('/(^|\n)\s*/',"$1", $sourceandlinks);
191        $sourceandlinks=preg_replace("/\s*$/",'', $sourceandlinks);
192
193        // separate source from links part
194        $source_parts=preg_split("/\n[\$\s]*(?=\[)/", $sourceandlinks, 2);
195
196        // links: store mappings for image map
197        if(array_key_exists(1, $source_parts)){
198            foreach (explode("\n", $source_parts[1]) as $link_line) {
199                if(preg_match('/\[\s*([^\s])\s*\|\s*([^\]]+?)\s*\]/', $link_line, $matches)) {
200                    $this->dgm['imappings'][$matches[1]]=$matches[2];
201                }
202            }
203        }
204        ksort($this->dgm['imappings']);
205        foreach($this->dgm['imappings'] as $symb => $href) {
206            $str_for_hash .= $symb . '!' . $href . '!';
207        }
208
209        // source
210        $source_lines=explode("\n", $source_parts[0]);
211
212        // header
213        preg_match('/(\$\$[^\s]*)?\s*(.*)/', $source_lines[0], $matches);
214        $hdr=$matches[1];
215        $heading=$matches[2];
216        $hdr=explode('#', $hdr);
217        $h_ops = $hdr[0];
218
219        if(strpos($h_ops, 'W')) {
220            $this->dgm['black_first'] = false;
221        }
222        if(strpos($h_ops, 'b')) {
223            $this->dgm['break'] = 'true';
224        }
225        if(strpos($h_ops, 'c')) {
226            $this->dgm['coord_markers'] = true;
227        }
228        if(strpos($h_ops, 'l')) {
229            $this->dgm['divclass'] = 'left';
230        }
231        if(strpos($h_ops, 'r')) {
232            $this->dgm['divclass'] = 'right';
233        }
234        if(preg_match('/(\d+)/', $h_ops, $matches)) {
235            $this->dgm['board_size'] = $matches[1];
236        }
237        $this->dgm['title'] = $heading;
238        $heading = htmlspecialchars($heading);
239        $last=count($source_lines)-1;
240        if(preg_match('/^(\$|\s)*[-+]+\s*$/', $source_lines[$last])) {
241            $this->dgm['edge_bottom']=true;
242            unset($source_lines[$last]);
243        }
244        unset($source_lines[0]);
245        if(preg_match('/^(\$|\s)*[-+]+\s*$/', $source_lines[1])) {
246            $this->dgm['edge_top']=true;
247            unset($source_lines[1]);
248        }
249
250        // get the diagram into $this->dgm['dia'][y][x], figure out dimensions and edges,
251        // and generate html for image map
252        $row=0;
253        foreach ($source_lines as $source_line) {
254            if(preg_match('/^(\s|\$)*\|/', $source_line)) { $this->dgm['edge_left']=true; }
255            if(preg_match('/\|\s*$/', $source_line)) { $this->dgm['edge_right']=true; }
256            $plainstr=str_replace(array('$', ' ', '|'), '', $source_line);
257            $str_for_hash .= '$' . $plainstr;
258            $as_array=preg_split('//', $plainstr, -1, PREG_SPLIT_NO_EMPTY);
259            $len=count($as_array);
260            foreach($as_array as $bx => $symb) {
261                if(array_key_exists($symb, $this->dgm['imappings'])) {
262                    $this->dgm['imap_html'] .= $this->imap_area($bx, $row, $this->dgm['imappings'][$symb]);
263                }
264            }
265            if($len>$this->dgm['gridh']) { $this->dgm['gridh']=$len; }
266            $this->dgm['dia'][$row++]=$as_array;
267        }
268        $this->dgm['gridv']=$row;
269
270        // calc dimensions
271        $dimh=($this->dgm['gridh']-1)*$this->style['line_sp']+$this->style['edge_sp']*2+1;
272        $dimv=($this->dgm['gridv']-1)*$this->style['line_sp']+$this->style['edge_sp']*2+1;
273        if($this->dgm['coord_markers']) {
274            $dimh += $this->style['coord_sp'];
275            $dimv += $this->style['coord_sp'];
276        }
277        if(($offs_sz_arr=$this->calc_offsets_and_size())===false) {
278            return $this->error_box('non-square board');
279        }
280        list($this->dgm['board_size'], //this will be overwritten if it conflicts with other input
281                $this->dgm['offset_x'],
282                $this->dgm['offset_y'])   =   $offs_sz_arr;
283
284        if($this->dgm['board_size'] > $this->style['board_max'])
285            return $this->error_box('board too large (max is ' . $this->style['board_max'] . ')');
286
287        //determine implicit edges
288        if($this->dgm['gridv'] >= $this->dgm['board_size'] - 1) {
289            $this->dgm['edge_top'] = true;
290            if($this->dgm['gridv'] == $this->dgm['board_size'])
291            $this->dgm['edge_bottom']=true;
292        }
293        if($this->dgm['gridh'] >= $this->dgm['board_size'] - 1) {
294            $this->dgm['edge_left']=true;
295            if($this->dgm['gridh'] == $this->dgm['board_size'])
296                $this->dgm['edge_right']=true;
297        }
298
299        // compute hashes (yes this duplicates a bit of what getCacheName() would do anyway,
300        // but keeping it here makes porting easier)
301        $str_for_hash .=  '!' . $this->dgm['black_first']
302        . '!' . $this->dgm['edge_top']
303        . '!' . $this->dgm['edge_bottom']
304        . '!' . $this->dgm['edge_left']
305        . '!' . $this->dgm['edge_right'];
306
307        $str_for_hash_sgf = $str_for_hash
308            . '!' . $this->dgm['board_size']
309            . '!' . $this->dgm['title'];
310
311        $str_for_hash_img = $str_for_hash
312            . '!' . ($this->dgm['coord_markers'] ? $this->dgm['board_size'] : false);
313
314        $md5hash_png=md5($str_for_hash_img . '!' . serialize($this->style));
315        $md5hash_sgf=md5($str_for_hash_sgf);
316
317        // get filenames
318        $filename_png = getCacheName($md5hash_png,'.godiag.png');
319        $filename_sgf = getCacheName($md5hash_sgf,'.godiag.sgf');
320
321        // if we don't have the PNG for this diagram, create it
322        if(@filemtime($filename_png) < filemtime(__FILE__)) {
323            $draw_result = $this->save_diagram($dimh, $dimv, $filename_png);
324            if($draw_result!==0) return $draw_result; // contains error message
325        }
326
327        // if we don't have the SGF for this diagram, create it
328        if(@filemtime($filename_sgf) < filemtime(__FILE__)) {
329            if(!io_saveFile($filename_sgf,$this->SGF())){
330                return $this->error_box("Cannot create SGF file.");
331            }
332        }
333
334        // now pass only the interesting data to the renderer
335        $data = array(
336            'md5hash_png' => $md5hash_png,
337            'md5hash_sgf' => $md5hash_sgf,
338            'imap_html'   => $this->dgm['imap_html'],
339            'divclass'    => $this->dgm['divclass'],
340            'break'       => $this->dgm['break'],
341            'heading'     => $heading,
342            'width'       => $dimh,
343            'height'      => $dimv,
344        );
345
346        return $data;
347    } // end of get_html()
348
349    /**
350     * creates an anti-aliased circle
351     *
352     * @author <klaas at kosmokrator dot com>
353     * @link http://www.php.net/manual/en/function.imageantialias.php#61932
354     */
355    function circ( &$img, $cx, $cy, $cr, $color) {
356        $ir = $cr;
357        $ix = 0;
358        $iy = $ir;
359        $ig = 2 * $ir - 3;
360        $idgr = -6;
361        $idgd = 4 * $ir - 10;
362        $fill = imageColorExactAlpha( $img, $color[0], $color[1], $color[2], 0);
363        imageLine( $img, $cx + $cr - 1, $cy, $cx, $cy, $fill );
364        imageLine( $img, $cx - $cr + 1, $cy, $cx - 1, $cy, $fill );
365        imageLine( $img, $cx, $cy + $cr - 1, $cx, $cy + 1, $fill );
366        imageLine( $img, $cx, $cy - $cr + 1, $cx, $cy - 1, $fill );
367        $draw = imageColorExactAlpha( $img, $color[0], $color[1], $color[2], 42);
368        imageSetPixel( $img, $cx + $cr, $cy, $draw );
369        imageSetPixel( $img, $cx - $cr, $cy, $draw );
370        imageSetPixel( $img, $cx, $cy + $cr, $draw );
371        imageSetPixel( $img, $cx, $cy - $cr, $draw );
372        while ( $ix <= $iy - 2 ) {
373            if ( $ig < 0 ) {
374                $ig += $idgd;
375                $idgd -= 8;
376                $iy--;
377            } else {
378                $ig += $idgr;
379                $idgd -= 4;
380            }
381            $idgr -= 4;
382            $ix++;
383            imageLine( $img, $cx + $ix, $cy + $iy - 1, $cx + $ix, $cy + $ix, $fill );
384            imageLine( $img, $cx + $ix, $cy - $iy + 1, $cx + $ix, $cy - $ix, $fill );
385            imageLine( $img, $cx - $ix, $cy + $iy - 1, $cx - $ix, $cy + $ix, $fill );
386            imageLine( $img, $cx - $ix, $cy - $iy + 1, $cx - $ix, $cy - $ix, $fill );
387            imageLine( $img, $cx + $iy - 1, $cy + $ix, $cx + $ix, $cy + $ix, $fill );
388            imageLine( $img, $cx + $iy - 1, $cy - $ix, $cx + $ix, $cy - $ix, $fill );
389            imageLine( $img, $cx - $iy + 1, $cy + $ix, $cx - $ix, $cy + $ix, $fill );
390            imageLine( $img, $cx - $iy + 1, $cy - $ix, $cx - $ix, $cy - $ix, $fill );
391            $filled = 0;
392            for ( $xx = $ix - 0.45; $xx < $ix + 0.5; $xx += 0.2 ) {
393                for ( $yy = $iy - 0.45; $yy < $iy + 0.5; $yy += 0.2 ) {
394                    if ( sqrt( pow( $xx, 2 ) + pow( $yy, 2 ) ) < $cr ) $filled += 4;
395                }
396            }
397            $draw = imageColorExactAlpha( $img, $color[0], $color[1], $color[2], ( 100 - $filled ) );
398            imageSetPixel( $img, $cx + $ix, $cy + $iy, $draw );
399            imageSetPixel( $img, $cx + $ix, $cy - $iy, $draw );
400            imageSetPixel( $img, $cx - $ix, $cy + $iy, $draw );
401            imageSetPixel( $img, $cx - $ix, $cy - $iy, $draw );
402            imageSetPixel( $img, $cx + $iy, $cy + $ix, $draw );
403            imageSetPixel( $img, $cx + $iy, $cy - $ix, $draw );
404            imageSetPixel( $img, $cx - $iy, $cy + $ix, $draw );
405            imageSetPixel( $img, $cx - $iy, $cy - $ix, $draw );
406        }
407    }
408
409    function draw_hoshi($im, $bx, $by) {
410        $coords=$this->get_coords($bx, $by);
411        $this->circ($im, $coords[0], $coords[1], $this->style['hoshi_radius'], $this->style['line_acolor']);
412    }
413
414    function draw_white($im, $bx, $by) {
415        $coords=$this->get_coords($bx, $by);
416        $this->circ($im, $coords[0], $coords[1], $this->style['stone_radius'], $this->style['white_rim_acolor']);
417        $this->circ($im, $coords[0], $coords[1], $this->style['stone_radius']-1, $this->style['white_acolor']);
418    }
419
420    function draw_black($im, $bx, $by) {
421        $coords=$this->get_coords($bx, $by);
422        $this->circ($im, $coords[0], $coords[1], $this->style['stone_radius'], $this->style['black_acolor']);
423    }
424
425    function draw_white_circle($im, $bx, $by) {
426        $coords=$this->get_coords($bx, $by);
427        $this->draw_white($im, $bx, $by);
428        $this->circ($im, $coords[0], $coords[1], $this->style['mark_radius'], $this->style['mark_acolor']);
429        $this->circ($im, $coords[0], $coords[1], $this->style['mark_radius']-2, $this->style['white_acolor']);
430    }
431
432    function draw_black_circle($im, $bx, $by) {
433        $coords=$this->get_coords($bx, $by);
434        $this->draw_black($im, $bx, $by);
435        $this->circ($im, $coords[0], $coords[1], $this->style['mark_radius'], $this->style['mark_acolor']);
436        $this->circ($im, $coords[0], $coords[1], $this->style['mark_radius']-2, $this->style['black_acolor']);
437    }
438
439    function draw_circle($im, $bx, $by) {
440        $coords=$this->get_coords($bx, $by);
441        list($x, $y) = $coords=$this->get_coords($bx, $by);
442        $r=$this->style['mark_radius'];
443        $sim = $this->style['circle_mark_img']['im'];
444        $dim = $this->style['circle_mark_img']['dim'];
445        imagecopymerge($im, $sim, $x-$r-1, $y-$r-1, 0, 0, $dim, $dim, 100);
446    }
447
448    function draw_square($im, $bx, $by) {
449        list($x, $y) = $coords=$this->get_coords($bx, $by);
450        $x1=$x-($this->style['mark_sqheight']/2);
451        $x2=$x+($this->style['mark_sqheight']/2);
452        $y1=$y-($this->style['mark_sqheight']/2);
453        $y2=$y+($this->style['mark_sqheight']/2);
454        imagefilledrectangle($im, $x1, $y1, $x2, $y2, $this->dgm['mark_color']);
455    }
456    function draw_link($im, $bx, $by) {
457        list($x, $y) = $coords=$this->get_coords($bx, $by);
458        $x1=$x-($this->style['link_sqheight']/2);
459        $x2=$x+($this->style['link_sqheight']/2);
460        $y1=$y-($this->style['link_sqheight']/2);
461        $y2=$y+($this->style['link_sqheight']/2);
462        imagefilledrectangle($im, $x1, $y1, $x2, $y2, $this->dgm['link_color']);
463    }
464    function draw_white_square($im, $bx, $by) {
465        $this->draw_white($im, $bx, $by);
466        $this->draw_square($im, $bx, $by);
467    }
468    function draw_black_square($im, $bx, $by) {
469        $this->draw_black($im, $bx, $by);
470        $this->draw_square($im, $bx, $by);
471    }
472    function draw_num($im, $bx, $by) {
473        $str=$this->dgm['dia'][$by][$bx];
474        $str=($str=='0') ? '10' : $str;
475        $blacks_turn = intval($str) % 2 == ($this->dgm['black_first'] ? 1 : 0);
476        if($blacks_turn)
477            $this->draw_black($im, $bx, $by);
478        else
479            $this->draw_white($im, $bx, $by);
480        list($x, $y) = $coords=$this->get_coords($bx, $by);
481        $col=$blacks_turn ? $this->dgm['white_color'] : $this->dgm['black_color'];
482        $box = imagettfbbox($this->style['ttfont_sz'], 0, $this->style['ttfont'], $str);
483        $basex=$x - intval(abs($box[2]-$box[0])+1)/2 - $box[0];
484        imagettftext($im, $this->style['ttfont_sz'], 0, $basex, $y+$this->style['majusc_voffs'], $col, $this->style['ttfont'], $str);
485    }
486
487    function draw_let($im, $bx, $by) {
488        list($x, $y) = $coords=$this->get_coords($bx, $by);
489        $str = $this->dgm['dia'][$by][$bx];
490        $box = imagettfbbox($this->style['ttfont_sz'], 0, $this->style['ttfont'], $str);
491        $basex = $x - intval(abs($box[2]-$box[0])+1)/2 - $box[0];
492        $r = $this->style['stone_radius']-3;
493        imagefilledrectangle($im, $x-$r, $y-$r, $x+$r, $y+$r, $this->dgm['goban_color']);
494        imagettftext($im, $this->style['ttfont_sz'], 0, $basex, $y+$this->style['minusc_voffs'], $this->dgm['string_color'], $this->style['ttfont'], $str);
495    }
496
497    function draw_coord($im, $bx, $by) {
498        list($x, $y) = $coords=$this->get_coords($bx, $by);
499        if($bx==-1)
500            $str = $this->dgm['board_size'] - $this->dgm['offset_y'] - $by;
501        else {
502            $bx2 = $this->dgm['offset_x'] + $bx;
503            $str=chr(65+$bx2+($bx2>7? 1 : 0));
504        }
505        $box = imagettfbbox($this->style['ttfont_sz'], 0, $this->style['ttfont'], $str);
506        if($bx==-1)
507            $basex=$x - $this->style['stone_radius']+($str<10?$this->style['sm_offs']:0);
508        else {
509            $basex=$x - intval(abs($box[2]-$box[0])+1)/2 - $box[0];
510        }
511        imagettftext($im, $this->style['ttfont_sz'], 0, $basex, $y+$this->style['majusc_voffs'], $this->dgm['string_color'], $this->style['ttfont'], $str);
512    }
513
514    function draw_wipe($im, $bx, $by) {
515        list($x, $y) = $coords=$this->get_coords($bx, $by);
516        imagefilledrectangle($im, $x-$this->style['line_sp']/2+1, $y-$this->style['line_sp']/2+1, $x+$this->style['line_sp']/2, $y+$this->style['line_sp']/2, $this->dgm['goban_color']);
517    }
518
519    /**
520     * draw and save diagram
521     */
522    function save_diagram($dimh, $dimv, $filename_png) {
523        $im = imagecreatetruecolor($dimh, $dimv);
524        if(!$im)
525            return($this->error_box("Cannot initialize GD image stream."));
526
527        //some things we only want to do once
528        if(!array_key_exists('sm_offs', $this->style)) {
529            // calc <10 number horiz offset for coords
530            $box = imagettfbbox($this->style['ttfont_sz'], 0, $this->style['ttfont'], '1');
531            $box2 = imagettfbbox($this->style['ttfont_sz'], 0, $this->style['ttfont'], '11');
532            $this->style['sm_offs'] = ($box2[2]-$box[1]) - ($box[2]-$box[0]) - 1;
533            // vertical offsets for majuscules and minuscules
534            $box = imagettfbbox($this->style['ttfont_sz'], 0, $this->style['ttfont'], 'a');
535            $this->style['minusc_voffs'] = (-$box[5])/2+1;
536            $box = imagettfbbox($this->style['ttfont_sz'], 0, $this->style['ttfont'], 'A');
537            $this->style['majusc_voffs'] = (-$box[5])/2;
538        }
539        if(!array_key_exists('circle_mark_img', $this->style)) {
540            $r=$this->style['mark_radius'];
541            $dim=$r*2+3;
542            $xim = imagecreatetruecolor($dim, $dim);
543            $gc = $this->style['goban_acolor'];
544            $transp = imagecolorallocate($xim, $gc[0], $gc[1], $gc[2]);
545            imagecolortransparent($xim, $transp);
546            imagefill($xim, 0, 0, $transp);
547            $this->circ($xim, $r+1, $r+1, $r, $this->style['mark_acolor']);
548            $this->circ($xim, $r+1, $r+1, $r-2, $this->style['goban_acolor']);
549            $this->style['circle_mark_img']['im'] = $xim;
550            $this->style['circle_mark_img']['dim'] = $dim;
551        }
552
553        $this->dgm['line_color'] = $this->acolor2color($im, $this->style['line_acolor']);
554        $this->dgm['mark_color'] = $this->acolor2color($im, $this->style['mark_acolor']);
555        $this->dgm['link_color'] = $this->acolor2color($im, $this->style['link_acolor_alpha'], true);
556        $this->dgm['goban_color'] = $this->acolor2color($im, $this->style['goban_acolor']);
557        $this->dgm['black_color'] = $this->acolor2color($im, $this->style['black_acolor']);
558        $this->dgm['white_color'] = $this->acolor2color($im, $this->style['white_acolor']);
559        $this->dgm['string_color'] = $this->acolor2color($im, $this->style['string_acolor']);
560
561        imagefill($im, 0, 0, $this->dgm['goban_color']);
562
563        // draw lines
564        $beginv=$this->dgm['edge_top'] ? $this->style['edge_sp'] : $this->style['line_begin'];
565        $beginh=$this->dgm['edge_left'] ? $this->style['edge_sp'] : $this->style['line_begin'];
566        $endv=$dimv - ($this->dgm['edge_bottom'] ? $this->style['edge_sp'] : $this->style['line_begin']) - 1;
567        $endh=$dimh - ($this->dgm['edge_right'] ? $this->style['edge_sp'] : $this->style['line_begin']) - 1;
568        if($this->dgm['coord_markers']) {
569            $beginv += $this->style['coord_sp'];
570            $beginh += $this->style['coord_sp'];
571        }
572
573        // draw horizontal lines
574        for($i=0; $i<$this->dgm['gridv']; $i+=1) {
575            $coords=$this->get_coords($i, $i);
576            imageline($im, $beginh, $coords[0], $endh, $coords[0], $this->dgm['line_color']);
577        }
578        // draw vertical lines
579        for($i=0; $i<$this->dgm['gridh']; $i+=1) {
580            $coords=$this->get_coords($i, $i);
581            imageline($im, $coords[0], $beginv, $coords[0], $endv, $this->dgm['line_color']);
582        }
583
584        // draw coordinates (if requested)
585        if($this->dgm['coord_markers']) {
586            $this->draw_coord($im, -1, 2);
587            for($i=0; $i<$this->dgm['gridv']; $i++)
588                $this->draw_coord($im, -1, $i);
589            for($i=0; $i<$this->dgm['gridh']; $i++)
590                $this->draw_coord($im, $i, -1);
591        }
592
593        // draw rest
594        foreach($this->dgm['dia'] as $by => $row) {
595            foreach($row as $bx => $symb) {
596                if($symb>='0' and $symb<='9')
597                    $this->draw_num($im, $bx, $by);
598                else if($symb>='a' and $symb<='z')
599                    $this->draw_let($im, $bx, $by);
600                else if($symb!='.') {
601                    if(!array_key_exists($symb, $this->functab)) {
602                        imagedestroy($im);
603                        $oopshtml=htmlspecialchars("unknown symbol \"$symb\"");
604                        return $this->error_box($oopshtml);
605                    }
606                    call_user_func(array($this, $this->functab[$symb]), $im, $bx, $by);
607                }
608                // draw link, if any
609                if(array_key_exists($symb, $this->dgm['imappings'])) {
610                    $this->draw_link($im, $bx, $by);
611                }
612
613            }
614        }
615
616        // save
617        if(!imagepng($im, $filename_png)) {
618            imagedestroy($im);
619            $hfilename=htmlspecialchars($filename_png);
620            return $this->error_box("Cannot output diagram to file.");
621        }
622        imagedestroy($im);
623        return 0;
624    } // end of $this->save_diagram()
625
626    // other funcs
627    function get_coords($bx, $by) {
628        $additional_sp=0;
629        if($this->dgm['coord_markers'])
630            $additional_sp+=$this->style['coord_sp'];
631        return array($bx*$this->style['line_sp']+$this->style['edge_sp']+$additional_sp,
632                $by*$this->style['line_sp']+$this->style['edge_sp']+$additional_sp);
633    }
634
635    /**
636     * Calculate an area tag for a image map. Handles external and internal links
637     */
638    function imap_area($bx, $by, $href) {
639        global $ID;
640
641        list($x, $y) = $coords=$this->get_coords($bx, $by);
642        $x1=$x-$this->style['line_sp']/2;
643        $y1=$y-$this->style['line_sp']/2;
644        $x2=$x+$this->style['line_sp']/2;
645        $y2=$y+$this->style['line_sp']/2;
646        // external or internal link?
647        if(strpos($href, '://')){
648            $href  = hsc($href);
649            $title = $href;
650        }else{
651            $ns = getNS($ID);
652            resolve_pageid($ns,$href,$exists);
653            $title = $href;
654            $href  = wl($href);
655        }
656
657        return "<area href=\"$href\" title=\"$title\" alt=\"$title\" coords=\"$x1,$y1,$x2,$y2\"/>";
658    }
659
660    function acolor2color($im, $acolor, $alpha = false) {
661        if($alpha)
662            return imagecolorexactalpha($im, $acolor[0], $acolor[1], $acolor[2], $acolor[3]);
663        else
664            return imagecolorallocate($im, $acolor[0], $acolor[1], $acolor[2]);
665    }
666
667    /**
668     * Used for error reporting. Passes the string inside an array for streamlined
669     * handler/renderer error transfer
670     */
671    function error_box($str) {
672        return array('error' => $str);
673    }
674
675    /**
676     * calculates board size and offsets for SGF and coordinate drawing
677     * returns an array (board_size, offset_x, offset_y) or false if there's a conflict
678     */
679    function calc_offsets_and_size() {
680        $sizex = $this->dgm['gridh'];
681        $sizey = $this->dgm['gridv'];
682        $heightdefined = $this->dgm['edge_top'] && $this->dgm['edge_bottom'];
683        $widthdefined = $this->dgm['edge_left'] && $this->dgm['edge_right'];
684        $offset_x = 0;
685        $offset_y = 0;
686        if ($heightdefined) {
687            if ($widthdefined && $sizex != $sizey)
688                return false;
689            if ($sizex > $sizey)
690                return false;
691            $size = $sizey;
692            if ($this->dgm['edge_right']) $offset_x = $size-$sizex;
693            elseif (!$this->dgm['edge_left'])     $offset_x = ($size-$sizex)/2;
694        }
695        elseif ($widthdefined)
696        {
697            if ($sizey > $sizex)
698                return false;
699            $size = $sizex;
700            if ($this->dgm['edge_bottom'])        $offset_y = $size-$sizey;
701            elseif (!$this->dgm['edge_top'])      $offset_y = ($size-$sizey)/2;
702        }
703        else
704        {
705            $size = max($sizex, $sizey, $this->dgm['board_size']);
706
707            if ($this->dgm['edge_right']) $offset_x = $size-$sizex;
708            elseif (!$this->dgm['edge_left'])     $offset_x = ($size-$sizex)/2;
709
710            if ($this->dgm['edge_bottom'])        $offset_y = $size-$sizey;
711            elseif (!$this->dgm['edge_top'])      $offset_y = ($size-$sizey)/2;
712        }
713        return(array($size, intval($offset_x), intval($offset_y)));
714    }
715
716    /**
717     * Create an SGF file for the current diagram
718     */
719    function SGF() {
720        $rows = $this->dgm['dia'];
721        $title = str_replace(']', '\]', $this->dgm['title']);
722        $comment = str_replace(']', '\]', $this->style['sgf_comment']);
723        $sizex = $this->dgm['gridh'];
724        $sizey = $this->dgm['gridv'];
725        $size = $this->dgm['board_size'];
726        $offset_x = $this->dgm['offset_x'];
727        $offset_y = $this->dgm['offset_y'];
728        // SGF Root node string
729        $firstcolor = $this->dgm['black_first'] ? 'B' : 'W';
730        $SGFString = "(;GM[1]FF[4]SZ[$size]\n\n" .
731            "GN[$title]\n" .
732            "AP[GoDiag/DokuWiki]\n" .
733            "DT[".date("Y-m-d")."]\n" .
734            "PL[$firstcolor]\n" .
735            "C[$comment]\n";
736
737        $AB = array();
738        $AW = array();
739        $CR = array();
740        $SQ = array();
741        $LB = array();
742
743        if (!$this->dgm['black_first']) {
744            $oddplayer = 'W';
745            $evenplayer = 'B';
746        } else {
747            $oddplayer = 'B';
748            $evenplayer = 'W';
749        }
750
751        // output stones, numbers etc. for each row
752        for ($ypos=0; $ypos<$sizey; $ypos++) {
753            for ($xpos=0; $xpos<$sizex; $xpos++) {
754                if(array_key_exists($ypos, $rows) && array_key_exists($xpos, $rows[$ypos]))
755                    $curchar = $rows[$ypos][$xpos];
756                else
757                    continue;
758                $position = chr(97+$xpos+$offset_x) .
759                    chr(97+$ypos+$offset_y);
760
761                if ($curchar == 'X' || $curchar == 'B' || $curchar == '#')
762                    $AB[] = $position;    // add black stone
763
764                if ($curchar == 'O' || $curchar == 'W' || $curchar == '@')
765                    $AW[] = $position;    // add white stone
766
767                if ($curchar == 'B' || $curchar == 'W' || $curchar == 'C')
768                    $CR[] = $position;    // add circle markup
769
770                if ($curchar == '#' || $curchar == '@' || $curchar == 'S')
771                    $SQ[] = $position;    // add circle markup
772
773                // other markup
774                if ($curchar % 2 == 1)     // odd numbers (moves)
775                {
776                    $Moves[$curchar][1] = $position;
777                    $Moves[$curchar][2] = $oddplayer;
778                }
779                elseif ($curchar*2 > 0 || $curchar == '0')  // even num (moves)
780                {
781                    if ($curchar == '0')
782                        $curchar = '10';
783                    $Moves[$curchar][1] = $position;
784                    $Moves[$curchar][2] = $evenplayer;
785                }
786                elseif (($curchar >= 'a') && ($curchar <= 'z')) // letter markup
787                    $LB[] = "$position:$curchar";
788            } // for xpos loop
789        }// for ypos loop
790
791        // parse title for hint of more moves
792        if ($cnt = preg_match_all($this->hintre, $title, $match)) {
793            for ($i=0; $i < $cnt; $i++)
794            {
795                if (!isset($Moves[$match[1][$i]])  // only if not set on board
796                        &&   isset($Moves[$match[2][$i]])) // referred move must be set
797                {
798                    $mvnum = $match[1][$i];
799                    $Moves[$mvnum][1] = $Moves[$match[2][$i]][1];
800                    $Moves[$mvnum][2] = $mvnum % 2 ? $oddplayer : $evenplayer;
801                }
802            }
803        }
804
805        // build SGF string
806        if (count($AB)) $SGFString .= 'AB[' . join('][', $AB) . "]\n";
807        if (count($AW)) $SGFString .= 'AW[' . join('][', $AW) . "]\n";
808        $Markup = '';
809        if (count($CR)) $Markup = 'CR[' . join('][', $CR) . "]\n";
810        if (count($SQ)) $Markup .= 'SQ[' . join('][', $SQ) . "]\n";
811        if (count($LB)) $Markup .= 'LB[' . join('][', $LB) . "]\n";
812        $SGFString .= "$Markup\n";
813
814        for ($mv=1; $mv <= 10; $mv++)
815        {
816            if (isset($Moves[$mv])) {
817                $SGFString .= ';' . $Moves[$mv][2] . '[' . $Moves[$mv][1] . ']';
818                $SGFString .= 'C['. $Moves[$mv][2] . $mv . "]\n";
819                $SGFString .= $Markup;
820            }
821        }
822
823        $SGFString .= ")\n";
824
825        return $SGFString;
826    } // end of $this->SGF()
827
828
829} // end of class
830
831