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