@amandaghassaei/flat-svg
Version:
A TypeScript library for converting nested SVGs into a flat list of elements, paths, or segments and applying style-based filters.
340 lines (339 loc) • 13.7 kB
TypeScript
import { SVG_STYLE_DISPLAY, SVG_STYLE_FILL, SVG_STYLE_FILL_OPACITY, SVG_STYLE_OPACITY, SVG_STYLE_STROKE_COLOR, SVG_STYLE_STROKE_LINECAP, SVG_STYLE_STROKE_LINEJOIN, SVG_STYLE_STROKE_MITERLIMIT, SVG_STYLE_STROKE_OPACITY, SVG_STYLE_STROKE_WIDTH, SVG_STYLE_VISIBILITY, SVG_STYLE_COLOR, SVG_STYLE_STROKE_DASH_ARRAY } from './constants-private';
import { SVG_LINE, SVG_RECT, SVG_POLYLINE, SVG_POLYGON, SVG_CIRCLE, SVG_ELLIPSE, SVG_PATH, FLAT_SEGMENT_ARC, FLAT_SEGMENT_BEZIER, FLAT_SEGMENT_LINE, FLAT_SVG_STRAY_VERTEX_MOVETO_ONLY, FLAT_SVG_STRAY_VERTEX_POLYLINE_SINGLE_POINT, FLAT_SVG_STRAY_VERTEX_POLYGON_SINGLE_POINT } from './constants-public';
import { TextNode } from 'svg-parser';
import { Colord } from 'colord';
/**
* SVG length units recognized by flat-svg.
*/
export type FlatSVGUnit = 'in' | 'cm' | 'mm' | 'px' | 'pt' | 'em' | 'ex' | 'pc';
/**
* 2D [x, y] tuple. Treated as immutable — segments may share endpoints by
* reference; copy via `[p[0], p[1]]` if you need to mutate.
*/
export type FlatSVGPoint = readonly [number, number];
/**
* Histogram of fill or stroke colors. `none` counts elements with explicit
* 'none' OR no attribute. `colors` keys are hex (`#RRGGBB`) for valid values,
* raw string for unparseable. Alpha is not represented — colors with different
* opacities collapse into the same hex bucket.
*/
export interface FlatSVGColorHistogram {
readonly none: number;
readonly colors: Readonly<{
[color: string]: number;
}>;
}
/**
* 2D affine transform — SVG `matrix(a b c d e f)`, equivalent to the matrix
* `[[a c e][b d f][0 0 1]]` applied to a column vector `[x y 1]`. Identity
* is `{ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }`.
*/
export interface FlatSVGTransform {
a: number;
b: number;
c: number;
d: number;
e: number;
f: number;
}
/**
* Inheritable SVG/CSS style properties resolved during the cascade. Note that
* `clip-path` / `mask` / `filter` are NOT here — they don't inherit; they're
* surfaced as chains on `FlatElementBase`.
*/
export interface FlatSVGStyle {
[SVG_STYLE_STROKE_WIDTH]?: number;
[SVG_STYLE_STROKE_COLOR]?: string;
[SVG_STYLE_STROKE_OPACITY]?: number;
[SVG_STYLE_STROKE_LINECAP]?: string;
[SVG_STYLE_STROKE_LINEJOIN]?: string;
[SVG_STYLE_STROKE_MITERLIMIT]?: number;
[SVG_STYLE_FILL]?: string;
[SVG_STYLE_FILL_OPACITY]?: number;
[SVG_STYLE_OPACITY]?: number;
[SVG_STYLE_COLOR]?: string;
[SVG_STYLE_STROKE_DASH_ARRAY]?: number | string;
[SVG_STYLE_DISPLAY]?: string;
[SVG_STYLE_VISIBILITY]?: string;
}
/** SVG attributes shared by every shape: style properties + `id` + `class`. */
export interface SVGBaseProperties extends FlatSVGStyle {
id?: string;
class?: string;
}
export interface SVGLineProperties extends SVGBaseProperties {
x1: number;
y1: number;
x2: number;
y2: number;
}
export interface SVGRectProperties extends SVGBaseProperties {
x: number;
y: number;
width: number;
height: number;
}
export interface SVGPolylineProperties extends SVGBaseProperties {
points: string;
}
export interface SVGPolygonProperties extends SVGBaseProperties {
points: string;
}
export interface SVGCircleProperties extends SVGBaseProperties {
r: number;
cx: number;
cy: number;
}
export interface SVGEllipseProperties extends SVGBaseProperties {
rx: number;
ry: number;
cx: number;
cy: number;
}
export interface SVGPathProperties extends SVGBaseProperties {
d: string;
}
/**
* Catch-all attribute bag for any SVG element at parse time — superset of
* `FlatSVGStyle` plus every geometry attribute. `clip-path`/`mask`/`filter`
* appear here because they reach the parse tree, but they surface to consumers
* as chains on `FlatElementBase` (not via this bag); `style` is expanded into
* individual style properties during flattening.
*/
export interface SVGElementProperties extends FlatSVGStyle {
viewBox?: string;
id?: string;
class?: string;
x1?: number;
y1?: number;
x2?: number;
y2?: number;
x?: string;
y?: string;
width?: string;
height?: string;
points?: string;
d?: string;
cx?: number;
cy?: number;
rx?: number;
ry?: number;
r?: number;
transform?: string;
'clip-path'?: string;
mask?: string;
filter?: string;
style?: string;
}
/** svg-parser's `ElementNode`, narrowed to exclude string children. */
export type SVGParserElementNode = {
type: 'element';
tagName?: string | undefined;
properties?: SVGElementProperties;
children: Array<SVGParserNode>;
value?: string | undefined;
metadata?: string | undefined;
};
/** A node in svg-parser's parse tree — text or element. */
export type SVGParserNode = TextNode | SVGParserElementNode;
/**
* A flattened SVG element. IMPORTANT: `properties` coordinates (x/y/cx/cy/
* points/...) are in source coordinates — ancestor transforms are NOT applied.
* `transform` exposes the composed ancestor matrix for callers who want to
* apply it themselves. For viewBox coordinates, use FlatSVG.paths (transform
* baked into `properties.d`) or FlatSVG.segments (baked into p1/p2).
*/
export interface FlatElementBase {
/**
* Ancestor-composed transform matrix (ancestor-first × self). Per SVG spec
* `transform` doesn't inherit as a property — each ancestor's matrix
* multiplies through; flat-svg collapses the composition onto each leaf.
* Immutable; siblings sharing an ancestor may share this matrix by reference.
*/
readonly transform?: Readonly<FlatSVGTransform>;
/**
* `clip-path` attribute values from outermost ancestor to self (e.g.
* `"url(#mask1)"`). Per SVG spec clip-path doesn't inherit — every link in
* the chain clips the result below it, so all entries apply simultaneously.
* Immutable; siblings may share this array by reference.
*/
readonly clipPaths?: ReadonlyArray<string>;
/** `mask` chain — same semantics as clipPaths. */
readonly masks?: ReadonlyArray<string>;
/** `filter` chain — same semantics as clipPaths. */
readonly filters?: ReadonlyArray<string>;
/**
* Space-joined chain of ancestor `<g>` ids, outermost first. Excludes this
* element's own id (on `properties.id`). flat-svg-internal lineage metadata
* — not a real SVG attribute. Resolve from a path/segment via
* sourceElementIndex.
*/
readonly ancestorIds?: string;
/**
* Space-joined chain of ancestor `<g>` classes, outermost first. Excludes
* this element's own class. Multiple classes per ancestor concatenate; per-
* ancestor grouping is not preserved. Same rationale as ancestorIds.
*/
readonly ancestorClasses?: string;
}
export interface FlatLineElement extends FlatElementBase {
readonly tagName: typeof SVG_LINE;
readonly properties: Readonly<SVGLineProperties>;
}
export interface FlatRectElement extends FlatElementBase {
readonly tagName: typeof SVG_RECT;
readonly properties: Readonly<SVGRectProperties>;
}
export interface FlatPolylineElement extends FlatElementBase {
readonly tagName: typeof SVG_POLYLINE;
readonly properties: Readonly<SVGPolylineProperties>;
}
export interface FlatPolygonElement extends FlatElementBase {
readonly tagName: typeof SVG_POLYGON;
readonly properties: Readonly<SVGPolygonProperties>;
}
export interface FlatCircleElement extends FlatElementBase {
readonly tagName: typeof SVG_CIRCLE;
readonly properties: Readonly<SVGCircleProperties>;
}
export interface FlatEllipseElement extends FlatElementBase {
readonly tagName: typeof SVG_ELLIPSE;
readonly properties: Readonly<SVGEllipseProperties>;
}
export interface FlatPathElement extends FlatElementBase {
readonly tagName: typeof SVG_PATH;
readonly properties: Readonly<SVGPathProperties>;
}
/** Flattened SVG geometry element. Discriminated union — narrow by `tagName`. */
export type FlatElement = FlatLineElement | FlatRectElement | FlatPolylineElement | FlatPolygonElement | FlatCircleElement | FlatEllipseElement | FlatPathElement;
/**
* SVG element whose tagName flat-svg can't convert to paths/segments — e.g.
* `<use>`, `<text>`, `<image>`, `<foreignObject>`, nested `<svg>`, unknown
* tags. Same `FlatElementBase` shape as `FlatElement` (transform / clip-path
* chains, ancestor lineage), but `tagName` is open-ended and `properties`
* carries only the inherited cascade styles — the element's own SVG attributes
* (e.g. `<text>` x/y, `<use>` href) are NOT collected here.
*/
export interface FlatUnsupportedElement extends FlatElementBase {
readonly tagName: string;
readonly properties: Readonly<FlatSVGStyle>;
}
/**
* Flattened SVG element re-encoded as a `<path>` — output of path conversion.
* Distinct from `FlatPathElement`, which is the source-element shape when the
* input SVG had a `<path>`. `properties.d` is in viewBox coordinates.
*/
export type FlatPath = {
readonly properties: Readonly<SVGPathProperties>;
/** Index of the source element in FlatSVG.elements. */
readonly sourceElementIndex: number;
};
/** Straight line from p1 to p2. */
export type FlatLineSegment = {
readonly type: typeof FLAT_SEGMENT_LINE;
readonly p1: FlatSVGPoint;
readonly p2: FlatSVGPoint;
readonly properties: Readonly<SVGBaseProperties>;
/** Index of the source element in FlatSVG.elements. */
readonly sourceElementIndex: number;
};
/**
* Quadratic (`controlPoints.length === 1`) or cubic (`controlPoints.length === 2`)
* Bézier from p1 to p2. For cubic, `controlPoints[0]` is the control near p1
* and `controlPoints[1]` is the control near p2 (matching SVG `C` argument order).
*/
export type FlatBezierSegment = {
readonly type: typeof FLAT_SEGMENT_BEZIER;
readonly p1: FlatSVGPoint;
readonly p2: FlatSVGPoint;
readonly controlPoints: ReadonlyArray<FlatSVGPoint>;
readonly properties: Readonly<SVGBaseProperties>;
/** Index of the source element in FlatSVG.elements. */
readonly sourceElementIndex: number;
};
/**
* Elliptical arc from p1 to p2 — semantics match the SVG path `A` command,
* `xAxisRotation` in degrees. Only emitted when `FlatSVG` was constructed
* with `preserveArcs: true`; otherwise arcs are approximated as cubic beziers.
*/
export type FlatArcSegment = {
readonly type: typeof FLAT_SEGMENT_ARC;
readonly p1: FlatSVGPoint;
readonly p2: FlatSVGPoint;
readonly rx: number;
readonly ry: number;
readonly xAxisRotation: number;
readonly largeArcFlag: boolean;
readonly sweepFlag: boolean;
readonly properties: Readonly<SVGBaseProperties>;
/** Index of the source element in FlatSVG.elements. */
readonly sourceElementIndex: number;
};
/** Flattened path segment. Discriminated union — narrow by `type`. */
export type FlatSegment = FlatLineSegment | FlatBezierSegment | FlatArcSegment;
/**
* Spec passed to `FlatSVG.filter*ByStyle`. Matches against the element's
* resolved value for `key`; `tolerance` is permissible distance — Delta
* E2000 in [0, 1] for colors, raw numeric distance for numbers.
*
* `value` type must match `key`:
* - color keys (`stroke`, `fill`, `color`): `string | Colord`
* - numeric keys (`stroke-width`, `opacity`, `stroke-opacity`, ...): `number`
* - `stroke-dasharray`: `number[] | string`
*
* Mismatched pairs throw at runtime.
*/
export type FlatSVGStyleFilter = {
key: string;
value: string | number | number[] | Colord;
tolerance?: number;
};
/** Which kind of degenerate SVG element produced a stray vertex. */
export type FlatSVGStrayVertexCause = typeof FLAT_SVG_STRAY_VERTEX_MOVETO_ONLY | typeof FLAT_SVG_STRAY_VERTEX_POLYLINE_SINGLE_POINT | typeof FLAT_SVG_STRAY_VERTEX_POLYGON_SINGLE_POINT;
/**
* Isolated point from a source element that collapsed (circle r=0, single-
* point polyline, moveto-only path, ...). Position in viewBox coordinates.
*/
export interface FlatSVGStrayVertex {
readonly position: FlatSVGPoint;
readonly cause: FlatSVGStrayVertexCause;
/** Index of the source element in FlatSVG.elements. */
readonly sourceElementIndex: number;
}
/**
* A <defs> child definition (clipPath, mask, gradient, symbol, marker, pattern,
* ...). flat-svg records what's defined but does NOT resolve url(#id)
* references — cross-reference by id yourself.
*/
export interface FlatSVGDef {
/** Element tag, e.g. 'clipPath', 'mask', 'linearGradient', 'symbol'. */
readonly tagName: string;
/** id attribute, if present — what url(#id) references resolve against. */
readonly id?: string;
}
/**
* Aggregated diagnostic output from FlatSVG.analyze().
* All fields are JSON-serializable — no class instances.
*/
export interface FlatSVGAnalysis {
readonly viewBox: readonly [number, number, number, number];
readonly units: FlatSVGUnit;
readonly counts: Readonly<{
elements: number;
paths: number;
segments: number;
zeroLengthSegments: number;
strayVertices: number;
defs: number;
unsupportedElements: number;
}>;
readonly strokeColors: FlatSVGColorHistogram;
readonly fillColors: FlatSVGColorHistogram;
readonly containsClipPaths: boolean;
/** Indices into FlatSVG.segments of zero-length segments. */
readonly zeroLengthSegmentIndices: ReadonlyArray<number>;
readonly strayVertices: ReadonlyArray<FlatSVGStrayVertex>;
/** Elements whose tagName flat-svg can't convert to paths/segments. */
readonly unsupportedElements: ReadonlyArray<FlatUnsupportedElement>;
readonly warnings: ReadonlyArray<string>;
}