1<?php
2
3/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5/**
6 * Class for handling output in SVG format.
7 *
8 * LICENSE: This library is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU Lesser General Public License as published by
10 * the Free Software Foundation; either version 2.1 of the License, or (at your
11 * option) any later version. This library is distributed in the hope that it
12 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
14 * General Public License for more details. You should have received a copy of
15 * the GNU Lesser General Public License along with this library; if not, write
16 * to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
17 * 02111-1307 USA
18 *
19 * @category   Images
20 * @package    Image_Canvas
21 * @author     Jesper Veggerby <pear.nosey@veggerby.dk>
22 * @copyright  Copyright (C) 2003, 2004 Jesper Veggerby Hansen
23 * @license    http://www.gnu.org/copyleft/lesser.html  LGPL License 2.1
24 * @version    CVS: $Id: SVG.php 287471 2009-08-18 23:12:01Z clockwerx $
25 * @link       http://pear.php.net/package/Image_Canvas
26 */
27
28/**
29 * Include file Image/Canvas.php
30 */
31require_once 'Image/Canvas.php';
32
33/**
34 * Include file Image/Canvas/Color.php
35 */
36require_once 'Image/Canvas/Color.php';
37
38/**
39 * SVG Canvas class.
40 *
41 * @category   Images
42 * @package    Image_Canvas
43 * @author     Jesper Veggerby <pear.nosey@veggerby.dk>
44 * @copyright  Copyright (C) 2003, 2004 Jesper Veggerby Hansen
45 * @license    http://www.gnu.org/copyleft/lesser.html  LGPL License 2.1
46 * @version    Release: @package_version@
47 * @link       http://pear.php.net/package/Image_Canvas
48 */
49class Image_Canvas_SVG extends Image_Canvas
50{
51
52    /**
53     * The SVG elements
54     * @var string
55     * @access private
56     */
57    var $_elements = '';
58
59    /**
60     * The SVG defines
61     * @var string
62     * @access private
63     */
64    var $_defs = '';
65
66    /**
67     * The current indention level
68     * @var string
69     * @access private
70     */
71    var $_indent = '    ';
72
73    /**
74     * A unieuq id counter
75     * @var int
76     * @access private
77     */
78    var $_id = 1;
79
80    /**
81     * The current group ids
82     * @var array
83     * @access private
84     */
85    var $_groupIDs = array();
86
87    /**
88     * The XML encoding (default iso-8859-1)
89     * @var string
90     * @access private
91     */
92    var $_encoding = 'iso-8859-1';
93
94    /**
95     * Create the SVG canvas.
96     *
97     * Parameters available:
98     *
99     * 'width' The width of the graph
100     *
101     * 'height' The height of the graph
102     *
103     * 'encoding' The encoding of the SVG document
104     *
105     * @param array $param Parameter array
106     */
107    function Image_Canvas_SVG($params)
108    {
109        parent::Image_Canvas($params);
110        $this->_reset();
111
112        if (isset($params['encoding'])) {
113            $this->_encoding = $params['encoding'];
114        }
115    }
116
117    /**
118     * Add a SVG "element" to the output
119     *
120     * @param string $element The element
121     * @access private
122     */
123    function _addElement($element, $params = array()) {
124        $elementdata = $this->_indent . $element . "\n";
125
126        if (isset($params['url'])) {
127            $url = $params['url'];
128            $target = (isset($params['target']) ? $params['target'] : false);
129            $alt = (isset($params['alt']) ? $params['alt'] : false);
130
131            $tags = '';
132            if (isset($params['htmltags'])) {
133                foreach ($params['htmltags'] as $key => $value) {
134                    $tags .= ' ';
135                    if (strpos($value, '"') >= 0) {
136                        $tags .= $key . '=\'' . $value . '\'';
137                    } else {
138                        $tags .= $key . '="' . $value . '"';
139                    }
140                }
141            }
142
143            $elementdata =
144                $this->_indent . '<a xlink:href="' . $url . '"' . ($target ? ' target="' . $target . '"' : '') . '>' . "\n" .
145                '    ' . $elementdata .
146                $this->_indent . '</a>' . "\n";
147        }
148
149
150        $this->_elements .= $elementdata;
151    }
152
153    /**
154     * Add a SVG "define" to the output
155     *
156     * @param string $def The define
157     * @access private
158     */
159    function _addDefine($def) {
160        $this->_defs .= '        ' . $def . "\n";
161    }
162
163    /**
164     * Get the color index for the RGB color
165     *
166     * @param int $color The color
167     * @return int A SVG compatible color
168     * @access private
169     */
170    function _color($color = false)
171    {
172        if ($color === false) {
173            return 'transparent';
174        } else {
175            $color = Image_Canvas_Color::color2RGB($color);
176            return 'rgb(' . $color[0] . ',' . $color[1] . ',' . $color[2] . ')';
177        }
178    }
179
180    /**
181     * Get the opacity for the RGB color
182     *
183     * @param int $color The color
184     * @return int A SVG compatible opacity value
185     * @access private
186     */
187    function _opacity($color = false)
188    {
189        if ($color === false) {
190            return false;
191        } else {
192            $color = Image_Canvas_Color::color2RGB($color);
193            if ($color[3] != 255) {
194                return sprintf('%0.1f', $color[3]/255);
195            } else {
196                return false;
197            }
198        }
199    }
200
201    /**
202     * Get the SVG applicable linestyle
203     *
204     * @param mixed $lineStyle The line style to return, false if the one
205     *   explicitly set
206     * @return mixed A SVG compatible linestyle
207     * @access private
208     */
209    function _getLineStyle($lineStyle = false)
210    {
211        $result = '';
212        if ($lineStyle === false) {
213            $lineStyle = $this->_lineStyle;
214        }
215
216        // TODO Linestyles (i.e. fx. dotted) does not work
217
218        if (($lineStyle != 'transparent') && ($lineStyle !== false)) {
219            $result = 'stroke-width:' . $this->_thickness . ';';
220            $result .= 'stroke:' .$this->_color($lineStyle) . ';';
221            if ($opacity = $this->_opacity($lineStyle)) {
222                $result .= 'stroke-opacity:' . $opacity . ';';
223            }
224        }
225        return $result;
226    }
227
228    /**
229     * Get the SVG applicable fillstyle
230     *
231     * @param mixed $fillStyle The fillstyle to return, false if the one
232     *   explicitly set
233     * @return mixed A SVG compatible fillstyle
234     * @access private
235     */
236    function _getFillStyle($fillStyle = false)
237    {
238        $result = '';
239        if ($fillStyle === false) {
240            $fillStyle = $this->_fillStyle;
241        }
242
243        if (is_array($fillStyle)) {
244            if ($fillStyle['type'] == 'gradient') {
245                $id = 'gradient_' . ($this->_id++);
246                $startColor = $this->_color($fillStyle['start']);
247                $endColor = $this->_color($fillStyle['end']);
248                $startOpacity = $this->_opacity($fillStyle['start']);
249                $endOpacity = $this->_opacity($fillStyle['end']);
250
251                switch ($fillStyle['direction']) {
252                case 'horizontal':
253                case 'horizontal_mirror':
254                    $x1 = '0%';
255                    $y1 = '0%';
256                    $x2 = '100%';
257                    $y2 = '0%';
258                    break;
259
260                case 'vertical':
261                case 'vertical_mirror':
262                    $x1 = '0%';
263                    $y1 = '100%';
264                    $x2 = '0%';
265                    $y2 = '0%';
266                    break;
267
268                case 'diagonal_tl_br':
269                    $x1 = '0%';
270                    $y1 = '0%';
271                    $x2 = '100%';
272                    $y2 = '100%';
273                    break;
274
275                case 'diagonal_bl_tr':
276                    $x1 = '0%';
277                    $y1 = '100%';
278                    $x2 = '100%';
279                    $y2 = '0%';
280                    break;
281
282                case 'radial':
283                    $cx = '50%';
284                    $cy = '50%';
285                    $r = '100%';
286                    $fx = '50%';
287                    $fy = '50%';
288                    break;
289
290                }
291
292                if ($fillStyle['direction'] == 'radial') {
293                    $this->_addDefine(
294                        '<radialGradient id="' . $id . '" cx="' .
295                            $cx .'" cy="' . $cy .'" r="' . $r .'" fx="' .
296                            $fx .'" fy="' . $fy .'">'
297                    );
298                    $this->_addDefine(
299                        '    <stop offset="0%" style="stop-color:' .
300                            $startColor. ';' . ($startOpacity ? 'stop-opacity:' .
301                            $startOpacity . ';' : ''). '"/>'
302                    );
303                    $this->_addDefine(
304                        '    <stop offset="100%" style="stop-color:' .
305                            $endColor. ';' . ($endOpacity ? 'stop-opacity:' .
306                            $endOpacity . ';' : ''). '"/>'
307                    );
308                    $this->_addDefine(
309                        '</radialGradient>'
310                    );
311                } elseif (($fillStyle['direction'] == 'vertical_mirror') ||
312                    ($fillStyle['direction'] == 'horizontal_mirror'))
313                {
314                    $this->_addDefine(
315                        '<linearGradient id="' . $id . '" x1="' .
316                            $x1 .'" y1="' . $y1 .'" x2="' . $x2 .'" y2="' .
317                            $y2 .'">'
318                    );
319                    $this->_addDefine(
320                        '    <stop offset="0%" style="stop-color:' .
321                            $startColor. ';' . ($startOpacity ? 'stop-opacity:' .
322                            $startOpacity . ';' : ''). '"/>'
323                    );
324                    $this->_addDefine(
325                        '    <stop offset="50%" style="stop-color:' .
326                            $endColor. ';' . ($endOpacity ? 'stop-opacity:' .
327                            $endOpacity . ';' : ''). '"/>'
328                    );
329                    $this->_addDefine(
330                        '    <stop offset="100%" style="stop-color:' .
331                            $startColor. ';' . ($startOpacity ? 'stop-opacity:' .
332                            $startOpacity . ';' : ''). '"/>'
333                    );
334                    $this->_addDefine(
335                        '</linearGradient>'
336                    );
337                } else {
338                    $this->_addDefine(
339                        '<linearGradient id="' . $id . '" x1="' .
340                            $x1 .'" y1="' . $y1 .'" x2="' . $x2 .'" y2="' .
341                            $y2 .'">'
342                    );
343                    $this->_addDefine(
344                        '    <stop offset="0%" style="stop-color:' .
345                            $startColor. ';' . ($startOpacity ? 'stop-opacity:' .
346                            $startOpacity . ';' : ''). '"/>'
347                    );
348                    $this->_addDefine(
349                        '    <stop offset="100%" style="stop-color:' .
350                            $endColor. ';' . ($endOpacity ? 'stop-opacity:' .
351                            $endOpacity . ';' : ''). '"/>'
352                    );
353                    $this->_addDefine(
354                        '</linearGradient>'
355                    );
356                }
357
358                return 'fill:url(#' . $id . ');';
359            }
360        } elseif (($fillStyle != 'transparent') && ($fillStyle !== false)) {
361            $result = 'fill:' . $this->_color($fillStyle) . ';';
362            if ($opacity = $this->_opacity($fillStyle)) {
363                $result .= 'fill-opacity:' . $opacity . ';';
364            }
365            return $result;
366        } else {
367            return 'fill:none;';
368        }
369    }
370
371    /**
372     * Sets an image that should be used for filling
373     *
374     * @param string $filename The filename of the image to fill with
375     */
376    function setFillImage($filename)
377    {
378    }
379
380    /**
381     * Sets a gradient fill
382     *
383     * @param array $gradient Gradient fill options
384     */
385    function setGradientFill($gradient)
386    {
387        $this->_fillStyle = $gradient;
388        $this->_fillStyle['type'] = 'gradient';
389    }
390
391    /**
392     * Sets the font options.
393     *
394     * The $font array may have the following entries:
395     * 'type' = 'ttf' (TrueType) or omitted for default<br>
396     * If 'type' = 'ttf' then the following can be specified<br>
397     * 'size' = size in pixels<br>
398     * 'angle' = the angle with which to write the text
399     * 'file' = the .ttf file (either the basename, filename or full path)
400     *
401     * @param array $font The font options.
402     */
403    function setFont($fontOptions)
404    {
405        parent::setFont($fontOptions);
406        if (!isset($this->_font['size'])) {
407            $this->_font['size'] = 10;
408        }
409    }
410
411    /**
412     * Parameter array:
413     * 'x0': int X start point
414     * 'y0': int Y start point
415     * 'x1': int X end point
416     * 'y1': int Y end point
417     * 'color': mixed [optional] The line color
418     * @param array $params Parameter array
419     */
420    function line($params)
421    {
422        $x0 = $this->_getX($params['x0']);
423        $y0 = $this->_getY($params['y0']);
424        $x1 = $this->_getX($params['x1']);
425        $y1 = $this->_getY($params['y1']);
426        $color = (isset($params['color']) ? $params['color'] : false);
427
428        $attrs = (isset($params['attrs']) && is_array($params['attrs'])) ? $this->_getAttributes($params['attrs']) : null;
429
430        $style = $this->_getLineStyle($color) . $this->_getFillStyle('transparent');
431        if ($style != '') {
432            $this->_addElement(
433                '<line ' .
434                    'x1="' . round($x0) . '" ' .
435                    'y1="' . round($y0) . '" ' .
436                    'x2="' . round($x1) . '" ' .
437                    'y2="' . round($y1) . '" ' .
438                    'style="' . $style . '"' .
439                    ($attrs ? ' ' . $attrs : '') .
440                '/>',
441                $params
442            );
443        }
444        parent::line($params);
445    }
446
447    /**
448     * Parameter array:
449     * 'connect': bool [optional] Specifies whether the start point should be
450     *   connected to the endpoint (closed polygon) or not (connected line)
451     * 'fill': mixed [optional] The fill color
452     * 'line': mixed [optional] The line color
453     * @param array $params Parameter array
454     */
455    function polygon($params = array())
456    {
457        $connectEnds = (isset($params['connect']) ? $params['connect'] : false);
458        $fillColor = (isset($params['fill']) ? $params['fill'] : false);
459        $lineColor = (isset($params['line']) ? $params['line'] : false);
460
461        if (!$connectEnds) {
462            $fillColor = 'transparent';
463        }
464        $style = $this->_getLineStyle($lineColor) . $this->_getFillStyle($fillColor);
465
466        $attrs = (isset($params['attrs']) && is_array($params['attrs'])) ? $this->_getAttributes($params['attrs']) : null;
467
468        $first = true;
469        $spline = false;
470        $lastpoint = false;
471        foreach($this->_polygon as $point) {
472            if ($first) {
473                $points = 'M';
474            } elseif (!$spline) {
475                $points .= ' L';
476            }
477
478            if (($spline) && ($lastpoint !== false)) {
479                $points .= ' ' .round($lastpoint['P1X']) . ',' . round($lastpoint['P1Y']) . ' ' .
480                           round($lastpoint['P2X']) . ',' . round($lastpoint['P2Y']);
481            }
482
483            $points .= ' ' . round($point['X']) . ',' . round($point['Y']);
484
485            if ((isset($point['P1X'])) && (isset($point['P1Y'])) &&
486                (isset($point['P2X'])) && (isset($point['P2Y'])))
487            {
488                if (($first) || (!$spline)) {
489                    $points .= ' C';
490                }
491                $lastpoint = $point;
492                $spline = true;
493            } else {
494                $spline = false;
495            }
496            $first = false;
497        }
498        if ($connectEnds) {
499            $points .= ' Z';
500        }
501        $this->_addElement(
502            '<path ' .
503                 'd="' . $points . '" ' .
504                 'style="' . $style . '"' .
505                 ($attrs ? ' ' . $attrs : '') .
506            '/>',
507            $params
508        );
509
510        parent::polygon($params);
511    }
512
513    /**
514     * Draw a rectangle
515     *
516     * Parameter array:
517     * 'x0': int X start point
518     * 'y0': int Y start point
519     * 'x1': int X end point
520     * 'y1': int Y end point
521     * 'fill': mixed [optional] The fill color
522     * 'line': mixed [optional] The line color
523     * @param array $params Parameter array
524     */
525    function rectangle($params)
526    {
527        $x0 = min($this->_getX($params['x0']), $this->_getX($params['x1']));
528        $y0 = min($this->_getY($params['y0']), $this->_getY($params['y1']));
529        $x1 = max($this->_getX($params['x0']), $this->_getX($params['x1']));
530        $y1 = max($this->_getY($params['y0']), $this->_getY($params['y1']));
531        $fillColor = (isset($params['fill']) ? $params['fill'] : false);
532        $lineColor = (isset($params['line']) ? $params['line'] : false);
533
534        $attrs = (isset($params['attrs']) && is_array($params['attrs'])) ? $this->_getAttributes($params['attrs']) : null;
535
536        $style = $this->_getLineStyle($lineColor) . $this->_getFillStyle($fillColor);
537        if ($style != '') {
538            $this->_addElement(
539                '<rect ' .
540                    'x="' . round($x0) . '" ' .
541                    'y="' . round($y0) . '" ' .
542                    'width="' . round(abs($x1 - $x0)) . '" ' .
543                    'height="' . round(abs($y1 - $y0)) . '" ' .
544                    'style="' . $style . '"' .
545                    ($attrs ? ' ' . $attrs : '') .
546                '/>',
547                $params
548            );
549        }
550        parent::rectangle($params);
551    }
552
553    /**
554     * Draw an ellipse
555     *
556     * Parameter array:
557     * 'x': int X center point
558     * 'y': int Y center point
559     * 'rx': int X radius
560     * 'ry': int Y radius
561     * 'fill': mixed [optional] The fill color
562     * 'line': mixed [optional] The line color
563     * @param array $params Parameter array
564     */
565    function ellipse($params)
566    {
567        $x = $this->_getX($params['x']);
568        $y = $this->_getY($params['y']);
569        $rx = $this->_getX($params['rx']);
570        $ry = $this->_getY($params['ry']);
571        $fillColor = (isset($params['fill']) ? $params['fill'] : false);
572        $lineColor = (isset($params['line']) ? $params['line'] : false);
573
574        $attrs = (isset($params['attrs']) && is_array($params['attrs'])) ? $this->_getAttributes($params['attrs']) : null;
575
576        $style = $this->_getLineStyle($lineColor) . $this->_getFillStyle($fillColor);
577        if ($style != '') {
578            $this->_addElement(
579                '<ellipse ' .
580                    'cx="' . round($x) . '" ' .
581                    'cy="' . round($y) . '" ' .
582                    'rx="' . round($rx) . '" ' .
583                    'ry="' . round($ry) . '" ' .
584                    'style="' . $style . '"' .
585                    ($attrs ? ' ' . $attrs : '') .
586                '/>',
587                $params
588            );
589        }
590        parent::ellipse($params);
591    }
592
593    /**
594     * Draw a pie slice
595     *
596     * Parameter array:
597     * 'x': int X center point
598     * 'y': int Y center point
599     * 'rx': int X radius
600     * 'ry': int Y radius
601     * 'v1': int The starting angle (in degrees)
602     * 'v2': int The end angle (in degrees)
603     * 'srx': int [optional] Starting X-radius of the pie slice (i.e. for a doughnut)
604     * 'sry': int [optional] Starting Y-radius of the pie slice (i.e. for a doughnut)
605     * 'fill': mixed [optional] The fill color
606     * 'line': mixed [optional] The line color
607     * @param array $params Parameter array
608     */
609    function pieslice($params)
610    {
611        $x = $this->_getX($params['x']);
612        $y = $this->_getY($params['y']);
613        $rx = $this->_getX($params['rx']);
614        $ry = $this->_getY($params['ry']);
615        $v1 = $this->_getX($params['v1']);
616        $v2 = $this->_getY($params['v2']);
617        $srx = (isset($params['srx']) ? $this->_getX($params['srx']) : false);
618        $sry = (isset($params['sry']) ? $this->_getX($params['sry']) : false);
619        $fillColor = (isset($params['fill']) ? $params['fill'] : false);
620        $lineColor = (isset($params['line']) ? $params['line'] : false);
621
622        $attrs = (isset($params['attrs']) && is_array($params['attrs'])) ? $this->_getAttributes($params['attrs']) : null;
623
624        $dv = max($v2, $v1) - min($v2, $v1);
625        if ($dv >= 360) {
626            $this->ellipse($params);
627        }
628        else {
629            $style = $this->_getLineStyle($lineColor) . $this->_getFillStyle($fillColor);
630            if ($style != '') {
631                $x1 = ($x + $rx * cos(deg2rad(min($v1, $v2) % 360)));
632                $y1 = ($y + $ry * sin(deg2rad(min($v1, $v2) % 360)));
633                $x2 = ($x + $rx * cos(deg2rad(max($v1, $v2) % 360)));
634                $y2 = ($y + $ry * sin(deg2rad(max($v1, $v2) % 360)));
635                $this->_addElement(
636                    '<path d="' .
637                        'M' . round($x) . ',' . round($y) . ' ' .
638                        'L' . round($x1) . ',' . round($y1) . ' ' .
639                        'A' . round($rx) . ',' . round($ry) . ($dv > 180 ? ' 0 1,1 ' : ' 0 0,1 ') .
640                              round($x2) . ',' . round($y2) . ' ' .
641                        'Z" ' .
642                        'style="' . $style . '"' .
643                        ($attrs ? ' ' . $attrs : '') .
644                    '/>',
645                    $params
646                );
647            }
648
649            parent::pieslice($params);
650        }
651    }
652
653    /**
654     * Get the width of a text,
655     *
656     * @param string $text The text to get the width of
657     * @return int The width of the text
658     */
659    function textWidth($text)
660    {
661        if ((isset($this->_font['vertical'])) && ($this->_font['vertical'])) {
662            return $this->_font['size'];
663        } else {
664            return round($this->_font['size'] * 0.7 * strlen($text));
665        }
666    }
667
668    /**
669     * Get the height of a text,
670     *
671     * @param string $text The text to get the height of
672     * @return int The height of the text
673     */
674    function textHeight($text)
675    {
676        if ((isset($this->_font['vertical'])) && ($this->_font['vertical'])) {
677            return round($this->_font['size'] * 0.7 * strlen($text));
678        } else {
679            return $this->_font['size'];
680        }
681    }
682
683    /**
684     * Writes text
685     *
686     * Parameter array:
687     * 'x': int X-point of text
688     * 'y': int Y-point of text
689     * 'text': string The text to add
690     * 'alignment': array [optional] Alignment
691     * 'color': mixed [optional] The color of the text
692     */
693    function addText($params)
694    {
695        $x = $this->_getX($params['x']);
696        $y = $this->_getY($params['y']);
697        $text = $params['text'];
698        $color = (isset($params['color']) ? $params['color'] : false);
699        $alignment = (isset($params['alignment']) ? $params['alignment'] : false);
700
701        $attrs = (isset($params['attrs']) && is_array($params['attrs'])) ? $this->_getAttributes($params['attrs']) : null;
702
703        $textHeight = $this->textHeight($text);
704
705        if (!is_array($alignment)) {
706            $alignment = array('vertical' => 'top', 'horizontal' => 'left');
707        }
708
709        if (!isset($alignment['vertical'])) {
710            $alignment['vertical'] = 'top';
711        }
712
713        if (!isset($alignment['horizontal'])) {
714            $alignment['horizontal'] = 'left';
715        }
716
717        $align = '';
718
719        if ((isset($this->_font['vertical'])) && ($this->_font['vertical'])) {
720//            $align .= 'writing-mode: tb-rl;';
721
722            if ($alignment['vertical'] == 'bottom') {
723                $align .= 'text-anchor:end;';
724                //$y = $y + $textHeight;
725            } elseif ($alignment['vertical'] == 'center') {
726                //$y = $y + ($textHeight / 2);
727                $align .= 'text-anchor:middle;';
728            }
729        } else {
730            if ($alignment['horizontal'] == 'right') {
731                $align .= 'text-anchor:end;';
732            } elseif ($alignment['horizontal'] == 'center') {
733                $align .= 'text-anchor:middle;';
734            }
735
736            if ($alignment['vertical'] == 'top') {
737                $y = $y + $textHeight;
738            } elseif ($alignment['vertical'] == 'center') {
739                $y = $y + ($textHeight / 2);
740            }
741        }
742
743        if (($color === false) && (isset($this->_font['color']))) {
744            $color = $this->_font['color'];
745        }
746
747        $textColor = $this->_color($color);
748        $textOpacity = $this->_opacity($color);
749
750        $this->_addElement(
751            '<g transform="translate(' . round($x) . ', ' . round($y) . ')">' . "\n" .
752            $this->_indent . '    <text ' .
753                'x="0" ' .
754                'y="0" ' .
755                (isset($this->_font['angle']) && ($this->_font['angle'] > 0) ?
756                    'transform="rotate(' . (($this->_font['angle'] + 180) % 360) . ')" ' :
757                    ''
758                ) .
759                'style="' .
760                (isset($this->_font['name']) ?
761                    'font-family:' . $this->_font['name'] . ';' : '') .
762                        'font-size:' . $this->_font['size'] . 'px;fill:' .
763                        $textColor . ($textOpacity ? ';fill-opacity:' .
764                        $textOpacity :
765                    ''
766                ) . ';' . $align . '"' .
767                ($attrs ? ' ' . $attrs : '') .
768                '>' .
769                htmlspecialchars($text) .
770            '</text>' . "\n" .
771            $this->_indent . '</g>',
772            $params
773        );
774        parent::addText($params);
775    }
776
777    /**
778     * Overlay image
779     *
780     * Parameter array:
781     * 'x': int X-point of overlayed image
782     * 'y': int Y-point of overlayed image
783     * 'filename': string The filename of the image to overlay
784     * 'width': int [optional] The width of the overlayed image (resizing if possible)
785     * 'height': int [optional] The height of the overlayed image (resizing if possible)
786     * 'alignment': array [optional] Alignment
787     */
788    function image($params)
789    {
790        $x = $this->_getX($params['x']);
791        $y = $this->_getY($params['y']);
792        $filename = $params['filename'];
793
794        $attrs = (isset($params['attrs']) && is_array($params['attrs'])) ? $this->_getAttributes($params['attrs']) : null;
795
796        list($width, $height, $type, $attr) = getimagesize($filename);
797        $width = (isset($params['width']) ? $params['width'] : $width);
798        $height = (isset($params['height']) ? $params['height'] : $height);
799        $alignment = (isset($params['alignment']) ? $params['alignment'] : false);
800
801        $file = fopen($filename, 'rb');
802        $filedata = fread($file, filesize($filename));
803        fclose($file);
804
805        $data = 'data:' . image_type_to_mime_type($type) . ';base64,' . base64_encode($filedata);
806
807        $this->_addElement(
808            '<image xlink:href="' . $data . '" x="' . $x . '" y="' . $y . '"' .
809                ($width ? ' width="' . $width . '"' : '') .
810                ($height ? ' height="' . $height . '"' : '') .
811                ($attrs ? ' ' . $attrs : '') .
812            ' preserveAspectRatio="none"/>',
813            $params
814        );
815        parent::image($params);
816    }
817
818    /**
819     * Start a group.
820     *
821     * What this does, depends on the canvas/format.
822     *
823     * @param string $name The name of the group
824     */
825    function startGroup($name = false)
826    {
827        $name = strtolower(str_replace(' ', '_', $name));
828        if (in_array($name, $this->_groupIDs)) {
829            $name .= $this->_id;
830            $this->_id++;
831        }
832        $this->_groupIDs[] = $name;
833        $this->_addElement('<g id="' . htmlspecialchars($name) . '">');
834        $this->_indent .= '    ';
835    }
836
837    /**
838     * End the "current" group.
839     *
840     * What this does, depends on the canvas/format.
841     */
842    function endGroup()
843    {
844        $this->_indent = substr($this->_indent, 0, -4);
845        $this->_addElement('</g>');
846    }
847
848    /**
849     * Output the result of the canvas
850     *
851     * @param array $param Parameter array
852     */
853    function show($param = false)
854    {
855        parent::show($param);
856
857        $attrs = (isset($param['attrs']) && is_array($param['attrs'])) ? $this->_getAttributes($param['attrs']) : null;
858
859        $output = $this->getData($param);
860
861        header('Content-Type: image/svg+xml');
862        header('Content-Disposition: inline; filename = "' . basename($_SERVER['PHP_SELF'], '.php') . '.svg"');
863        print $output;
864    }
865
866    /**
867     * Output the result of the canvas
868     *
869     * @param array $param Parameter array
870     */
871    function save($param = false)
872    {
873        parent::save($param);
874
875        $output = $this->getData($param);
876
877        $file = fopen($param['filename'], 'w+');
878        fwrite($file, $output);
879        fclose($file);
880    }
881
882
883    /**
884     * Get SVG data
885     *
886     * @param array $param Parameter array
887     *
888     * @return string
889     */
890    function getData($param = false)
891    {
892        $attrs = (isset($param['attrs']) && is_array($param['attrs'])) ? $this->_getAttributes($param['attrs']) : null;
893
894        return '<?xml version="1.0" encoding="'. $this->_encoding . '"?>' . "\n" .
895            '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN"' . "\n\t" .
896            ' "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">' . "\n" .
897            '<svg width="' . $this->_width . '" height="' . $this->_height .
898                '" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' .
899                ($attrs ? ' ' . $attrs : '') .
900                '>' . "\n" .
901            ($this->_defs ?
902                '    <defs>' . "\n" .
903                $this->_defs .
904                '    </defs>' . "\n" :
905                ''
906            ) .
907            $this->_elements .
908            '</svg>';
909     }
910
911    /**
912     * Set clipping to occur
913     *
914     * Parameter array:
915     *
916     * 'x0': int X point of Upper-left corner
917     * 'y0': int X point of Upper-left corner
918     * 'x1': int X point of lower-right corner
919     * 'y1': int Y point of lower-right corner
920     */
921    function setClipping($params = false)
922    {
923        if ($params === false) {
924            $this->_addElement('</g>');
925        }
926        else {
927            $group = "clipping_" . $this->_id;
928            $this->_id++;
929            $this->_addElement('<g clip-path="url(#' . $group . ')">');
930
931            $this->_addDefine('<clipPath id="' . $group . '">');
932            $this->_addDefine('    <path d="' .
933                'M' . $this->_getX($params['x0']) . ' ' . $this->_getY($params['y0']) .
934                ' H' . $this->_getX($params['x1']) .
935                ' V' . $this->_getY($params['y1']) .
936                ' H' . $this->_getX($params['x0']) .
937                ' Z"/>');
938            $this->_addDefine('</clipPath>');
939        }
940    }
941
942    /**
943     * Get a canvas specific HTML tag.
944     *
945     * This method implicitly saves the canvas to the filename in the
946     * filesystem path specified and parses it as URL specified by URL path
947     *
948     * Parameter array:
949     * 'filename': string
950     * 'filepath': string Path to the file on the file system. Remember the final slash
951     * 'urlpath': string Path to the file available through an URL. Remember the final slash
952     * 'width': int The width in pixels
953     * 'height': int The height in pixels
954     */
955    function toHtml($params)
956    {
957        parent::toHtml($params);
958        return '<embed src="' . $params['urlpath'] . $params['filename'] . '" width=' . $params['width'] . ' height=' . $params['height'] . ' type="image/svg+xml">';
959    }
960
961    /**
962     * Converts array of attributes to string
963     *
964     * @param array $attrs Attributes array
965     * @return array
966     */
967    function _getAttributes($attrs)
968    {
969        $string = '';
970
971        foreach ($attrs as $key => $value) {
972            $string .= ' ' . $key . '="' . $value . '"';
973        }
974
975        return $string;
976    }
977}
978
979?>
980