@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.
400 lines (370 loc) • 13.3 kB
text/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>,
}