Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
A
Axiom
/Marketplace/christiangeorgelucas/svg-path-tools
Sign in

christiangeorgelucas/svg-path-tools

v0.1.1TypeScript

The svg-path-tools package provides deterministic parsing and geometric manipulation of SVG path data, specifically the 'd' attribute mini-language. It enables users to perform various operations such as parsing, normalization, arc-to-bezier conversion, and bounding box calculations without any randomness or rendering, making it ideal for applications requiring precise path manipulations.

svgpath-manipulationgeometrytypescriptdeterministicparsingnormalizationbezierbounding-box

Use cases

  • Parse SVG path data for further analysis
  • Compute the bounding box of complex SVG shapes
  • Convert elliptical arcs to cubic Bezier curves
  • Normalize SVG paths for consistent rendering
  • Sample points and tangents along SVG paths

Nodes (11)

ConvertArcsToBeziersunary
SvgPath·SvgPath

The ConvertArcsToBeziers node transforms elliptical arc commands in SVG paths into equivalent cubic Bézier curves, ensuring compatibility with renderers that do not support the arc command. If the input path contains no arcs, it remains unchanged except for normalization of absolute coordinates.

View source
import { SvgPath } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated } from './svg_helpers';

/**
 * Replace every elliptical-arc command (A/a) with one or more equivalent
 * cubic Bézier curves (C), for renderers/toolchains that don't support the
 * arc command directly. Paths with no arcs pass through unchanged.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function convertArcsToBeziers(ax: AxiomContext, input: SvgPath): SvgPath {
  const out = new SvgPath();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  out.setD(parsed.sp.unarc().toString());
  return out;
}
GetBoundingBoxunary
SvgPath·BoundingBox

The GetBoundingBox node computes the axis-aligned bounding box of an SVG path, accurately reflecting the true geometric extent of curves rather than just their control points. This ensures that the bounding box encompasses all rendered elements of the path.

View source
import { svgPathBbox } from 'svg-path-bbox';
import { SvgPath, BoundingBox } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated, makeError, errMsg } from './svg_helpers';

/**
 * Compute the axis-aligned bounding box of everything a path actually
 * renders — curves are measured on their true geometric extent (their real
 * min/max over the curve), not the loose hull of their control points.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function getBoundingBox(ax: AxiomContext, input: SvgPath): BoundingBox {
  const out = new BoundingBox();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  let box: [number, number, number, number];
  try {
    // svg-path-bbox has its own (separate) path parser; we already validated
    // syntactic well-formedness above via svgpath, so a throw here would be
    // unexpected library-internal behavior, not malformed input — still
    // caught and reported structurally rather than left to crash the node.
    box = svgPathBbox(parsed.sp.toString());
  } catch (e) {
    out.setError(makeError('PARSE_ERROR', `failed to compute bounding box: ${errMsg(e)}`));
    return out;
  }

  const [minX, minY, maxX, maxY] = box;
  if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
    // svg-path-bbox's degenerate-input signal (e.g. a path with zero
    // renderable geometry) rather than a real box.
    out.setError(makeError('PARSE_ERROR', 'path has no renderable geometry to bound'));
    return out;
  }

  out.setMinX(minX);
  out.setMinY(minY);
  out.setMaxX(maxX);
  out.setMaxY(maxY);
  out.setWidth(maxX - minX);
  out.setHeight(maxY - minY);
  return out;
}
GetLengthunary
SvgPath·LengthResult

The GetLength node computes the total rendered arc length of a given SVG path, accurately summing the lengths of all line and curve segments, including arcs, across all subpaths.

View source
import { svgPathProperties } from 'svg-path-properties';
import { SvgPath, LengthResult } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated, makeError, errMsg } from './svg_helpers';

/**
 * Compute a path's total rendered arc length — the true length along every
 * line and curve segment, including arcs, summed across every subpath.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function getLength(ax: AxiomContext, input: SvgPath): LengthResult {
  const out = new LengthResult();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  try {
    const properties = new svgPathProperties(parsed.sp.toString());
    out.setLength(properties.getTotalLength());
  } catch (e) {
    // svg-path-properties has its own (separate, stricter) parser and can
    // throw on syntax our own svgpath-based validation above let through.
    out.setError(makeError('PARSE_ERROR', `failed to measure path length: ${errMsg(e)}`));
  }
  return out;
}
NormalizePathunary
NormalizeInput·SvgPath

The NormalizePath node rewrites a path's command vocabulary without altering its rendered shape by converting relative commands to absolute ones and expanding shorthand smooth-curve commands to their full form.

View source
import { NormalizeInput, SvgPath } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated } from './svg_helpers';

/**
 * Rewrite a path's command vocabulary without changing its rendered shape:
 * to_absolute converts every relative (lowercase) command to its absolute
 * (uppercase) form, expand_shorthand rewrites smooth-curve shorthand (S/T)
 * to its full form (C/Q). Both default false (a no-op copy) — set either or
 * both to actually normalize.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function normalizePath(ax: AxiomContext, input: NormalizeInput): SvgPath {
  const out = new SvgPath();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  let path: ReturnType<typeof parsed.sp.abs> = parsed.sp;
  if (input.getToAbsolute()) path = path.abs();
  if (input.getExpandShorthand()) path = path.unshort();

  out.setD(path.toString());
  return out;
}
ParsePathunary
SvgPath·ParsePathResult

The ParsePath node processes an SVG path's `d` string and converts it into an ordered list of command segments, each indicating whether it is absolute or relative, along with its raw parameters and resolved end point coordinates.

View source
import { SvgPath, ParsePathResult, PathSegment } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated } from './svg_helpers';

/**
 * Parse a path's `d` string into its ordered list of command segments
 * (M/L/H/V/C/S/Q/T/A/Z), each carrying whether it was written absolute or
 * relative in the source, its raw numeric parameters in source order, and
 * the absolute (x, y) of its resolved end point.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function parsePath(ax: AxiomContext, input: SvgPath): ParsePathResult {
  const out = new ParsePathResult();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  const segments: PathSegment[] = [];
  parsed.sp.iterate((s: any, _index: number, x: number, y: number) => {
    const rawCommand: string = s[0];
    const upper = rawCommand.toUpperCase();
    const relative = rawCommand !== upper;

    const seg = new PathSegment();
    seg.setCommand(upper);
    seg.setRelative(relative);
    seg.setParamsList(s.slice(1));

    // Resolve this segment's absolute end point, mirroring the same
    // absolute-coordinate bookkeeping svgpath's own iterate() does
    // internally (it does not surface the end point directly).
    let endX = x;
    let endY = y;
    switch (upper) {
      case 'M':
      case 'L':
      case 'T':
        endX = s[1] + (relative ? x : 0);
        endY = s[2] + (relative ? y : 0);
        break;
      case 'H':
        endX = s[1] + (relative ? x : 0);
        endY = y;
        break;
      case 'V':
        endX = x;
        endY = s[1] + (relative ? y : 0);
        break;
      case 'C':
        endX = s[5] + (relative ? x : 0);
        endY = s[6] + (relative ? y : 0);
        break;
      case 'S':
      case 'Q':
        endX = s[3] + (relative ? x : 0);
        endY = s[4] + (relative ? y : 0);
        break;
      case 'A':
        endX = s[6] + (relative ? x : 0);
        endY = s[7] + (relative ? y : 0);
        break;
      case 'Z':
        // A close segment carries no parameters; its true resolved end
        // point (the current subpath's start) is bookkeeping internal to
        // svgpath's own iterate() and not surfaced here — callers should
        // not depend on end_x/end_y for "Z".
        break;
      default:
        break;
    }

    seg.setEndX(endX);
    seg.setEndY(endY);
    segments.push(seg);
  });

  out.setSegmentsList(segments);
  return out;
}
ReversePathunary
SvgPath·SvgPath

The ReversePath node reverses the direction of travel of an SVG path while maintaining its rendered shape, ensuring that each segment is emitted in reverse order with endpoints and Bézier control points swapped. This allows for sampling the reversed path at parameter t to yield the same point as sampling the original path at (1 - t).

View source
import { SvgPath } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated, reversePathD, makeError, errMsg } from './svg_helpers';

/**
 * Reverse a path's direction of travel while preserving its exact rendered
 * shape: each subpath's segments are re-emitted in reverse order with
 * endpoints and Bézier control points swapped accordingly, so sampling the
 * reversed path at parameter t yields the same point as sampling the
 * original at (1 - t). Closed subpaths (ending in Z) stay closed and keep
 * their start point; open subpaths start from their old end point.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function reversePath(ax: AxiomContext, input: SvgPath): SvgPath {
  const out = new SvgPath();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  try {
    out.setD(reversePathD(input.getD()));
  } catch (e) {
    out.setError(makeError('PARSE_ERROR', `failed to reverse path: ${errMsg(e)}`));
  }
  return out;
}
RotatePathunary
RotateInput·SvgPath

The RotatePath node rotates a given SVG path by a specified angle in degrees, following the SVG's rotate() convention, around a defined center point. If the center point is not provided, the rotation defaults to the origin (0, 0).

View source
import { RotateInput, SvgPath } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated, makeError, isFiniteNumber } from './svg_helpers';

/**
 * Rotate a path by angle_degrees (clockwise, matching SVG's rotate()
 * convention) about the point (cx, cy), which defaults to the origin
 * (0, 0) when omitted.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function rotatePath(ax: AxiomContext, input: RotateInput): SvgPath {
  const out = new SvgPath();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  const angle = input.getAngleDegrees();
  const cx = input.getCx();
  const cy = input.getCy();
  if (!isFiniteNumber(angle) || !isFiniteNumber(cx) || !isFiniteNumber(cy)) {
    out.setError(makeError('INVALID_PARAM', 'angle_degrees, cx and cy must be finite numbers'));
    return out;
  }

  out.setD(parsed.sp.rotate(angle, cx, cy).toString());
  return out;
}
SampleAtunary
SampleAtInput·SampleAtResult

The SampleAt node samples the point and unit tangent vector at specified normalized parameter values along a path, efficiently performing a single evaluation pass for all requested values. It maps these parameter values to their corresponding arc-length positions on the path.

View source
import { svgPathProperties } from 'svg-path-properties';
import { SampleAtInput, SampleAtResult, SamplePoint, Point2 } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated, makeError, errMsg, isFiniteNumber } from './svg_helpers';

/**
 * Sample the point and unit tangent (direction of travel) at one or more
 * normalized parameter values t in [0, 1], where t is mapped to the
 * arc-length position t * total_length (0 = path start, 1 = path end) — a
 * single evaluation pass shared across every requested t.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function sampleAt(ax: AxiomContext, input: SampleAtInput): SampleAtResult {
  const out = new SampleAtResult();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  const tList = input.getTList();
  if (tList.length === 0) {
    out.setError(makeError('INVALID_PARAM', 't must contain at least one value'));
    return out;
  }
  for (const t of tList) {
    if (!isFiniteNumber(t) || t < 0 || t > 1) {
      out.setError(makeError('INVALID_PARAM', `t values must be finite numbers in [0, 1]; got ${t}`));
      return out;
    }
  }

  let properties;
  try {
    properties = new svgPathProperties(parsed.sp.toString());
  } catch (e) {
    out.setError(makeError('PARSE_ERROR', `failed to sample path: ${errMsg(e)}`));
    return out;
  }

  const totalLength = properties.getTotalLength();
  const samples: SamplePoint[] = [];
  for (const t of tList) {
    const atLength = t * totalLength;
    const p = properties.getPointAtLength(atLength);
    const tan = properties.getTangentAtLength(atLength);

    const point = new Point2();
    point.setX(p.x);
    point.setY(p.y);
    const tangent = new Point2();
    tangent.setX(tan.x);
    tangent.setY(tan.y);

    const sample = new SamplePoint();
    sample.setT(t);
    sample.setPoint(point);
    sample.setTangent(tangent);
    samples.push(sample);
  }

  out.setSamplesList(samples);
  return out;
}
ScalePathunary
ScaleInput·SvgPath

The ScalePath node scales every point in a given SVG path by specified factors (sx, sy) about the origin. It requires both scaling factors to be explicitly defined for accurate transformation.

View source
import { ScaleInput, SvgPath } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated, makeError, isFiniteNumber } from './svg_helpers';

/**
 * Scale every point in a path by (sx, sy) about the origin. Both factors
 * must be given explicitly — pass the same value for both to scale
 * uniformly; there is no implicit uniform-scale default.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function scalePath(ax: AxiomContext, input: ScaleInput): SvgPath {
  const out = new SvgPath();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  const sx = input.getSx();
  const sy = input.getSy();
  if (!isFiniteNumber(sx) || !isFiniteNumber(sy)) {
    out.setError(makeError('INVALID_PARAM', 'sx and sy must be finite numbers'));
    return out;
  }

  out.setD(parsed.sp.scale(sx, sy).toString());
  return out;
}
TransformPathunary
TransformInput·SvgPath

The TransformPath node applies a specified SVG transform-list string to a given path, enabling complex transformations such as translation, scaling, rotation, and skewing based on the SVG `transform` attribute grammar.

View source
import { TransformInput, SvgPath } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated, makeError, isValidTransformList } from './svg_helpers';

/**
 * Apply an arbitrary SVG transform-list string (e.g.
 * "translate(10,20) scale(2) rotate(45) skewX(10)") to a path, per the SVG
 * `transform` attribute grammar — the general-purpose escape hatch for
 * transforms (skew, raw matrix()) that TranslatePath/ScalePath/RotatePath's
 * structured parameters don't cover.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function transformPath(ax: AxiomContext, input: TransformInput): SvgPath {
  const out = new SvgPath();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  const transform = input.getTransform();
  if (!isValidTransformList(transform)) {
    // svgpath's own .transform() silently no-ops on a string it doesn't
    // recognize instead of reporting a problem, so an invalid transform
    // must be caught here rather than left to the library.
    out.setError(
      makeError(
        'INVALID_PARAM',
        'transform must be a non-empty SVG transform-list, e.g. "translate(10,20) scale(2) rotate(45)"',
      ),
    );
    return out;
  }

  out.setD(parsed.sp.transform(transform).toString());
  return out;
}
TranslatePathunary
TranslateInput·SvgPath

The TranslatePath node shifts every point in a given SVG path by specified distances in the x (dx) and y (dy) directions. It processes the input path and applies the translation to generate a new SVG path output.

View source
import { TranslateInput, SvgPath } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseValidated, makeError, isFiniteNumber } from './svg_helpers';

/**
 * Translate (shift) every point in a path by (dx, dy).
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function translatePath(ax: AxiomContext, input: TranslateInput): SvgPath {
  const out = new SvgPath();
  const parsed = parseValidated(input.getD());
  if ('error' in parsed) {
    out.setError(parsed.error);
    return out;
  }

  const dx = input.getDx();
  const dy = input.getDy();
  if (!isFiniteNumber(dx) || !isFiniteNumber(dy)) {
    out.setError(makeError('INVALID_PARAM', 'dx and dy must be finite numbers'));
    return out;
  }

  out.setD(parsed.sp.translate(dx, dy).toString());
  return out;
}

Messages (15)

Download .proto
BoundingBox
min_x:double
min_y:double
max_x:double
max_y:double
width:double
height:double
error:SvgError
LengthResult
length:double
error:SvgError
NormalizeInput
d:string
to_absolute:bool

A boolean flag indicating whether to convert all relative commands to absolute commands.

expand_shorthand:bool

A boolean flag indicating whether to expand shorthand smooth-curve commands to their full form.

ParsePathResult
segments:[]PathSegment

An array of path segments, each detailing the command type, its relative/absolute nature, parameters, and resolved end point coordinates.

error:SvgError
PathSegment
command:string
relative:bool
params:[]double
end_x:double
end_y:double
Point2
x:double
y:double
RotateInput
d:string

The SVG path data that will be rotated.

angle_degrees:double

The angle in degrees by which the path will be rotated clockwise.

cx:double

The x-coordinate of the center point around which the path will be rotated; defaults to 0 if omitted.

cy:double

The y-coordinate of the center point around which the path will be rotated; defaults to 0 if omitted.

SampleAtInput
d:string
t:[]double
SampleAtResult
samples:[]SamplePoint
error:SvgError
SamplePoint
t:double
point:Point2
tangent:Point2
ScaleInput
d:string

The SVG path data that needs to be scaled.

sx:double

The scaling factor along the x-axis.

sy:double

The scaling factor along the y-axis.

SvgError
code:string
message:string
SvgPath
d:string

The SVG path data that will be rotated.

error:SvgError
TransformInput
d:string

The original path data that will be transformed.

transform:string

The SVG transform-list string that defines the transformations to be applied to the path.

TranslateInput
d:string

The SVG path data to be translated.

dx:double

The distance to shift the path in the x direction.

dy:double

The distance to shift the path in the y direction.

Use as a tool in any AI agent

christiangeorgelucas/svg-path-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.

claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"

Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.

MCP server setup & other clients →

Use this package

Sign in to import christiangeorgelucas/svg-path-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.

Sign in to useOpen editor

Stats

Error rate (1h)0.0%
Back to marketplace