[Back] <?php
/**
* @package php-svg-lib
* @link http://github.com/dompdf/php-svg-lib
* @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html
*/
namespace Svg\Tag;
use Svg\Surface\SurfaceInterface;
class Path extends Shape
{
// kindly borrowed from fabric.util.parsePath.
/* @see https://github.com/fabricjs/fabric.js/blob/master/src/util/path.js#L664 */
const NUMBER_PATTERN = '([-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?)\s*';
const COMMA_PATTERN = '(?:\s+,?\s*|,\s*)?';
const FLAG_PATTERN = '([01])';
const ARC_REGEXP = '/'
. self::NUMBER_PATTERN
. self::COMMA_PATTERN
. self::NUMBER_PATTERN
. self::COMMA_PATTERN
. self::NUMBER_PATTERN
. self::COMMA_PATTERN
. self::FLAG_PATTERN
. self::COMMA_PATTERN
. self::FLAG_PATTERN
. self::COMMA_PATTERN
. self::NUMBER_PATTERN
. self::COMMA_PATTERN
. self::NUMBER_PATTERN
. '/';
static $commandLengths = array(
'm' => 2,
'l' => 2,
'h' => 1,
'v' => 1,
'c' => 6,
's' => 4,
'q' => 4,
't' => 2,
'a' => 7,
);
static $repeatedCommands = array(
'm' => 'l',
'M' => 'L',
);
public static function parse(string $commandSequence): array
{
$commands = array();
preg_match_all('/([MZLHVCSQTAmzlhvcsqta])([eE ,\-.\d]+)*/', $commandSequence, $commands, PREG_SET_ORDER);
$path = array();
foreach ($commands as $c) {
if (count($c) == 3) {
$commandLower = strtolower($c[1]);
// arcs have special flags that apparently don't require spaces.
if ($commandLower === 'a' && preg_match_all(static::ARC_REGEXP, $c[2], $matches, PREG_PATTERN_ORDER)) {
$numberOfMatches = count($matches[0]);
for ($k = 0; $k < $numberOfMatches; ++$k) {
$path[] = [
$c[1],
$matches[1][$k],
$matches[2][$k],
$matches[3][$k],
$matches[4][$k],
$matches[5][$k],
$matches[6][$k],
$matches[7][$k],
];
}
continue;
}
$arguments = array();
preg_match_all('/([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/i', $c[2], $arguments, PREG_PATTERN_ORDER);
$item = $arguments[0];
if (
isset(self::$commandLengths[$commandLower]) &&
($commandLength = self::$commandLengths[$commandLower]) &&
count($item) > $commandLength
) {
$repeatedCommand = isset(self::$repeatedCommands[$c[1]]) ? self::$repeatedCommands[$c[1]] : $c[1];
$command = $c[1];
for ($k = 0, $klen = count($item); $k < $klen; $k += $commandLength) {
$_item = array_slice($item, $k, $k + $commandLength);
array_unshift($_item, $command);
$path[] = $_item;
$command = $repeatedCommand;
}
} else {
array_unshift($item, $c[1]);
$path[] = $item;
}
} else {
$item = array($c[1]);
$path[] = $item;
}
}
return $path;
}
public function start($attributes)
{
if (!isset($attributes['d'])) {
$this->hasShape = false;
return;
}
$path = static::parse($attributes['d']);
$surface = $this->document->getSurface();
// From https://github.com/kangax/fabric.js/blob/master/src/shapes/path.class.js
$current = null; // current instruction
$previous = null;
$subpathStartX = 0;
$subpathStartY = 0;
$x = 0; // current x
$y = 0; // current y
$controlX = 0; // current control point x
$controlY = 0; // current control point y
$tempX = null;
$tempY = null;
$tempControlX = null;
$tempControlY = null;
$l = 0; //-((this.width / 2) + $this.pathOffset.x),
$t = 0; //-((this.height / 2) + $this.pathOffset.y),
foreach ($path as $current) {
switch ($current[0]) { // first letter
case 'l': // lineto, relative
$x += $current[1];
$y += $current[2];
$surface->lineTo($x + $l, $y + $t);
break;
case 'L': // lineto, absolute
$x = $current[1];
$y = $current[2];
$surface->lineTo($x + $l, $y + $t);
break;
case 'h': // horizontal lineto, relative
$x += $current[1];
$surface->lineTo($x + $l, $y + $t);
break;
case 'H': // horizontal lineto, absolute
$x = $current[1];
$surface->lineTo($x + $l, $y + $t);
break;
case 'v': // vertical lineto, relative
$y += $current[1];
$surface->lineTo($x + $l, $y + $t);
break;
case 'V': // verical lineto, absolute
$y = $current[1];
$surface->lineTo($x + $l, $y + $t);
break;
case 'm': // moveTo, relative
$x += $current[1];
$y += $current[2];
$subpathStartX = $x;
$subpathStartY = $y;
$surface->moveTo($x + $l, $y + $t);
break;
case 'M': // moveTo, absolute
$x = $current[1];
$y = $current[2];
$subpathStartX = $x;
$subpathStartY = $y;
$surface->moveTo($x + $l, $y + $t);
break;
case 'c': // bezierCurveTo, relative
$tempX = $x + $current[5];
$tempY = $y + $current[6];
$controlX = $x + $current[3];
$controlY = $y + $current[4];
$surface->bezierCurveTo(
$x + $current[1] + $l, // x1
$y + $current[2] + $t, // y1
$controlX + $l, // x2
$controlY + $t, // y2
$tempX + $l,
$tempY + $t
);
$x = $tempX;
$y = $tempY;
break;
case 'C': // bezierCurveTo, absolute
$x = $current[5];
$y = $current[6];
$controlX = $current[3];
$controlY = $current[4];
$surface->bezierCurveTo(
$current[1] + $l,
$current[2] + $t,
$controlX + $l,
$controlY + $t,
$x + $l,
$y + $t
);
break;
case 's': // shorthand cubic bezierCurveTo, relative
// transform to absolute x,y
$tempX = $x + $current[3];
$tempY = $y + $current[4];
if (!preg_match('/[CcSs]/', $previous[0])) {
// If there is no previous command or if the previous command was not a C, c, S, or s,
// the control point is coincident with the current point
$controlX = $x;
$controlY = $y;
} else {
// calculate reflection of previous control points
$controlX = 2 * $x - $controlX;
$controlY = 2 * $y - $controlY;
}
$surface->bezierCurveTo(
$controlX + $l,
$controlY + $t,
$x + $current[1] + $l,
$y + $current[2] + $t,
$tempX + $l,
$tempY + $t
);
// set control point to 2nd one of this command
// "... the first control point is assumed to be
// the reflection of the second control point on
// the previous command relative to the current point."
$controlX = $x + $current[1];
$controlY = $y + $current[2];
$x = $tempX;
$y = $tempY;
break;
case 'S': // shorthand cubic bezierCurveTo, absolute
$tempX = $current[3];
$tempY = $current[4];
if (!preg_match('/[CcSs]/', $previous[0])) {
// If there is no previous command or if the previous command was not a C, c, S, or s,
// the control point is coincident with the current point
$controlX = $x;
$controlY = $y;
} else {
// calculate reflection of previous control points
$controlX = 2 * $x - $controlX;
$controlY = 2 * $y - $controlY;
}
$surface->bezierCurveTo(
$controlX + $l,
$controlY + $t,
$current[1] + $l,
$current[2] + $t,
$tempX + $l,
$tempY + $t
);
$x = $tempX;
$y = $tempY;
// set control point to 2nd one of this command
// "... the first control point is assumed to be
// the reflection of the second control point on
// the previous command relative to the current point."
$controlX = $current[1];
$controlY = $current[2];
break;
case 'q': // quadraticCurveTo, relative
// transform to absolute x,y
$tempX = $x + $current[3];
$tempY = $y + $current[4];
$controlX = $x + $current[1];
$controlY = $y + $current[2];
$surface->quadraticCurveTo(
$controlX + $l,
$controlY + $t,
$tempX + $l,
$tempY + $t
);
$x = $tempX;
$y = $tempY;
break;
case 'Q': // quadraticCurveTo, absolute
$tempX = $current[3];
$tempY = $current[4];
$surface->quadraticCurveTo(
$current[1] + $l,
$current[2] + $t,
$tempX + $l,
$tempY + $t
);
$x = $tempX;
$y = $tempY;
$controlX = $current[1];
$controlY = $current[2];
break;
case 't': // shorthand quadraticCurveTo, relative
// transform to absolute x,y
$tempX = $x + $current[1];
$tempY = $y + $current[2];
// calculate reflection of previous control points
if (preg_match('/[QqT]/', $previous[0])) {
$controlX = 2 * $x - $controlX;
$controlY = 2 * $y - $controlY;
} elseif ($previous[0] === 't') {
$controlX = 2 * $x - $tempControlX;
$controlY = 2 * $y - $tempControlY;
} else {
$controlX = $x;
$controlY = $y;
}
$tempControlX = $controlX;
$tempControlY = $controlY;
$surface->quadraticCurveTo(
$controlX + $l,
$controlY + $t,
$tempX + $l,
$tempY + $t
);
$x = $tempX;
$y = $tempY;
break;
case 'T':
$tempX = $current[1];
$tempY = $current[2];
// calculate reflection of previous control points
if (preg_match('/[QqTt]/', $previous[0])) {
$controlX = 2 * $x - $controlX;
$controlY = 2 * $y - $controlY;
} else {
$controlX = $x;
$controlY = $y;
}
$surface->quadraticCurveTo(
$controlX + $l,
$controlY + $t,
$tempX + $l,
$tempY + $t
);
$x = $tempX;
$y = $tempY;
break;
case 'a':
$this->drawArc(
$surface,
$x + $l,
$y + $t,
array(
$current[1],
$current[2],
$current[3],
$current[4],
$current[5],
$current[6] + $x + $l,
$current[7] + $y + $t
)
);
$x += $current[6];
$y += $current[7];
break;
case 'A':
// TODO: optimize this
$this->drawArc(
$surface,
$x + $l,
$y + $t,
array(
$current[1],
$current[2],
$current[3],
$current[4],
$current[5],
$current[6] + $l,
$current[7] + $t
)
);
$x = $current[6];
$y = $current[7];
break;
case 'z':
case 'Z':
$x = $subpathStartX;
$y = $subpathStartY;
$surface->closePath();
break;
}
$previous = $current;
}
}
function drawArc(SurfaceInterface $surface, $fx, $fy, $coords)
{
$rx = $coords[0];
$ry = $coords[1];
$rot = $coords[2];
$large = $coords[3];
$sweep = $coords[4];
$tx = $coords[5];
$ty = $coords[6];
$segs = array(
array(),
array(),
array(),
array(),
);
$toX = $tx - $fx;
$toY = $ty - $fy;
if ((float)($toX + $toY) === 0.0) {
return;
}
if ((float)abs($rx) == 0.0 || (float)abs($ry) === 0.0) {
$surface->lineTo($tx, $ty);
return;
}
$segsNorm = $this->arcToSegments($toX, $toY, $rx, $ry, $large, $sweep, $rot);
for ($i = 0, $len = count($segsNorm); $i < $len; $i++) {
$segs[$i][0] = $segsNorm[$i][0] + $fx;
$segs[$i][1] = $segsNorm[$i][1] + $fy;
$segs[$i][2] = $segsNorm[$i][2] + $fx;
$segs[$i][3] = $segsNorm[$i][3] + $fy;
$segs[$i][4] = $segsNorm[$i][4] + $fx;
$segs[$i][5] = $segsNorm[$i][5] + $fy;
call_user_func_array(array($surface, "bezierCurveTo"), $segs[$i]);
}
}
function arcToSegments($toX, $toY, $rx, $ry, $large, $sweep, $rotateX)
{
$th = $rotateX * M_PI / 180;
$sinTh = sin($th);
$cosTh = cos($th);
$fromX = 0;
$fromY = 0;
$rx = abs($rx);
$ry = abs($ry);
$px = -$cosTh * $toX * 0.5 - $sinTh * $toY * 0.5;
$py = -$cosTh * $toY * 0.5 + $sinTh * $toX * 0.5;
$rx2 = $rx * $rx;
$ry2 = $ry * $ry;
$py2 = $py * $py;
$px2 = $px * $px;
$pl = $rx2 * $ry2 - $rx2 * $py2 - $ry2 * $px2;
$root = 0;
if ($pl < 0) {
$s = sqrt(1 - $pl / ($rx2 * $ry2));
$rx *= $s;
$ry *= $s;
} else {
$root = ($large == $sweep ? -1.0 : 1.0) * sqrt($pl / ($rx2 * $py2 + $ry2 * $px2));
}
$cx = $root * $rx * $py / $ry;
$cy = -$root * $ry * $px / $rx;
$cx1 = $cosTh * $cx - $sinTh * $cy + $toX * 0.5;
$cy1 = $sinTh * $cx + $cosTh * $cy + $toY * 0.5;
$mTheta = $this->calcVectorAngle(1, 0, ($px - $cx) / $rx, ($py - $cy) / $ry);
$dtheta = $this->calcVectorAngle(($px - $cx) / $rx, ($py - $cy) / $ry, (-$px - $cx) / $rx, (-$py - $cy) / $ry);
if ($sweep == 0 && $dtheta > 0) {
$dtheta -= 2 * M_PI;
} else {
if ($sweep == 1 && $dtheta < 0) {
$dtheta += 2 * M_PI;
}
}
// $Convert $into $cubic $bezier $segments <= 90deg
$segments = ceil(abs($dtheta / M_PI * 2));
$result = array();
$mDelta = $dtheta / $segments;
$mT = 8 / 3 * sin($mDelta / 4) * sin($mDelta / 4) / sin($mDelta / 2);
$th3 = $mTheta + $mDelta;
for ($i = 0; $i < $segments; $i++) {
$result[$i] = $this->segmentToBezier(
$mTheta,
$th3,
$cosTh,
$sinTh,
$rx,
$ry,
$cx1,
$cy1,
$mT,
$fromX,
$fromY
);
$fromX = $result[$i][4];
$fromY = $result[$i][5];
$mTheta = $th3;
$th3 += $mDelta;
}
return $result;
}
function segmentToBezier($th2, $th3, $cosTh, $sinTh, $rx, $ry, $cx1, $cy1, $mT, $fromX, $fromY)
{
$costh2 = cos($th2);
$sinth2 = sin($th2);
$costh3 = cos($th3);
$sinth3 = sin($th3);
$toX = $cosTh * $rx * $costh3 - $sinTh * $ry * $sinth3 + $cx1;
$toY = $sinTh * $rx * $costh3 + $cosTh * $ry * $sinth3 + $cy1;
$cp1X = $fromX + $mT * (-$cosTh * $rx * $sinth2 - $sinTh * $ry * $costh2);
$cp1Y = $fromY + $mT * (-$sinTh * $rx * $sinth2 + $cosTh * $ry * $costh2);
$cp2X = $toX + $mT * ($cosTh * $rx * $sinth3 + $sinTh * $ry * $costh3);
$cp2Y = $toY + $mT * ($sinTh * $rx * $sinth3 - $cosTh * $ry * $costh3);
return array(
$cp1X,
$cp1Y,
$cp2X,
$cp2Y,
$toX,
$toY
);
}
function calcVectorAngle($ux, $uy, $vx, $vy)
{
$ta = atan2($uy, $ux);
$tb = atan2($vy, $vx);
if ($tb >= $ta) {
return $tb - $ta;
} else {
return 2 * M_PI - ($ta - $tb);
}
}
}