Repository

lib-svg

Kinogaki's SVG library.

GitHub
kinogaki/lib-svg (private)
Folder
libs/lib-svg/
Crates
2
Rust lines
17,578 in src, 597 in tests
depends on (an edge implied by others shows when you point at a box)used by tests alone

Crates

CrateWhat it isFilesLinesTest lines
kinogaki-svgKinogaki SVG reads SVG documents the way resvg (and so, closely, a browser) does, converts them into a render tree with every property resolved, and draws them with an exact-coverage anti-aliased rasterizer.4916,916597
kinogaki-svg-toolsShared pieces of the conformance and benchmark tools: rendering a document with kinogaki-svg and with resvg into premultiplied RGBA8, comparing bitmaps, and writing side-by-side sheets.36620

README

lib-svg

Kinogaki's SVG library. kinogaki-svg reads SVG documents the way resvg (and so, closely, a browser) does, converts them into a render tree with every property resolved, and draws them with an exact-coverage anti-aliased rasterizer. lib-ui draws its icons with it. It is a port of luce-svg's full renderer, redesigned in Rust and faster than both luce-svg and resvg on the icon set (see docs/BENCHMARKS.md).

CrateContents
crates/svg (kinogaki-svg)The library: XML, CSS, the SVG tree, conversion, the rasterizer and renderer, the icon cache. No dependencies; owned code forbids unsafe
crates/svg-tools (kinogaki-svg-tools)conformance (icons against resvg, resvg's test suite against its references) and bench. Uses resvg as the oracle

IconCache holds named icons parsed once and their rasterizations by size. Malformed documents answer an Error with its kind and source position; Limits bound size, nesting, node count, entity expansion, use copies, bitmap size and the steps reading takes (each selector test, each element converted, a referenced clip path's, mask's or marker's content again at each use), and a drawing cuts at most 16,777,216 dashes, so hostile input cannot exhaust memory or time.

What is supported

Everything luce-svg's full renderer draws: XML with DOCTYPE entities and namespaces; <style> sheets (type, class, id, attribute and :first-child selectors with combinators, specificity, !important) and style attributes; inherit; every length unit; transforms with transform-origin; use, symbol, nested svg, switch and conditional attributes; every basic shape and path command; fills and strokes (rules, dashes, caps, joins including miter-clip, paint-order, shape-rendering); every CSS color syntax including CSS Color 4 spaces; linear and two-point conical radial gradients with spreads; patterns; clip paths and masks (nested, both units); markers with context paints; group opacity, the sixteen blend modes and isolation; images (SVG images converted in place; PNG, JPEG and others through the caller's ImageDecoder); filters: the CSS filter functions and every primitive resvg renders (blend, color matrix, component transfer, composite with arithmetic, convolution, displacement, drop shadow, flood, Gaussian blur, image, merge, morphology, offset, tile, turbulence, diffuse and specular lighting with distant, point and spot lights), in linearRGB or sRGB. Compressed SVG (SVGZ) images are inflated by a built-in, bounded DEFLATE decoder. Text: <text>, <tspan>, <tref>, <a> and <textPath> laid out as usvg lays them out. That covers chunks, per-character x, y, dx, dy and rotate, text-anchor, letter and word spacing, kerning, textLength with lengthAdjust, baseline-shift, dominant and alignment baselines, decorations, text along paths, vertical writing modes, and bidi runs. Glyphs come from a GlyphSource. With the fonts feature, fonts::FontFiles provides them over font files: the system's by default, matched by CSS's rules, shaped by harfrust, with outlines from skrifa.

Conformance

The 449 luciaos icons match resvg within 0.2% of pixels at 24, 48 and 256 px; cargo test checks every icon at 24, 48 and 128 px. Against the 1719 integration tests resvg runs (300 px, a pixel differs when a premultiplied channel is off by more than 40, a test passes with at most 0.5% differing): with the suite's fonts, filters 396/397, masking 93/93, paint servers 151/153, painting 305/306, shapes 133/133, structure 254/258, text 354/379: 1686/1719 in all (luce-svg: 1671). Without fonts: 1308. What fails: color fonts and variable-font axes (font-variation-settings), GIF and WebP images (the tools decode PNG and JPEG), rgba() with a percentage alpha (read as CSS Color 4 reads it; resvg rejects it), and a few sub-pixel placements.

Checks

The Kinogaki Studio tree keeps this repository at libs/lib-svg, beside lib-ui. See docs/ARCHITECTURE.md and THIRD_PARTY_NOTICES.md.

Its documents

The repository's architecture document as written there (code blocks and links left out), and the opening of each other document.

lib-svg architecture (libs/lib-svg/docs/ARCHITECTURE.md)

lib-svg architecture

kinogaki-svg turns SVG text into pixels in five stages, each a module with one responsibility. The design follows luce-svg's full renderer (itself modelled on resvg's usvg/resvg split), so behavior can be compared stage by stage, but the data structures are Rust's: borrowed strings through parsing, an owned render tree with shared (Arc) paint servers, and a renderer that streams coverage rows.

ModuleResponsibility
xmlElements and text runs, borrowing from the source; DOCTYPE entities (which may hold markup) with a depth and byte budget; character references; namespace prefixes; errors with line and column
cssStyle sheets: selectors (type, *, class, id, attribute tests, :first-child, descendant/child/sibling combinators), specificity, declarations with !important
treeThe SVG tree: only SVG elements, attributes after the cascade (presentation attribute < sheet rules by specificity < style), inherit resolved, use targets copied in with cycles refused and a copy budget, reference cycles (clip/mask/filter/pattern) broken. names/ holds the element and attribute names, and names/tables.rs their generated tables with the inheritance flags
valuesAttribute microsyntaxes: numbers, lengths, angles, transforms, viewBox, preserveAspectRatio, CSS colors (hex, named, rgb/hsl/hwb, lab/lch, oklab/oklch, color()), path data, points, IRIs
convertTree to render tree, as usvg: units against the nearest viewport, visibility and conditions, groups only where needed; shapes, paint (fills, strokes, dashes, context paints), servers (gradients, patterns, href chains), structure (use, symbol, nested svg, switch), clip (clip paths, masks), markers (placement, orientation, paint order), filters (filter functions and elements into filter::Filter), image (data URLs, files, nested SVG, the caller's decoder)
sceneThe render tree: Group (transform, opacity, blend, isolation, clip, mask, filters, children), Shape (path, fill, stroke), Image; Gradient, Pattern, ClipPath, Mask
geomPoints, transforms, rectangles, paths (arcs and quadratics raised to cubics, tight bounds), flattening within 0.05 device pixels, the stroker, dashing
rasterCoverage: exact-area accumulation in bands, exact sub-scanline rows where the outline overlaps itself, pixel-centre coverage for shape-rendering crisp shapes
renderCanvases of premultiplied floats, shaders (solid, gradients, pattern and image tiles), the sixteen blend modes, layers, clip paths and masks
filterThe filter model; render runs a group's filters over layer-sized images (subregions, color-space conversion, named results), ops the combining and recoloring primitives, blur blurs, shadows and morphology, light turbulence and lighting
iconIcon and IconCache: parse once, rasterize per size

The rasterizer

Every fill and stroke becomes closed outlines in device pixels. Rasterizer::resolve sorts the lines by top and walks bands of 16 rows. Each line adds, to an accumulation buffer, the signed area it contributes to every cell it crosses (the technique of font rasterizers): a running sum along a row then gives each pixel's winding-weighted covered area, which the fill rule folds (nonzero: |a| clamped to 1; even-odd: folded at every two). This is exact wherever the outline does not overlap itself within a pixel. When a row has a pixel whose winding exceeds one and an edge crossing it (two coincident contours, say), that row is resolved again exactly: sixteen sub-scanlines, each sweeping its crossings with the winding counted and adding the exact horizontal extent of the spans inside. Coverage rows stream to the painter, so memory is bounded by the shape's width, not its area.

Strokes

A stroke is built in user space and mapped to the device piece by piece, so non-uniform transforms stretch it as SVG asks. An open polyline becomes one outline: the left offset forward, the end cap, the right offset backward, the start cap; a closed one two loops. The outer side of each vertex gets the join (miter, miter-clip, bevel, round arc); the inner side the point where the offset lines cross, or a pivot through the vertex when they do not cross within both segments. As chains this outline equals the sum of every segment's rectangle and every join's wedge, all wound alike, so under the nonzero rule it covers exactly their union. Dashes cut the flattened path first; curves are measured as tiny-skia measures them (a polyline within 0.5 / resolution scale, quadratics by their own rule), so dash ends land where resvg puts them.

Rendering

The renderer walks the tree with the transform so far. A group that needs isolation (opacity, clip, mask, filters, blend mode, isolation) draws into a layer sized to its content's conservative device bounds; clip and mask coverage multiply the layer, which composites onto its parent. A group isolated only by opacity whose sole child is a shape with one paint is drawn without a layer, the opacity folded into the paint (identical output). Compositing is in sRGB as browsers and resvg composite; RenderOptions { linear: true } composites in linear light.

Bounds on work

Limits caps source bytes, nesting depth, node count, entity expansion, use copies, bitmap sides and the steps reading a document takes (max_work: each test of a selector against an element, so a selector of many descendant combinators can't try its combinations without end, and each element converted, so a clip path's, mask's or marker's content, converted again at each use, can't multiply through nesting); past it the document is refused. href chains stop after 64 links, patterns nest at most 16 deep, SVG images 8 deep, a drawing cuts at most 16,777,216 dashes across its strokes and their runs, flattening stops after 1000 pieces a curve, turbulence keeps its lattice in floats so any base frequency is a number, and every geometric computation tolerates NaN and infinite input. tests/robustness.rs feeds mutated icons, random markup and numeric extremes through parse and render.

Dependencies

None at run time. The tools crate and the conformance tests use resvg 0.45.1 as the reference renderer.

Benchmarks (libs/lib-svg/docs/BENCHMARKS.md)

All numbers are parse plus render of every one of the 449 luciaos icons (crates/svg/tests/fixtures/icons), each drawn into a square bitmap of the stated size, per round. Each figure is the median of the rounds, after one warm-up round. Release builds throughout.