xref: /plugin/captcha/EasySVG.php (revision 09b1e97e3cb9f2c4be8ca729baa9d49a3ba58ba1)
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 + $this->font->em * $this->font->letterSpacing * $fontSize;
254        }
255        return implode(' ', $def);
256    }
257
258    /**
259     * Function takes UTF-8 encoded string and size, returns width and height of the whole text
260     * @param string $text UTF-8 encoded text
261     * @return array ($width, $height)
262     */
263    public function textDimensions($text)
264    {
265        $fontSize = (float) $this->font->size / $this->font->unitsPerEm;
266        $text = $this->_utf8ToUnicode($text);
267
268        $lineWidth = 0;
269        $lineHeight = ($this->font->ascent + $this->font->descent) * $fontSize * 2;
270
271        $width = 0;
272        $height = $lineHeight;
273        $counter = count($text);
274
275        for ($i = 0; $i < $counter; $i++) {
276            $letter = $text[$i];
277
278            // line break support (10 is unicode for linebreak)
279            if ($letter == 10) {
280                $width = $lineWidth > $width ? $lineWidth : $width;
281                $height += $lineHeight * $this->font->lineHeight;
282                $lineWidth = 0;
283                continue;
284            }
285
286            $lineWidth += $this->font->glyphs[$letter]->horizAdvX * $fontSize + $this->font->em * $this->font->letterSpacing * $fontSize;
287        }
288
289        // only keep the widest line's width
290        $width = $lineWidth > $width ? $lineWidth : $width;
291
292        return [$width, $height];
293    }
294
295    /**
296     * Function takes unicode character and returns the UTF-8 equivalent
297     * @param string $str
298     * @return string
299     */
300    public function unicodeDef($unicode)
301    {
302
303        $horizAdvY = $this->font->ascent + $this->font->descent;
304        $fontSize = (float) $this->font->size / $this->font->unitsPerEm;
305
306        // extract character definition
307        $d = $this->font->glyphs[hexdec($unicode)]->d;
308
309        // transform typo from original SVG format to straight display
310        $d = $this->defScale($d, $fontSize, -$fontSize);
311        $d = $this->defTranslate($d, 0, $horizAdvY * $fontSize * 2);
312
313        return $d;
314    }
315
316    /**
317     * Returns the character width, as set in the font file
318     * @param string $str
319     * @param boolean $is_unicode
320     * @return float
321     */
322    public function characterWidth($char, $is_unicode = false)
323    {
324        if ($is_unicode) {
325            $letter = hexdec($char);
326        } else {
327            $letter = $this->_utf8ToUnicode($char);
328        }
329
330        if (!isset($this->font->glyphs[$letter])) {
331            return null;
332        }
333
334        $fontSize = (float) $this->font->size / $this->font->unitsPerEm;
335        return $this->font->glyphs[$letter]->horizAdvX * $fontSize;
336    }
337
338    /**
339     * Applies a translate transformation to definition
340     * @param string $def definition
341     * @param float $x
342     * @param float $y
343     * @return string
344     */
345    public function defTranslate($def, $x = 0, $y = 0)
346    {
347        return $this->defApplyMatrix($def, [1, 0, 0, 1, $x, $y]);
348    }
349
350    /**
351     * Applies a translate transformation to definition
352     * @param string $def Definition
353     * @param integer $angle Rotation angle (degrees)
354     * @param integer $x X coordinate of rotation center
355     * @param integer $y Y coordinate of rotation center
356     * @return string
357     */
358    public function defRotate($def, $angle, $x = 0, $y = 0)
359    {
360        if ($x == 0 && $y == 0) {
361            $angle = deg2rad($angle);
362            return $this->defApplyMatrix($def, [cos($angle), sin($angle), -sin($angle), cos($angle), 0, 0]);
363        }
364
365        // rotate by a given point
366        $def = $this->defTranslate($def, $x, $y);
367        $def = $this->defRotate($def, $angle);
368        $def = $this->defTranslate($def, -$x, -$y);
369        return $def;
370    }
371
372    /**
373     * Applies a scale transformation to definition
374     * @param string $def definition
375     * @param integer $x
376     * @param integer $y
377     * @return string
378     */
379    public function defScale($def, $x = 1, $y = 1)
380    {
381        return $this->defApplyMatrix($def, [$x, 0, 0, $y, 0, 0]);
382    }
383
384    /**
385     * Calculates the new definition with the matrix applied
386     * @param string $def
387     * @param array $matrix
388     * @return string
389     */
390    public function defApplyMatrix($def, $matrix)
391    {
392
393        // if there are several shapes in this definition, do the operation for each
394        preg_match_all('/M[^zZ]*[zZ]/', $def, $shapes);
395        $shapes = $shapes[0];
396        if (count($shapes) > 1) {
397            foreach ($shapes as &$shape) {
398                $shape = $this->defApplyMatrix($shape, $matrix);
399            }
400            return implode(' ', $shapes);
401        }
402
403        preg_match_all('/[a-zA-Z]+[^a-zA-Z]*/', $def, $instructions);
404        $instructions = $instructions[0];
405        foreach ($instructions as &$instruction) {
406            $i = preg_replace('/[^a-zA-Z]*/', '', $instruction);
407            preg_match_all('/\-?[0-9\.]+/', $instruction, $coords);
408            $coords = $coords[0];
409
410            if (empty($coords)) {
411                continue;
412            }
413
414            $new_coords = [];
415            while (count($coords) > 0) {
416                // do the matrix calculation stuff
417                [$a, $b, $c, $d, $e, $f] = $matrix;
418
419                // exception for relative instruction
420                if (preg_match('/[a-z]/', $i)) {
421                    $e = 0;
422                    $f = 0;
423                }
424
425                // convert horizontal lineto (relative)
426                if ($i == 'h') {
427                    $i = 'l';
428                    $x = (float) array_shift($coords);
429                    $y = 0;
430
431                    // add new point's coordinates
432                    $current_point = [$a * $x + $e, $b * $x + $f];
433                    $new_coords = [...$new_coords, ...$current_point];
434                } // convert vertical lineto (relative)
435                elseif ($i == 'v') {
436                    $i = 'l';
437                    $x = 0;
438                    $y = (float) array_shift($coords);
439
440                    // add new point's coordinates
441                    $current_point = [$c * $y + $e, $d * $y + $f];
442                    $new_coords = [...$new_coords, ...$current_point];
443                } // convert quadratic bezier curve (relative)
444                elseif ($i == 'q') {
445                    $x = (float) array_shift($coords);
446                    $y = (float) array_shift($coords);
447
448                    // add new point's coordinates
449                    $current_point = [$a * $x + $c * $y + $e, $b * $x + $d * $y + $f];
450                    $new_coords = [...$new_coords, ...$current_point];
451
452                    // same for 2nd point
453                    $x = (float) array_shift($coords);
454                    $y = (float) array_shift($coords);
455
456                    // add new point's coordinates
457                    $current_point = [$a * $x + $c * $y + $e, $b * $x + $d * $y + $f];
458                    $new_coords = array_merge($new_coords, $current_point);
459                }
460
461                // every other commands
462                // @TODO: handle 'a,c,s' (elliptic arc curve) commands
463                // cf. http://www.w3.org/TR/SVG/paths.html#PathDataCurveCommands
464                else {
465                    $x = (float) array_shift($coords);
466                    $y = (float) array_shift($coords);
467
468                    // add new point's coordinates
469                    $current_point = [$a * $x + $c * $y + $e, $b * $x + $d * $y + $f];
470                    $new_coords = [...$new_coords, ...$current_point];
471                }
472            }
473
474            $instruction = $i . implode(',', $new_coords);
475
476            // remove useless commas
477            $instruction = preg_replace('/,\-/', '-', $instruction);
478        }
479
480        return implode('', $instructions);
481    }
482
483
484
485    /**
486     *
487     * Short-hand methods
488     *
489     */
490
491    /**
492     * Return full SVG XML
493     * @return string
494     */
495    public function asXML()
496    {
497        return $this->svg->asXML();
498    }
499
500    /**
501     * Adds an attribute to the SVG
502     * @param string $key
503     * @param string $value
504     */
505    public function addAttribute($key, $value)
506    {
507        return $this->svg->addAttribute($key, $value);
508    }
509}
510