Development

SVG import

Copy Markdown

How static SVG content is parsed into Inkfinite native shapes and assets.

Inkfinite parses the supported static SVG subset in Rust and maps it to native shape properties. The importer does not retain an SVG-specific document model. Its output is a normalized tree. build_svg_import_transaction turns that result into one ordered transaction containing the source asset, extracted assets, a root container, nested groups, and native shapes.

Import boundary

inkfinite_core::svg_import exposes two entry points:

  • parse_svg(&str) parses UTF-8 SVG text.
  • import_svg(impl AsRef<[u8]>) accepts SVG text or UTF-8 file bytes.

Both functions return an SvgImport value:

SvgImport {
    view_box: Option<SvgViewBox>,
    root: SvgGroup,
    source_asset: SvgAsset,
    assets: Vec<SvgAsset>,
    warnings: Vec<SvgImportWarning>,
}

SvgGroup retains its source ID, parent-relative transform, opacity, calculated size, and ordered SvgImportNode children. A node is a group, a native SvgShape, or an SvgImage referencing an extracted asset. Source IDs are hints for the transaction layer. Callers assign document record IDs when they create the import transaction.

source_asset is the exact UTF-8 input as an image/svg+xml asset with a content-addressed ID. assets contains the embedded raster assets referenced by image nodes. The source asset remains available for provenance and re-import even when the native tree omits unsupported content.

Native mappings

SVG elementImport result
gSvgGroup, which maps to a native container
rectrect shape with width, height, radius, fill, and stroke
circleellipse shape with equal width and height
ellipseellipse shape
lineline shape with local endpoints, stroke, and width
polygonClosed path shape
polylineOpen path shape
pathpath shape
texttext shape
embedded raster imageSvgImage plus an embedded SvgAsset

The importer handles SVG coordinates in user units. Numeric lengths can use px, pt, pc, mm, cm, in, or percentages when the root has a view box. Negative coordinates are valid, and negative dimensions are rejected.

Path normalization

SVG path data is normalized to the native path representation described in the native path geometry guide:

  • relative and absolute move, line, horizontal, and vertical commands become move and line segments
  • quadratic, cubic, smooth quadratic, and smooth cubic commands become native quadratic and cubic segments
  • elliptical arcs become one or more cubic Bézier segments
  • Z sets the current subpath’s closed flag
  • a later command after Z starts a new normalized subpath at the closing point
  • fill-rule="nonzero" and fill-rule="evenodd" are preserved

Path parsing rejects malformed command data and produces no partial path.

Transforms

Each group keeps its own SVG transform. A shape keeps its element transform and combines it with its local geometry origin. Nested groups therefore compose in the same order as the SVG hierarchy.

The native transform model represents translation, rotation, and scale. SVG transform lists and matrices are accepted when they decompose into that model. Skewed matrices and zero scales return an error rather than changing the geometry silently.

Painting and opacity

Presentation attributes and declarations in style are resolved together with inherited values. The importer preserves supported fill, stroke, stroke-width, fill-rule, fill-opacity, stroke-opacity, and opacity values. SVG defaults are a black fill and no stroke. none and transparent become absent native paint values.

Supported linear and radial gradients are converted to native paint properties and remain editable after import. Basic user-space clip paths, path-based alpha and luminance masks, and the initial filter subset (blur, colour adjustments, opacity, and drop shadows) are stored as native effect properties. Unsupported clip units, mask content, and filter primitives remain visible through the sanitized static fallback when they affect the visual result. Stylesheet blocks, event-handler attributes, scripts, and SVG animation elements are removed with warnings. External image URLs and other resource references are also omitted. No script, animation, stylesheet, or resource is executed, inserted into a live DOM, or fetched during import. The retained source asset is input data for provenance and future re-import, not executable document content.

Text

Simple SVG text becomes one native text shape. The importer uses the first x and y value as the native text origin, copies the first font family, font-size, and fill, and concatenates descendant text nodes. Nested tspan elements contribute their text but do not create separate shapes. Text layout, text paths, anchors, and rich span styling are not represented yet.

Embedded images

The importer accepts embedded PNG, JPEG, GIF, and WebP data URLs. It decodes the bytes once, creates a content-addressed SvgAsset with a SHA-256 digest, and reuses that asset when multiple image elements contain the same bytes. External URLs and unsupported media types are skipped with warnings. The parser does not fetch resources.

SvgImage nodes retain the source element’s position, size, transform, opacity, and asset ID. The SVG transaction maps each node to a native image shape that references the extracted asset.

Unsupported content and security

The importer is a static parser. Named unsupported visual features are reported through typed SvgImportWarning::UnsupportedFeature values. Other unsupported elements use SvgImportWarning::UnsupportedElement. If unsupported content affects the visual result, the importer replaces that SVG’s partial native tree with one image backed by sanitized SVG bytes. The sanitizer retains static SVG geometry, paint servers, clips, masks, and filters. It removes scripts, event handlers, animations, stylesheets, foreignObject, external resources, and unknown elements. The exact original bytes remain in source_asset for provenance.

Desktop imports use the native Tauri dialog plugin to select a path, then Rust reads, parses, and commits the file through the active session. The browser adapter exposes separate Import menu actions for adding an SVG to the active document and creating a document from an SVG file. Pasted SVG code is added to the active document. SVG file bytes and pasted markup cross one reusable worker. The Rust document session parses the bytes, builds the shared SVG transaction, commits one history entry, and returns the new canonical snapshot and editor projection. The browser caches that projection with the canonical IndexedDB bytes and uses it to hydrate the editor. File selection, drag-and-drop, and pasted markup use this path. The web build runs scripts/build-wasm.mjs before Vite packages the worker. It requires the matching wasm-bindgen CLI. The CLI accepts inkfinite import svg FILE --input ARTWORK.svg and can validate the transaction with --dry-run before saving.

Browser WASM facade

@inkfinite/wasm exposes the Rust document session, importer, and deterministic SVG renderer. The browser loads the generated module once inside the shared worker. Import requests transfer their byte buffer to the session. Render requests send one canonical snapshot and one render-options object across the worker boundary. Rust response envelopes and renderer options use the generated types in @inkfinite/bindings/wasm.

The web adapter uses the canonical session snapshot before export. Page and selection filters become Rust renderer options, and renderer warnings become export conversion notes. Canvas drawing and PNG capture stay in TypeScript, where they can use the browser’s frame loop and canvas APIs.

Input safety and failures

The parser accepts at most 16 MiB of UTF-8 input. XML, numeric attributes, transforms, path data, and embedded image data are validated before they enter the result. Malformed input returns an error rather than a partial import. The WASM response preserves the parser’s structured error code and message across the worker boundary, and a failed import leaves the editor’s busy state and current document unchanged.