Skip to content

Instantly share code, notes, and snippets.

@karlwilcox
Created January 8, 2013 00:04
Show Gist options
  • Save karlwilcox/4479813 to your computer and use it in GitHub Desktop.
Save karlwilcox/4479813 to your computer and use it in GitHub Desktop.
PHP SVG Path Manipulation Functions
<?php /* SVG PATH MANIPULATION FUNCTIONS
rotatePath(), mirrorPath(), reversePath(), scalePath(), translatePath(), matrixPath() and makeRelativePath()
The path argument is the string value of the 'd' attribute of the svg 'path' element, NOT the whole element
Other arguments should be self-explanatory
The return value is a string that can used as the value of the 'd' attribute
It is usually best to make the path relative first, then carry out other manipulations
Shortcomings:
THE PRECISION OF THE RESULTING STRING SHOULD REALLY BE CONFIGURABLE (currently hardcoded at 4 decimal places)
MATRIX OPERATIONS ON ARCS ARE NOT IMPLEMENTED
mirrorPath() ONLY MIRRORS either the x or y axis, not arbitrary axes (but you can of course rotate afterwards)
DON'T FORGET - these operations are (mostly) NOT COMMUTATIVE: translatePath(rotatePath()) <> rotatePath(translatePath())
*/
// UTILITY FUNCTION
function rotatePoint( $x, $y, $theta ) {
$retval = '';
switch ( $theta ) {
case 360:
case 0: // should not happen, but just in case...
$x2 = $x;
$y2 = $y;
break;
case 90:
$x2 = $y * -1;
$y2 = $x;
break;
case 180:
$x2 = $x * -1;
$y2 = $y * -1;
break;
case 270:
$x2 = $y;
$y2 = $x * -1;
break;
default:
$cos = cos(deg2rad($theta));
$sin = sin(deg2rad($theta));
$x2 = ($cos * $x) - ($sin * $y);
$y2 = ($cos * $y) + ($sin * $x);
break;
}
$retval = sprintf('%.4g,%.4g',$x2,$y2);
return $retval;
}
// UTILITY FUNCTION
function getNumbers ( $argString ) {
$argString = preg_replace('/-/',' -', $argString);
$argArray = array();
$length = strlen($argString);
$numStr = '';
$expStr = '';
$component = 'man';
for ( $count = 0; $count < $length; $count++ ) {
$char = $argString{$count};
if ( $char == 'e' ) {
/* NOT FULLY TESTED - INKSCAPE OFTEN USES 1e-6, WHEN IT REALLY MEANS ZERO */
$component = 'exp';
// warningMessage('Exponent found in number');
} elseif ( ctype_digit($char) or $char == '.' or $char == '-' ) {
if ( $component == 'man' )
$numStr .= $char;
else
$expStr .= $char;
} else { // Got a complete number
if ( strlen($numStr) ) {
$number = (float) $numStr;
if ( $component == 'exp' ) {
$number = 0.0;
}
$argArray[] = $number;
$numStr = '';
$expStr = '';
$component = 'man';
}
}
} // Process last number
if ( strlen($numStr) ) {
$number = (float) $numStr;
if ( $component == 'exp' ) {
$number = 0.0;
}
$argArray[] = $number;
}
return $argArray;
}
function rotatePath( $path, $theta ) {
if ( $theta == 0 ) return $path;
$newPath = '';
$chunks = preg_split('/([a-z])/i', $path, null, PREG_SPLIT_DELIM_CAPTURE );
for ( $i = 0; $i < count($chunks); $i++ ) {
$command = $chunks[$i];
if ( isset($chunks[$i+1]) )
$numbers = getNumbers( $chunks[$i+1] );
switch ( strtolower($command) ) {
case 'h':
$x = $numbers[0];
if ($theta == 270 )
$newPath .= sprintf('v%.4g', ($x * -1));
elseif ( $theta == 180 )
$newPath .= sprintf('h%.4g', ($x * -1));
elseif ( $theta == 90 )
$newPath .= 'v' . $x;
else
$newPath .= 'l' . rotatePoint($x,0, $theta);
break;
case 'v':
$y = $numbers[0];
if ($theta == 270 )
$newPath .= 'h' . $y;
elseif ( $theta == 180 )
$newPath .= sprintf('v%.4g', ($y * -1));
elseif ( $theta == 90 )
$newPath .= sprintf('h%.4g', ($y * -1));
else
$newPath .= 'l' . rotatePoint(0,$y, $theta);
break;
case 'a':
list($rx,$ry,$rot,$arc,$sweep,$x,$y) = $numbers;
$newPath .= sprintf('a%.4g,%.4g %.4g,%d,%d %s', $rx,$ry, (($rot + $theta)%360),$arc,$sweep, rotatePoint($x,$y, $theta));
break;
case 'l':
case 'q':
case 'c':
case 'm':
$newPath .= $command;
if ($command == 'M') {
$newPath .= $numbers[0] . ',' . $numbers[1];
} else {
for ( $j = 0; $j < count($numbers); $j += 2 )
$newPath .= rotatePoint($numbers[$j], $numbers[$j+1], $theta ) . ' ';
}
break;
case 'z':
$newPath .= $command;
break;
}
$newPath .= ' ';
}
return $newPath;
}
function translatePath( $path, $xTrans, $yTrans ) {
if ( $xTrans == 0 and $yTrans == 0 ) return $path;
$newPath = '';
$chunks = preg_split('/([a-z])/i', $path, null, PREG_SPLIT_DELIM_CAPTURE );
$firstMove = true;
for ( $i = 0; $i < count($chunks); $i++ ) {
$command = $chunks[$i];
if ( isset($chunks[$i+1]) )
$numbers = getNumbers( $chunks[$i+1] );
switch ( $command ) {
case 'H':
$newPath .= sprintf('H%.4g', $numbers[0] + $xTrans);
break;
case 'V':
$newPath .= sprintf('V%.4g', $numbers[0] + $yTrans);
break;
case 'M':
case 'L':
case 'C':
$newPath .= $command;
for ( $j = 0; $j < count($numbers); $j += 2 ) {
$newPath .= sprintf('%.4g,', ($numbers[$j] + $xTrans));
$newPath .= sprintf('%.4g ', ($numbers[$j+1] + $yTrans));
}
if ( $command == 'M' ) $firstMove = false;
break;
case 'm':
$newPath .= 'm';
if ( $firstMove ) {
$newPath .= sprintf('%.4g,', ($numbers[0] + $xTrans));
$newPath .= sprintf('%.4g ', ($numbers[1] + $yTrans));
for ( $j = 2; $j < count($numbers); $j += 2 ) {
$newPath .= sprintf('%.4g,', ($numbers[$j]));
$newPath .= sprintf('%.4g ', ($numbers[$j+1]));
}
$firstMove = false;
} else {
for ( $j = 0; $j < count($numbers); $j += 2 ) {
$newPath .= sprintf('%.4g,', ($numbers[$j]));
$newPath .= sprintf('%.4g ', ($numbers[$j+1]));
}
}
break;
case 'a':
list($rx,$ry,$rot,$arc,$sweep,$x,$y) = $numbers;
$newPath .= sprintf('a%.4g,%.4g %.4g,%d,%d %.4g,%.4g', $rx,$ry, $rot,$arc,$sweep, $x,$y);
break;
case 'h':
case 'v': // Pass all unchanged
$newPath .= $command . sprintf('%.4g', $numbers[0]);
break;
case 'l':
case 'q':
case 'c':
$newPath .= $command;
$odd = true;
foreach ( $numbers as $number ) {
$newPath .= (sprintf('%.4g', $number)) . ($odd ? ',':' ');
$odd = !$odd;
}
break;
case 'Z':
case 'z':
$newPath .= $command;
break;
}
$newPath .= ' ';
}
return $newPath;
}
// UTILITY FUNCTION
function matMul ( $a, $b, $c, $d, $e, $f, $x, $y ) {
return array ( (($a * $x) + ($c * $y) + $e), (($b * $x) + ($d * $y) + $f) );
}
function matrixPath( $path, $a, $b, $c, $d, $e, $f ) {
$newPath = '';
$chunks = preg_split('/([a-z])/i', $path, null, PREG_SPLIT_DELIM_CAPTURE );
$firstMove = true;
for ( $i = 0; $i < count($chunks); $i++ ) {
$command = $chunks[$i];
if ( isset($chunks[$i+1]) )
$numbers = getNumbers( $chunks[$i+1] );
switch ( $command ) {
case 'H':
$coords = matMul($a, $b, $c, $d, $e, $f, $numbers[0], 0);
$newPath .= sprintf('l%.4g,%.4g ', $coords[0], $coords[1]);
break;
case 'V':
$coords = matMul($a, $b, $c, $d, $e, $f, 0, $numbers[0]);
$newPath .= sprintf('l%.4g,%.4g ', $coords[0], $coords[1]);
break;
case 'M':
case 'L':
case 'C':
$newPath .= $command;
for ( $j = 0; $j < count($numbers); $j += 2 ) {
$coords = matMul($a, $b, $c, $d, $e, $f, $numbers[$j], $numbers[$j+1]);
$newPath .= sprintf('%.4g,', $coords[0]);
$newPath .= sprintf('%.4g ', $coords[1]);
}
if ( $command == 'M' ) $firstMove = false;
break;
case 'm':
$newPath .= 'm';
if ( $firstMove ) {
$coords = matMul($a, $b, $c, $d, $e, $f, $numbers[0], $numbers[1]);
$newPath .= sprintf('%.4g, %.4g ', $coords[0], $coords[1]);
for ( $j = 2; $j < count($numbers); $j += 2 ) {
$coords = matMul($a, $b, $c, $d, 0, 0, $numbers[$j], $numbers[$j+1]);
$newPath .= sprintf('%.4g,', ($coords[0]));
$newPath .= sprintf('%.4g ', ($coords[1]));
}
$firstMove = false;
} else {
for ( $j = 0; $j < count($numbers); $j += 2 ) {
$coords = matMul($a, $b, $c, $d, 0, 0, $numbers[$j], $numbers[$j+1]);
$newPath .= sprintf('%.4g,', ($coords[0]));
$newPath .= sprintf('%.4g ', ($coords[1]));
}
}
break;
case 'a':
/* THE CORRECT APPLICATION OF MATRIX MULTIPLICATION TO ARC COMMANDS IS LEFT AS AN EXERCISE
FOR THE READER */
list($rx,$ry,$rot,$arc,$sweep,$x,$y) = $numbers;
$newPath .= sprintf('a%.4g,%.4g %.4g,%d,%d %.4g,%.4g', $rx,$ry, $rot,$arc,$sweep, $x,$y);
// warningMessage('no matrix apply for arc');
break;
case 'h':
$coords = matMul($a, $b, $c, $d, 0, 0, $numbers[0], 0);
$newPath .= sprintf('l%.4g,%.4g ', $coords[0], $coords[1]);
break;
case 'v':
$coords = matMul($a, $b, $c, $d, 0, 0, 0, $numbers[0]);
$newPath .= sprintf('l%.4g,%.4g ', $coords[0], $coords[1]);
break;
case 'l':
case 'q':
case 'c':
$newPath .= $command;
for ( $j = 0; $j < count($numbers); $j += 2 ) {
$coords = matMul($a, $b, $c, $d, 0, 0, $numbers[$j], $numbers[$j+1]);
$newPath .= sprintf('%.4g,', ($coords[0]));
$newPath .= sprintf('%.4g ', ($coords[1]));
}
break;
case 'Z':
case 'z':
$newPath .= $command;
break;
}
$newPath .= ' ';
}
return $newPath;
}
function mirrorPath ( $path, $axis = 'x' ) {
$newPath = '';
$chunks = preg_split('/([a-z])/i', $path, null, PREG_SPLIT_DELIM_CAPTURE );
for ( $i = 0; $i < count($chunks); $i++ ) {
$command = $chunks[$i];
if ( isset($chunks[$i+1]) ) {
$numbers = getNumbers($chunks[$i+1]);
}
switch ( strtolower($command) ) {
case 'h':
$x = $numbers[0];
if ($axis == 'x')
$newPath .= sprintf('h%.4g',$x);
else
$newPath .= sprintf('h%.4g',($x * -1));
break;
case 'v':
$y = $numbers[0];
if ($axis == 'x')
$newPath .= sprintf('v%.4g',($y * -1));
else
$newPath .= sprintf('v%.4g',$y);
break;
case 'a':
list($rx,$ry,$rot,$arc,$swap,$x,$y) = $numbers;
if ( $axis == 'x' ) {
$swap = $swap == 1? 0 : 1;
$newPath .= sprintf('a%.4g,%.4g %.4g,%d,%d %.4g,%.4g', $rx,$ry, (($rot + 180)%360),$arc,$swap, $x,$y);
} else {
$x *= -1;
$newPath .= sprintf('a%.4g,%.4g %.4g,%d,%d %.4g,%.4g', $rx,$ry,(($rot + 180)%360),$arc,$swap, $x,$y);
}
break;
case 'l':
case 'q':
case 'c':
case 's':
case 'm':
$newPath .= $command;
for ( $j = 0; $j < count($numbers); $j += 2 ) {
if ( $command == 'M' ) {
$newX = $numbers[$j];
$newY = $numbers[$j+1];
} elseif ( $axis == 'x' ) {
$newX = $numbers[$j];
$newY = $numbers[$j+1] * -1;
} else {
$newX = $numbers[$j] * -1;
$newY = $numbers[$j+1];
}
$newPath .= sprintf ('%.4g,%.4g ', $newX, $newY);
}
break;
case 'z':
$newPath .= $command;
break;
}
$newPath .= ' ';
}
return $newPath;
}
function scalePath ( $path, $xScale = 1, $yScale = 1 ) {
$newPath = '';
$chunks = preg_split('/([a-z])/i', $path, null, PREG_SPLIT_DELIM_CAPTURE );
for ( $i = 0; $i < count($chunks); $i++ ) {
$command = $chunks[$i];
if ( isset($chunks[$i+1]) ) {
$numbers = getNumbers($chunks[$i+1]);
}
switch ( strtolower($command) ) {
case 'h':
$x = $numbers[0];
if ($xScale == 1)
$newPath .= sprintf('h%.4g',$x);
else
$newPath .= sprintf('h%.4g',($x * $xScale));
break;
case 'v':
$y = $numbers[0];
if ($yScale == 1)
$newPath .= sprintf('v%.4g',$y);
else
$newPath .= sprintf('v%.4g',($y * $yScale));
break;
case 'a':
list($rx,$ry,$rot,$arc,$swap,$x,$y) = $numbers;
if ( $xScale != 1 ) {
$rx *= $xScale;
$x *= $xScale;
}
if ( $yScale != 1 ) {
$ry *= $yScale;
$y *= $yScale;
}
$newPath .= sprintf('a%.4g,%.4g %.4g,%d,%d %.4g,%.4g', $rx,$ry, $rot,$arc,$swap, $x,$y);
break;
case 'c':
case 'l':
case 'q':
case 's':
case 'm':
$newPath .= $command;
for ( $j = 0; $j < count($numbers); $j += 2 ) {
$newY = $numbers[$j+1];
$newX = $numbers[$j];
if ( $xScale != 1 ) {
$newX *= $xScale;
}
if ( $yScale != 1 ) {
$newY *= $yScale;
}
$newPath .= sprintf ('%.4g,%.4g ', $newX, $newY);
}
break;
case 'z':
$newPath .= $command;
break;
}
$newPath .= ' ';
}
return $newPath;
}
function makeRelativePath ( $path, $Xoff = 0, $Yoff = 0 ) {
$newPath = '';
$chunks = preg_split('/([a-z])/i', $path, null, PREG_SPLIT_DELIM_CAPTURE );
$curX = $Xoff; $curY = $Yoff;
for ( $i = 0; $i < count($chunks); $i++ ) {
$command = $chunks[$i];
if ( isset($chunks[$i+1]) ) {
$numbers = getNumbers($chunks[$i+1]);
}
switch ( $command ) {
case 'h':
$x = $numbers[0];
$curX += $x;
$newPath .= sprintf('h%.4g',$x);
break;
case 'H':
$x = $numbers[0];
$newPath .= sprintf('h%.4g',($x - $curX));
$curX = $x;
break;
case 'v':
$y = $numbers[0];
$curY += $y;
$newPath .= sprintf('v%.4g',$y);
break;
case 'V':
$y = $numbers[0];
$newPath .= sprintf('v%.4g',($y - $curY));
$curY = $y;
break;
case 'a':
list($rx,$ry,$rot,$arc,$swap,$x,$y) = $numbers;
$newPath = sprintf('a%.4g,%.4g %.4g,%d,%d %.4g,%.4g', $rx,$ry, $rot,$arc,$swap, $x,$y);
$curX += $x;
$curY += $y;
break;
case 'A':
list($rx,$ry,$rot,$arc,$swap,$x,$y) = $numbers;
$newPath = sprintf('a%.4g,%.4g %.4g,%d,%d %.4g,%.4g', $rx,$ry,$rot,$arc,$swap, ($x - $curX),($y - $curY));
$curX = $x;
$curY = $y;
break;
case 'c':
$tempPath = 'c';
for ( $j = 0; $j < count($numbers); $j += 6 ) {
$cpStartX = $numbers[$j+0];
$cpStartY = $numbers[$j+1];
$cpEndX = $numbers[$j+2];
$cpEndY = $numbers[$j+3];
$endX = $numbers[$j+4];
$endY = $numbers[$j+5];
$tempPath .= sprintf('%.4g,%.4g %.4g,%.4g %.4g,%.4g ',$cpStartX,$cpStartY, $cpEndX,$cpEndY, $endX,$endY);
$curX += $endX;
$curY += $endY;
}
$newPath .= $tempPath;
break;
case 'C': // $newPath .= "($curX,$curY)";
$tempPath = 'c';
for ( $j = 0; $j < count($numbers); $j += 6 ) {
$cpStartX = $numbers[$j+0];
$cpStartY = $numbers[$j+1];
$cpEndX = $numbers[$j+2];
$cpEndY = $numbers[$j+3];
$endX = $numbers[$j+4];
$endY = $numbers[$j+5];
$tempPath .= sprintf('%.4g,%.4g %.4g,%.4g %.4g,%.4g ', ($cpStartX - $curX), ($cpStartY - $curY),($cpEndX - $curX),($cpEndY - $curY),($endX - $curX),($endY - $curY));
$curX = $endX;
$curY = $endY;
}
$newPath .= $tempPath;
break;
case 'l':
case 'm':
$tempPath = $command;
for ( $j = 0; $j < count($numbers); $j += 2 ) {
$y = $numbers[$j+1];
$x = $numbers[$j];
$tempPath .= sprintf('%.4g,%.4g ',$x,$y);
$curX += $x;
$curY += $y;
}
$newPath .= $tempPath;
break;
case 'L':
case 'M':
$tempPath = strtolower($command);
for ( $j = 0; $j < count($numbers); $j += 2 ) {
$y = $numbers[$j+1];
$x = $numbers[$j];
$tempPath .= sprintf('%.4g,%.4g ', ($x - $curX),($y - $curY));
$curX = $x;
$curY = $y;
}
$newPath .= $tempPath;
break;
case 'z':
case 'Z':
$newPath .= $command;
break;
}
$newPath .= ' ';
}
//draw_message('warning',$newPath);
return $newPath;
}
function reversePath ( $path ) {
$newPath = '';
$chunks = preg_split('/([a-z])/i', $path, null, PREG_SPLIT_DELIM_CAPTURE );
for ( $i = 0; $i < count($chunks); $i++ ) {
$command = $chunks[$i];
if ( isset($chunks[$i+1]) ) {
$numbers = getNumbers($chunks[$i+1]);
}
switch ( strtolower($command) ) {
case 'h':
$x = $numbers[0];
$newPath = sprintf('h%.4g',($x * -1)) . $newPath;
break;
case 'v':
$y = $numbers[0];
$newPath = sprintf('v%.4g',($y * -1)) . $newPath;
break;
case 'a':
list($rx,$ry,$rot,$arc,$swap,$x,$y) = $numbers;
$newPath = sprintf('a%.4g,%.4g %.4g,%d,%d %.4g,%.4g',$rx,$ry, $rot,$arc,$swap,($x * -1),($y * -1)) . $newPath;
break;
case 'c':
$tempPath = '';
for ( $j = 0; $j < count($numbers); $j += 6 ) {
$cpStartX = $numbers[$j+0];
$cpStartY = $numbers[$j+1];
$cpEndX = $numbers[$j+2];
$cpEndY = $numbers[$j+3];
$endX = $numbers[$j+4];
$endY = $numbers[$j+5];
$newCPStart = sprintf( '%.4g,%.4g ', ($cpEndX - $endX),($cpEndY - $endY));
$newCPEnd = sprintf( '%.4g,%.4g ', ($cpStartX - $endX),($cpStartY - $endY));
$newEnd = sprintf( '%.4g,%.4g ', ($endX * -1),($endY * -1));
$tempPath = $newCPStart . $newCPEnd . $newEnd . $tempPath;
}
$tempPath = $command . $tempPath;
$newPath = $tempPath . $newPath;
break;
case 'l':
case 'q':
case 's':
$tempPath = '';
for ( $j = 0; $j < count($numbers); $j += 2 ) {
$newY = $numbers[$j+1] * -1;
$newX = $numbers[$j] * -1;
$tempPath = sprintf('%.4g,%.4g ', $newX,$newY) . $tempPath;
}
$tempPath = $command . $tempPath;
$newPath = $tempPath . $newPath;
break;
case 'z':
$newPath .= $command;
break;
}
$newPath .= ' ';
}
return $newPath;
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment