1<?php
2
3/**
4 * EasySVG - Generate SVG from PHP
5 * @author Simon Tarchichi <kartsims@gmail.com>
6 * @version 0.1b
7 *
8 * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
9 * @see http://stackoverflow.com/questions/14684846/flattening-svg-matrix-transforms-in-inkscape
10 * @see http://stackoverflow.com/questions/7742148/how-to-convert-text-to-svg-paths
11 */
12class EasySVG
13{
14
15    protected $font;
16    protected $svg;
17
18    public function __construct()
19    {
20        // default font data
21        $this->font = new stdClass;
22        $this->font->id = '';
23        $this->font->horizAdvX = 0;
24        $this->font->unitsPerEm = 0;
25        $this->font->ascent = 0;
26        $this->font->descent = 0;
27        $this->font->glyphs = array();
28        $this->font->size = 20;
29        $this->font->color = '';
30        $this->font->lineHeight = 1;
31        $this->font->letterSpacing = 0;
32
33        $this->clearSVG();
34    }
35
36    public function clearSVG()
37    {
38        $this->svg = new SimpleXMLElement('<svg></svg>');
39    }
40
41    /**
42     * Function takes UTF-8 encoded string and returns unicode number for every character.
43     * @param string $str
44     * @return string
45     */
46    private function _utf8ToUnicode($str)
47    {
48        $unicode = array();
49        $values = array();
50        $lookingFor = 1;
51
52        for ($i = 0; $i < strlen($str); $i++) {
53            $thisValue = ord($str[$i]);
54            if ($thisValue < 128) {
55                $unicode[] = $thisValue;
56            } else {
57                if (count($values) == 0) $lookingFor = ($thisValue < 224) ? 2 : 3;
58                $values[] = $thisValue;
59                if (count($values) == $lookingFor) {
60                    $number = ($lookingFor == 3) ?
61                        (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64) :
62                        (($values[0] % 32) * 64) + ($values[1] % 64);
63
64                    $unicode[] = $number;
65                    $values = array();
66                    $lookingFor = 1;
67                }
68            }
69        }
70
71        return $unicode;
72    }
73
74    /**
75     * Set font params (short-hand method)
76     * @param string $filepath
77     * @param integer $size
78     * @param string $color
79     */
80    public function setFont($filepath, $size, $color)
81    {
82        $this->setFontSVG($filepath);
83        $this->setFontSize($size);
84        $this->setFontColor($color);
85    }
86
87    /**
88     * Set font size for display
89     * @param int $size
90     * @return void
91     */
92    public function setFontSize($size)
93    {
94        $this->font->size = $size;
95    }
96
97    /**
98     * Set font color
99     * @param string $color
100     * @return void
101     */
102    public function setFontColor($color)
103    {
104        $this->font->color = $color;
105    }
106
107    /**
108     * Set the line height from default (1) to custom value
109     * @param float $value
110     * @return void
111     */
112    public function setLineHeight($value)
113    {
114        $this->font->lineHeight = $value;
115    }
116
117    /**
118     * Set the letter spacing from default (0) to custom value
119     * @param float $value
120     * @return void
121     */
122    public function setLetterSpacing($value)
123    {
124        $this->font->letterSpacing = $value;
125    }
126
127    /**
128     * Function takes path to SVG font (local path) and processes its xml
129     * to get path representation of every character and additional
130     * font parameters
131     * @param string $filepath
132     * @return void
133     */
134    public function setFontSVG($filepath)
135    {
136        $this->font->glyphs = array();
137        $z = new XMLReader;
138        $z->open($filepath);
139
140        // move to the first <product /> node
141        while ($z->read()) {
142            $name = $z->name;
143
144            if ($z->nodeType == XMLReader::ELEMENT) {
145                if ($name == 'font') {
146                    $this->font->id = $z->getAttribute('id');
147                    $this->font->horizAdvX = $z->getAttribute('horiz-adv-x');
148                }
149
150                if ($name == 'font-face') {
151                    $this->font->unitsPerEm = $z->getAttribute('units-per-em');
152                    $this->font->ascent = $z->getAttribute('ascent');
153                    $this->font->descent = $z->getAttribute('descent');
154                }
155
156                if ($name == 'glyph') {
157                    $unicode = $z->getAttribute('unicode');
158                    $unicode = $this->_utf8ToUnicode($unicode);
159
160                    if (isset($unicode[0])) {
161                        $unicode = $unicode[0];
162
163                        $this->font->glyphs[$unicode] = new stdClass();
164                        $this->font->glyphs[$unicode]->horizAdvX = $z->getAttribute('horiz-adv-x');
165                        if (empty($this->font->glyphs[$unicode]->horizAdvX)) {
166                            $this->font->glyphs[$unicode]->horizAdvX = $this->font->horizAdvX;
167                        }
168                        $this->font->glyphs[$unicode]->d = $z->getAttribute('d');
169
170                        // save em value for letter spacing (109 is unicode for the letter 'm')
171                        if ($unicode == '109') {
172                            $this->font->em = $this->font->glyphs[$unicode]->horizAdvX;
173                        }
174                    }
175                }
176            }
177        }
178    }
179
180    /**
181     * Add a path to the SVG
182     * @param string $def
183     * @param array $attributes
184     * @return SimpleXMLElement
185     */
186    public function addPath($def, $attributes = array())
187    {
188        $path = $this->svg->addChild('path');
189        foreach ($attributes as $key => $value) {
190            $path->addAttribute($key, $value);
191        }
192        $path->addAttribute('d', $def);
193        return $path;
194    }
195
196    /**
197     * Add a text to the SVG
198     * @param string $def
199     * @param float $x
200     * @param float $y
201     * @param array $attributes
202     * @return SimpleXMLElement
203     */
204    public function addText($text, $x = 0, $y = 0, $attributes = array())
205    {
206        $def = $this->textDef($text);
207
208        if ($x != 0 || $y != 0) {
209            $def = $this->defTranslate($def, $x, $y);
210        }
211
212        if ($this->font->color) {
213            $attributes['fill'] = $this->font->color;
214        }
215
216        return $this->addPath($def, $attributes);
217    }
218
219    /**
220     * Function takes UTF-8 encoded string and size, returns xml for SVG paths representing this string.
221     * @param string $text UTF-8 encoded text
222     * @return string xml for text converted into SVG paths
223     */
224    public function textDef($text)
225    {
226        $def = array();
227
228        $horizAdvX = 0;
229        $horizAdvY = $this->font->ascent + $this->font->descent;
230        $fontSize = floatval($this->font->size) / $this->font->unitsPerEm;
231        $text = $this->_utf8ToUnicode($text);
232
233        for ($i = 0; $i < count($text); $i++) {
234
235            $letter = $text[$i];
236
237            // line break support (10 is unicode for linebreak)
238            if ($letter == 10) {
239                $horizAdvX = 0;
240                $horizAdvY += $this->font->lineHeight * ($this->font->ascent + $this->font->descent);
241                continue;
242            }
243
244            // extract character definition
245            $d = $this->font->glyphs[$letter]->d;
246
247            // transform typo from original SVG format to straight display
248            $d = $this->defScale($d, $fontSize, -$fontSize);
249            $d = $this->defTranslate($d, $horizAdvX, $horizAdvY * $fontSize * 2);
250
251            $def[] = $d;
252
253            // next letter's position
254            $horizAdvX += $this->font->glyphs[$letter]->horizAdvX * $fontSize + $this->font->em * $this->font->letterSpacing * $fontSize;
255        }
256        return implode(' ', $def);
257    }
258
259    /**
260     * Function takes UTF-8 encoded string and size, returns width and height of the whole text
261     * @param string $text UTF-8 encoded text
262     * @return array ($width, $height)
263     */
264    public function textDimensions($text)
265    {
266        $def = array();
267
268        $fontSize = floatval($this->font->size) / $this->font->unitsPerEm;
269        $text = $this->_utf8ToUnicode($text);
270
271        $lineWidth = 0;
272        $lineHeight = ($this->font->ascent + $this->font->descent) * $fontSize * 2;
273
274        $width = 0;
275        $height = $lineHeight;
276
277        for ($i = 0; $i < count($text); $i++) {
278
279            $letter = $text[$i];
280
281            // line break support (10 is unicode for linebreak)
282            if ($letter == 10) {
283                $width = $lineWidth > $width ? $lineWidth : $width;
284                $height += $lineHeight * $this->font->lineHeight;
285                $lineWidth = 0;
286                continue;
287            }
288
289            $lineWidth += $this->font->glyphs[$letter]->horizAdvX * $fontSize + $this->font->em * $this->font->letterSpacing * $fontSize;
290        }
291
292        // only keep the widest line's width
293        $width = $lineWidth > $width ? $lineWidth : $width;
294
295        return array($width, $height);
296    }
297
298    /**
299     * Function takes unicode character and returns the UTF-8 equivalent
300     * @param string $str
301     * @return string
302     */
303    public function unicodeDef($unicode)
304    {
305
306        $horizAdvY = $this->font->ascent + $this->font->descent;
307        $fontSize = floatval($this->font->size) / $this->font->unitsPerEm;
308
309        // extract character definition
310        $d = $this->font->glyphs[hexdec($unicode)]->d;
311
312        // transform typo from original SVG format to straight display
313        $d = $this->defScale($d, $fontSize, -$fontSize);
314        $d = $this->defTranslate($d, 0, $horizAdvY * $fontSize * 2);
315
316        return $d;
317    }
318
319    /**
320     * Returns the character width, as set in the font file
321     * @param string $str
322     * @param boolean $is_unicode
323     * @return float
324     */
325    public function characterWidth($char, $is_unicode = false)
326    {
327        if ($is_unicode) {
328            $letter = hexdec($char);
329        } else {
330            $letter = $this->_utf8ToUnicode($char);
331        }
332
333        if (!isset($this->font->glyphs[$letter])) {
334            return null;
335        }
336
337        $fontSize = floatval($this->font->size) / $this->font->unitsPerEm;
338        return $this->font->glyphs[$letter]->horizAdvX * $fontSize;
339    }
340
341    /**
342     * Applies a translate transformation to definition
343     * @param string $def definition
344     * @param float $x
345     * @param float $y
346     * @return string
347     */
348    public function defTranslate($def, $x = 0, $y = 0)
349    {
350        return $this->defApplyMatrix($def, array(1, 0, 0, 1, $x, $y));
351    }
352
353    /**
354     * Applies a translate transformation to definition
355     * @param string $def Definition
356     * @param integer $angle Rotation angle (degrees)
357     * @param integer $x X coordinate of rotation center
358     * @param integer $y Y coordinate of rotation center
359     * @return string
360     */
361    public function defRotate($def, $angle, $x = 0, $y = 0)
362    {
363        if ($x == 0 && $y == 0) {
364            $angle = deg2rad($angle);
365            return $this->defApplyMatrix($def, array(cos($angle), sin($angle), -sin($angle), cos($angle), 0, 0));
366        }
367
368        // rotate by a given point
369        $def = $this->defTranslate($def, $x, $y);
370        $def = $this->defRotate($def, $angle);
371        $def = $this->defTranslate($def, -$x, -$y);
372        return $def;
373    }
374
375    /**
376     * Applies a scale transformation to definition
377     * @param string $def definition
378     * @param integer $x
379     * @param integer $y
380     * @return string
381     */
382    public function defScale($def, $x = 1, $y = 1)
383    {
384        return $this->defApplyMatrix($def, array($x, 0, 0, $y, 0, 0));
385    }
386
387    /**
388     * Calculates the new definition with the matrix applied
389     * @param string $def
390     * @param array $matrix
391     * @return string
392     */
393    public function defApplyMatrix($def, $matrix)
394    {
395
396        // if there are several shapes in this definition, do the operation for each
397        preg_match_all('/M[^zZ]*[zZ]/', $def, $shapes);
398        $shapes = $shapes[0];
399        if (count($shapes) > 1) {
400            foreach ($shapes as &$shape) {
401                $shape = $this->defApplyMatrix($shape, $matrix);
402            }
403            return implode(' ', $shapes);
404        }
405
406        preg_match_all('/[a-zA-Z]+[^a-zA-Z]*/', $def, $instructions);
407        $instructions = $instructions[0];
408
409        $return = '';
410        foreach ($instructions as &$instruction) {
411            $i = preg_replace('/[^a-zA-Z]*/', '', $instruction);
412            preg_match_all('/\-?[0-9\.]+/', $instruction, $coords);
413            $coords = $coords[0];
414
415            if (empty($coords)) {
416                continue;
417            }
418
419            $new_coords = array();
420            while (count($coords) > 0) {
421
422                // do the matrix calculation stuff
423                list($a, $b, $c, $d, $e, $f) = $matrix;
424
425                // exception for relative instruction
426                if (preg_match('/[a-z]/', $i)) {
427                    $e = 0;
428                    $f = 0;
429                }
430
431                // convert horizontal lineto (relative)
432                if ($i == 'h') {
433                    $i = 'l';
434                    $x = floatval(array_shift($coords));
435                    $y = 0;
436
437                    // add new point's coordinates
438                    $current_point = array(
439                        $a * $x + $c * $y + $e,
440                        $b * $x + $d * $y + $f,
441                    );
442                    $new_coords = array_merge($new_coords, $current_point);
443                } // convert vertical lineto (relative)
444                elseif ($i == 'v') {
445                    $i = 'l';
446                    $x = 0;
447                    $y = floatval(array_shift($coords));
448
449                    // add new point's coordinates
450                    $current_point = array(
451                        $a * $x + $c * $y + $e,
452                        $b * $x + $d * $y + $f,
453                    );
454                    $new_coords = array_merge($new_coords, $current_point);
455                } // convert quadratic bezier curve (relative)
456                elseif ($i == 'q') {
457                    $x = floatval(array_shift($coords));
458                    $y = floatval(array_shift($coords));
459
460                    // add new point's coordinates
461                    $current_point = array(
462                        $a * $x + $c * $y + $e,
463                        $b * $x + $d * $y + $f,
464                    );
465                    $new_coords = array_merge($new_coords, $current_point);
466
467                    // same for 2nd point
468                    $x = floatval(array_shift($coords));
469                    $y = floatval(array_shift($coords));
470
471                    // add new point's coordinates
472                    $current_point = array(
473                        $a * $x + $c * $y + $e,
474                        $b * $x + $d * $y + $f,
475                    );
476                    $new_coords = array_merge($new_coords, $current_point);
477                }
478
479                // every other commands
480                // @TODO: handle 'a,c,s' (elliptic arc curve) commands
481                // cf. http://www.w3.org/TR/SVG/paths.html#PathDataCurveCommands
482                else {
483                    $x = floatval(array_shift($coords));
484                    $y = floatval(array_shift($coords));
485
486                    // add new point's coordinates
487                    $current_point = array(
488                        $a * $x + $c * $y + $e,
489                        $b * $x + $d * $y + $f,
490                    );
491                    $new_coords = array_merge($new_coords, $current_point);
492                }
493
494            }
495
496            $instruction = $i . implode(',', $new_coords);
497
498            // remove useless commas
499            $instruction = preg_replace('/,\-/', '-', $instruction);
500        }
501
502        return implode('', $instructions);
503    }
504
505
506
507    /**
508     *
509     * Short-hand methods
510     *
511     */
512
513    /**
514     * Return full SVG XML
515     * @return string
516     */
517    public function asXML()
518    {
519        return $this->svg->asXML();
520    }
521
522    /**
523     * Adds an attribute to the SVG
524     * @param string $key
525     * @param string $value
526     */
527    public function addAttribute($key, $value)
528    {
529        return $this->svg->addAttribute($key, $value);
530    }
531}
532