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