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