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