* @version 0.1b
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
* @see http://stackoverflow.com/questions/14684846/flattening-svg-matrix-transforms-in-inkscape
* @see http://stackoverflow.com/questions/7742148/how-to-convert-text-to-svg-paths
*/
class EasySVG
{
protected stdClass $font;
protected SimpleXMLElement $svg;
public function __construct()
{
// default font data
$this->font = new stdClass();
$this->font->id = '';
$this->font->horizAdvX = 0;
$this->font->unitsPerEm = 0;
$this->font->ascent = 0;
$this->font->descent = 0;
$this->font->glyphs = [];
$this->font->hkern = [];
$this->font->useKerning = false;
$this->font->size = 20;
$this->font->color = null;
$this->font->lineHeight = 1;
$this->font->letterSpacing = 0;
$this->clearSVG();
}
public function clearSVG(): void
{
$this->svg = new SimpleXMLElement('');
$this->svg->addAttribute('version', '1.1');
$this->svg->addAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
/**
* Function takes UTF-8 encoded string and returns unicode number for every character.
* @param null|string $str
* @return array
*/
private function _utf8ToUnicode(?string $str): array
{
if ($str === null) {
return [];
}
$unicode = [];
$values = [];
$lookingFor = 1;
for ($i = 0, $iMax = strlen($str); $i < $iMax; $i++) {
$thisValue = ord($str[ $i ]);
if ($thisValue < 128) {
$unicode[] = $thisValue;
} else {
if (count($values) === 0) {
$lookingFor = ($thisValue < 224) ? 2 : 3;
}
$values[] = $thisValue;
if (count($values) === $lookingFor) {
$number = ($lookingFor === 3) ?
(($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64) :
(($values[0] % 32) * 64) + ($values[1] % 64);
$unicode[] = $number;
$values = [];
$lookingFor = 1;
}
}
}
return $unicode;
}
/**
* Set font params (short-hand method)
* @param string $filepath
* @param integer $size
* @param string|null $color
*/
public function setFont(string $filepath, int $size, ?string $color = null): void
{
$this->setFontSVG($filepath);
$this->setFontSize($size);
if ($color) {
$this->setFontColor($color);
}
}
/**
* Set font size for display
* @param int $size
* @return void
*/
public function setFontSize(int $size): void
{
$this->font->size = $size;
}
/**
* Set kerning support flag
* @param bool $bool
* @return void
*/
public function setUseKerning(bool $bool): void
{
$this->font->useKerning = $bool;
}
/**
* Set font color
* @param string $color
* @return void
*/
public function setFontColor(string $color): void
{
$this->font->color = $color;
}
/**
* Set the line height from default (1) to custom value
* @param float $value
* @return void
*/
public function setLineHeight(float $value): void
{
$this->font->lineHeight = $value;
}
/**
* Set the letter spacing from default (0) to custom value
* @param float $value
* @return void
*/
public function setLetterSpacing(float $value): void
{
$this->font->letterSpacing = $value;
}
/**
* Function takes path to SVG font (local path) and processes its xml
* to get path representation of every character and additional
* font parameters
* @param string $filepath
* @return void
*/
public function setFontSVG(string $filepath): void
{
$this->font->glyphs = [];
$z = new XMLReader();
$z->open($filepath);
// move to the first node
while ($z->read()) {
$name = $z->name;
if ($z->nodeType === XMLReader::ELEMENT) {
if ($name === 'font') {
$this->font->id = $z->getAttribute('id');
$this->font->horizAdvX = $z->getAttribute('horiz-adv-x');
}
if ($name === 'font-face') {
$this->font->unitsPerEm = $z->getAttribute('units-per-em');
$this->font->ascent = $z->getAttribute('ascent');
$this->font->descent = $z->getAttribute('descent');
}
if ($name === 'glyph') {
$unicode = $z->getAttribute('unicode');
if (isset($unicode)) {
$unicode = $this->_utf8ToUnicode($unicode);
if (isset($unicode[0])) {
$unicode = $unicode[0];
$this->font->glyphs[$unicode] = new stdClass();
$this->font->glyphs[$unicode]->horizAdvX = $z->getAttribute('horiz-adv-x');
if (empty($this->font->glyphs[$unicode]->horizAdvX)) {
$this->font->glyphs[$unicode]->horizAdvX = $this->font->horizAdvX;
}
$this->font->glyphs[$unicode]->d = $z->getAttribute('d');
// save em value for letter spacing (109 is unicode for the letter 'm')
if ($unicode === 109) {
$this->font->em = $this->font->glyphs[$unicode]->horizAdvX;
}
}
}
}
if ($name === 'hkern') {
$u1 = $this->_utf8ToUnicode($z->getAttribute('u1'));
$u2 = $this->_utf8ToUnicode($z->getAttribute('u2'));
if (isset($u1[0], $u2[0])) {
$k = $z->getAttribute('k');
$this->font->hkern[$u1[0]][$u2[0]] = $k;
}
}
}
}
}
/**
* Add a path to the SVG
* @param string $def
* @param array $attributes
* @return null|SimpleXMLElement
*/
public function addPath(string $def, array $attributes = []): ?SimpleXMLElement
{
$path = $this->svg->addChild('path');
if ($path === null) {
return null;
}
foreach ($attributes as $key => $value) {
$path->addAttribute($key, $value);
}
$path->addAttribute('d', $def);
return $path;
}
/**
* Add a text to the SVG
* @param string $text
* @param float|string $x
* @param float|string $y
* @param array $attributes
* @return null|SimpleXMLElement
*/
public function addText(string $text, $x = 0, $y = 0, array $attributes = []): ?SimpleXMLElement
{
$def = $this->textDef($text);
if ($x === 'center' || $y === 'center') {
[$textWidth, $textHeight] = $this->textDimensions($text);
}
// center horizontally
if ($x === 'center') {
if ($this->svg['width'] === null) {
throw new Error('SVG width has to be set to center the text horizontally');
}
$x = ((int)$this->svg['width'] - $textWidth) / 2;
}
// center vertically
if ($y === 'center') {
if ($this->svg['height'] === null) {
throw new Error('SVG height has to be set to center the text vertically');
}
$y = ((int)$this->svg['height'] - $textHeight) / 2;
}
if ($x !== 0 || $y !== 0) {
$def = $this->defTranslate($def, $x, $y);
}
if ($this->font->color) {
$attributes['fill'] = $this->font->color;
}
return $this->addPath($def, $attributes);
}
/**
* Function takes UTF-8 encoded string and size, returns xml for SVG paths representing this string.
* @param string $text UTF-8 encoded text
* @return string xml for text converted into SVG paths
*/
public function textDef(string $text): string
{
$def = [];
$horizAdvX = 0;
$horizAdvY = $this->font->ascent + $this->font->descent;
$fontSize = (float)$this->font->size / $this->font->unitsPerEm;
$textUnicode = $this->_utf8ToUnicode($text);
$prevLetter = '';
foreach ($textUnicode as $letter) {
// kern
if ($this->font->useKerning && isset($this->font->hkern[$prevLetter][$letter])) {
$horizAdvX -= $this->font->hkern[$prevLetter][$letter] * $fontSize;
}
//ignore this glyph instead of throwing an error if the font does not define it
if (!array_key_exists($letter, $this->font->glyphs)) {
continue;
}
// line break support (10 is unicode for linebreak)
if ($letter === 10) {
$horizAdvX = 0;
$horizAdvY += $this->font->lineHeight * ($this->font->ascent + $this->font->descent);
continue;
}
// extract character definition
$d = $this->font->glyphs[$letter]->d;
// transform typo from original SVG format to straight display
$d = $this->defScale($d, $fontSize, -$fontSize);
$d = $this->defTranslate($d, $horizAdvX, $horizAdvY*$fontSize*2);
$def[] = $d;
// next letter's position
$horizAdvX += $this->font->glyphs[$letter]->horizAdvX * $fontSize + $this->font->em * $this->font->letterSpacing * $fontSize;
$prevLetter = $letter;
}
return implode(' ', $def);
}
/**
* Function takes UTF-8 encoded string and size, returns width and height of the whole text
* @param string $text UTF-8 encoded text
* @return array{width: float, height: float}
*/
public function textDimensions(string $text): array
{
$fontSize = (float)$this->font->size / $this->font->unitsPerEm;
$textUnicode = $this->_utf8ToUnicode($text);
$lineWidth = 0;
$lineHeight = ($this->font->ascent + $this->font->descent) * $fontSize * 2;
$width = 0;
$height = $lineHeight;
$prevLetter = '';
foreach ($textUnicode as $letter) {
//ignore this glyph instead of throwing an error if the font does not define it
if (!array_key_exists($letter, $this->font->glyphs)) {
continue;
}
// line break support (10 is unicode for linebreak)
if ($letter === 10) {
$width = max($lineWidth, $width);
$height += $lineHeight * $this->font->lineHeight;
$lineWidth = 0;
continue;
}
$lineWidth += $this->font->glyphs[$letter]->horizAdvX * $fontSize + $this->font->em * $this->font->letterSpacing * $fontSize;
// kern
if ($this->font->useKerning && isset($this->font->hkern[$prevLetter][$letter])) {
$lineWidth -= $this->font->hkern[$prevLetter][$letter] * $fontSize;
}
$prevLetter = $letter;
}
// only keep the widest line's width
$width = max($lineWidth, $width);
return [$width, $height];
}
/**
* Function takes unicode character and returns the UTF-8 equivalent
* @param string $unicode
* @return string
*/
public function unicodeDef(string $unicode): string
{
$horizAdvY = $this->font->ascent + $this->font->descent;
$fontSize = (float)$this->font->size / $this->font->unitsPerEm;
// extract character definition
$d = $this->font->glyphs[hexdec($unicode)]->d;
// handle if d attribute is not set in corresponding glyph
if(is_null($d)) {
$d = "";
}
// transform typo from original SVG format to straight display
$d = $this->defScale($d, $fontSize, -$fontSize);
return $this->defTranslate($d, 0, $horizAdvY*$fontSize*2);
}
/**
* Returns the character width, as set in the font file
* @param string $char
* @param boolean $is_unicode
* @return float
*/
public function characterWidth(string $char, bool $is_unicode = false)
{
if ($is_unicode) {
$letter = hexdec($char);
} else {
$letter = $this->_utf8ToUnicode($char);
}
if (!isset($this->font->glyphs[$letter])) {
return null;
}
$fontSize = (float)$this->font->size / $this->font->unitsPerEm;
return $this->font->glyphs[$letter]->horizAdvX * $fontSize;
}
/**
* Applies a translate transformation to definition
* @param string $def definition
* @param float $x
* @param float $y
* @return string
*/
public function defTranslate(string $def, float $x = 0, float $y = 0): string
{
return $this->defApplyMatrix($def, [1, 0, 0, 1, $x, $y]);
}
/**
* Applies a translate transformation to definition
* @param string $def Definition
* @param float $angle Rotation angle (degrees)
* @param float $x X coordinate of rotation center
* @param float $y Y coordinate of rotation center
* @return string
*/
public function defRotate(string $def, float $angle, float $x = 0, float $y = 0): string
{
if ($x === 0 && $y === 0) {
$angle = deg2rad($angle);
return $this->defApplyMatrix($def, [cos($angle), sin($angle), -sin($angle), cos($angle), 0, 0]);
}
// rotate by a given point
$def = $this->defTranslate($def, $x, $y);
$def = $this->defRotate($def, $angle);
return $this->defTranslate($def, -$x, -$y);
}
/**
* Applies a scale transformation to definition
* @param string $def definition
* @param float $x
* @param float $y
* @return string
*/
public function defScale(string $def, float $x = 1, float $y = 1): string
{
return $this->defApplyMatrix($def, [$x, 0, 0, $y, 0, 0]);
}
/**
* Calculates the new definition with the matrix applied
* @param string $def
* @param array $matrix
* @return string
*/
public function defApplyMatrix(string $def, array $matrix): string
{
// if there are several shapes in this definition, do the operation for each
preg_match_all('/M[^zZ]*[zZ]/', $def, $shapes);
$shapes = $shapes[0];
if (count($shapes)>1) {
foreach ($shapes as &$shape) {
$shape = $this->defApplyMatrix($shape, $matrix);
}
return implode(' ', $shapes);
}
preg_match_all('/[a-zA-Z]+[^a-zA-Z]*/', $def, $instructions);
$instructions = $instructions[0];
$return = '';
foreach ($instructions as &$instruction) {
$i = preg_replace('/[^a-zA-Z]*/', '', $instruction);
preg_match_all('/-?[0-9.]+/', $instruction, $coords);
$coords = $coords[0];
if (empty($coords)) {
continue;
}
$new_coords = [];
while (count($coords)>0) {
// do the matrix calculation stuff
[$a, $b, $c, $d, $e, $f] = $matrix;
// exception for relative instruction
if (preg_match('/[a-z]/', $i)) {
$e = 0;
$f = 0;
}
// convert horizontal lineto (relative)
if ($i === 'h') {
$i = 'l';
$x = (float)array_shift($coords);
$y = 0;
// add new point's coordinates
$current_point = [
$a*$x + $c*$y + $e,
$b*$x + $d*$y + $f,
];
$new_coords = array_merge($new_coords, $current_point);
}
// convert vertical lineto (relative)
elseif ($i === 'v') {
$i = 'l';
$x = 0;
$y = (float)array_shift($coords);
// add new point's coordinates
$current_point = [
$a*$x + $c*$y + $e,
$b*$x + $d*$y + $f,
];
$new_coords = array_merge($new_coords, $current_point);
}
// convert quadratic bezier curve (relative)
elseif ($i === 'q') {
$x = (float)array_shift($coords);
$y = (float)array_shift($coords);
// add new point's coordinates
$current_point = [
$a*$x + $c*$y + $e,
$b*$x + $d*$y + $f,
];
$new_coords = array_merge($new_coords, $current_point);
// same for 2nd point
$x = (float)array_shift($coords);
$y = (float)array_shift($coords);
// add new point's coordinates
$current_point = [
$a*$x + $c*$y + $e,
$b*$x + $d*$y + $f,
];
$new_coords = array_merge($new_coords, $current_point);
}
// every other commands
// @TODO: handle 'a,c,s' (elliptic arc curve) commands
// cf. http://www.w3.org/TR/SVG/paths.html#PathDataCurveCommands
else {
$x = (float)array_shift($coords);
$y = (float)array_shift($coords);
// add new point's coordinates
$current_point = [
$a*$x + $c*$y + $e,
$b*$x + $d*$y + $f,
];
$new_coords = array_merge($new_coords, $current_point);
}
}
$instruction = $i . implode(',', $new_coords);
// remove useless commas
$instruction = preg_replace('/,-/', '-', $instruction);
}
return implode('', $instructions);
}
/**
* Return full SVG XML
* @return string
*/
public function asXML(): string
{
return $this->svg->asXML();
}
/**
* Adds an attribute to the SVG
* @param string $key
* @param string $value
*/
public function addAttribute(string $key, string $value): void
{
$this->svg->addAttribute($key, $value);
}
}__halt_compiler();----SIGNATURE:----TEFMtle7Wh0Nw3gF2Fj5AZWjumMqiiQ2R8HxGIibgsZqTdOWEc/AG0WBhJ5WivxmqqAC3RDpPZm+o7VvBkiuFBDpvIN81xxe1xe8bYquV5emtI/0W3igIDf1g5YWXS0h10U1ssQRay4Ka864ZMA61dreDHS5Dn5W95HJ3YJTI8c6cYR3bdzWLlzyeIaTNIfIQ31gIh/k8k5Dretj5qozNffVL8UKvHI97ObGsdMWvTlZhS1wkXDppKnudr2RXdc8wgRHgFjluln/MxBSAevS2CpE1I/KphLfuhsXg/C5hbSrWEaanYdJrxQhl2I0TViMYsGx92DoaOEOEAf6f7gZqv6brlQT6fyWzj7rBRujrXG9sGrjTlTczUPC4EPJRBqiI5DSlPutJiVfHGTomyRz29duAUSH51V+l1vLLqXk9GmM3mOyZjypo9W/+5rfd8ZPoSrkSsntuTDBPCZOiXiUhWzYkebV6L12va9jlS/tB1bJDk4i6unj2w0NqTQqaezPG/RZLWTl9Q8XLNVZhQMovc9W9iyl4/lKV2nUD9BEMGVH1StA1JDpsYgRoKpK3lcbPhhS+DkBHtGqVPnKluBN/+yHYQ/oCU2Xl/p0JdJGGLodc+72WOcS7s1Zu2mk4MQDV47PyGBfWBpFU97uaMUkHBGDqEdzEe9iBwi1CBy8U84=----ATTACHMENT:----NjI2ODg0MzE4NDE5NjEzNiAyMTYwNjE1MDc2MjYzNjk0IDQwMzA1NjUzMzUwMzMxMjE=