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