UNPKG

@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
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>; }