src/Rasterization/Path/PathParser.php
<?php
namespace SVG\Rasterization\Path;
use SVG\Shims\Str;
/**
* This class can convert SVG path description strings into arrays of distinct
* commands + their arguments.
*/
class PathParser
{
/**
* @var int[] $commandLengths A map of command ids to their argument counts.
*/
private static array $commandLengths = [
'M' => 2, 'm' => 2, // MoveTo
'L' => 2, 'l' => 2, // LineTo
'H' => 1, 'h' => 1, // LineToHorizontal
'V' => 1, 'v' => 1, // LineToVertical
'C' => 6, 'c' => 6, // CurveToCubic
'S' => 4, 's' => 4, // CurveToCubicSmooth
'Q' => 4, 'q' => 4, // CurveToQuadratic
'T' => 2, 't' => 2, // CurveToQuadraticSmooth
'A' => 7, 'a' => 7, // ArcTo
'Z' => 0, 'z' => 0, // ClosePath
];
/**
* Parses a path description into a consecutive array of commands.
*
* The commands themselves are associative arrays with 2 keys:
* - string 'id': the command's id, e.g. 'M' or 'c' or ...
* - float[] 'args': consecutive array of command arguments
*
* @param string $description The SVG path description to parse.
*
* @return array[] An array of commands (structure: see above).
*/
public function parse(string $description): array
{
$commands = [];
$matches = [];
$idString = implode('', array_keys(self::$commandLengths));
preg_match_all('/([' . $idString . '])([^' . $idString . ']*)/', $description, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$id = $match[1];
$args = $this->splitArguments($match[2]);
$success = $this->parseCommandChain($id, $args, $commands);
if (!$success) {
break;
}
}
return $commands;
}
/**
* Splits the given arguments string into an array of strings.
*
* @param string $str The string to split.
*
* @return string[] The split arguments.
*/
private function splitArguments(string $str): array
{
$str = Str::trim($str);
$args = [];
if ($str !== '') {
preg_match_all('/[+-]?(\d*\.\d+|\d+)(e[+-]?\d+)?/', $str, $args);
$args = $args[0];
}
return $args;
}
/**
* Groups the given argument chain into sets and constructs a command array
* for every set, which is then pushed to the given array reference.
*
* @param string $id The command id that the arguments belong to.
* @param string[] $args All of the arguments following the command.
* @param array[] $commands The array to add all parsed commands to.
*
* @return bool Whether the command is known AND the arg count is correct.
*/
private function parseCommandChain(string $id, array $args, array &$commands): bool
{
if (!isset(self::$commandLengths[$id])) {
// unknown command
return false;
}
$length = self::$commandLengths[$id];
if ($length === 0) {
if (count($args) > 0) {
return false;
}
$commands[] = [
'id' => $id,
'args' => $args,
];
return true;
}
foreach (array_chunk($args, $length) as $subArgs) {
if (count($subArgs) !== $length) {
return false;
}
$commands[] = [
'id' => $id,
'args' => array_map('floatval', $subArgs),
];
// If a MoveTo command is followed by additional coordinate pairs,
// those are interpreted as implicit LineTo commands
if ($id === 'M') {
$id = 'L';
} elseif ($id === 'm') {
$id = 'l';
}
}
return true;
}
}