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