resvg regression suite, resvg-test-suite reference corpus, usvg lowering and the SVG support table
Document status:
reviewed. Canonical source.
Summary
resvg is a Rust static-SVG renderer built on tiny-skia. Parsing and rendering are split: usvg lowers SVG into a resolved tree (only absolute path segments, resolved use, CSS, text and markers, objectBoundingBox converted to userSpaceOnUse), and resvg rasterises that tree. The regression suite consists of roughly 1,700 single-issue SVG files with a fixed 200 × 200 viewBox, rendered at 300 px width with pinned fonts and compared against PNGs with a per-channel threshold of 1 and zero tolerated differing pixels. A generator script emits one #[test] per SVG. The separately maintained resvg-test-suite holds the same SVGs together with manually verified reference PNGs and publishes a per-feature support table for resvg, browsers and other libraries. The README claims bit-identical output across platforms because no system libraries are used.
Evidence
- Scope: resvg “aims to only support the static SVG subset; i.e. no
a,script,vieworcursorelements, no events and no animations”; SVG Tiny 1.2 “is not supported and support is also not planned”; SVG 2 support is in progress (README, “SVG support”). - Suite size: “a vast test suite that includes around 1600 tests”, described as SVG-to-PNG regression tests that exclude dependency tests (README, lines 23–25). The current tree holds 1,722 SVG files under
crates/resvg/tests/tests/: filters 398, masking 93, paint-servers 151, painting 306, shapes 133, structure 262, text 379 (GitHub tree listing, 2026-08-29);render.rscontains 1,716 generated tests. - Reproducibility claim: “if you render an SVG file on x86 Windows and then render it on ARM macOS - the produced image will be identical. Each pixel would have the same value.” (README, “Reproducibility”).
- Naming: tests are organised as
tests/<category>/<element-or-attribute>/<case>.svgwith a sibling.png, for examplepainting/stroke-linejoin/{arcs,bevel,miter,miter-clip,round}.svg(tree listing). - Authoring rules: fixed 200 × 200 viewBox template with a frame
rect; “Each test must test only a single issue”; every element needs anid; uniquetitleunder 60 characters; line length under 100; UTF-8;check.pyenforces these (crates/resvg/tests/README.md). - Reference generation: render with
--width 300 --skip-system-fonts --use-fonts-dir 'tests/fonts' --font-family 'Noto Sans' --serif-family 'Noto Serif' --sans-serif-family 'Noto Sans' --cursive-family 'Yellowtail' --fantasy-family 'Sedgwick Ave Display' --monospace-family 'Noto Mono', thenoxipng -o 6 -Z; 300 px “to test scaling” (crates/resvg/tests/README.md, “Render PNG”). - Two PNG sets:
resvg-test-suite/pngcontains reference images (“how the SVG files should be rendered”);resvg/tests/pngcontains images rendered by resvg itself “used only for regression testing” (crates/resvg/tests/README.md, “resvg tests vs resvg-test-suite tests”). - Harness:
IMAGE_SIZE: u32 = 300; a globalfontdbloadstests/fontsand sets the five generic families;MAKE_REFregenerates references; the actual image is alpha-demultiplied before comparison;get_diffmarks a pixel different if any of R, G, B, A differs by more thanDIFF_THRESHOLD = 1, treats two fully transparent pixels as equal, counts size mismatches as differences, and writes a three-panel diff PNG totests/diffs/(crates/resvg/tests/integration/main.rs). - Generated tests:
gen-tests.pywalkstests/**/*.svg, derives a function name from the path and emits#[test] fn ... { assert_eq!(render("..."), 0); }; theIGNORElist excludesfilters/feMorphology/huge-radius(CI timeout), invalid-size and non-UTF-8 structure cases, andpaint-servers/radialGradient/focal-point-correctionwith the comment “Produces slightly different output on some hardware. Not a bug, just a SIMD rounding difference.” (crates/resvg/tests/gen-tests.py). - usvg lowering: attributes resolved (inheritance, defaults), CSS applied, basic shapes converted to paths, only absolute MoveTo/LineTo/QuadTo/CurveTo/ClosePath segments,
useand nestedsvgresolved, invalid elements removed, relative units converted, images loaded or decoded, references resolved,switchresolved, text “completely resolved”, markers converted into regular elements, all filters supported, recursive elements removed,objectBoundingBoxreplaced withuserSpaceOnUse(crates/usvg/README.md, “Features”). - Unsupported features are enumerated (font-based SVG elements,
color-profile, externaluse,clip,color-interpolation,direction,unicode-bidi, and others) (docs/unsupported.md). - Support table: rows are SVG elements and attributes grouped by category; columns are resvg, Chrome, Firefox, Safari, Batik, Inkscape, librsvg, SVG.NET, QtSvg; legend “Passed | Failed | Crashed | ? | Undefined behavior” (linebender.org/resvg-test-suite/svg-support-table.html). Results are produced by manual comparison recorded in
results.csv“viatools/vdiff” and charted bystats.py(resvg-test-suite/README.md). - Versions: resvg and usvg 0.48.1 (crates.io, 2026-08-02).
Mechanism
Fixture: tests/<category>/<feature>/<case>.svg (viewBox 0 0 200 200, one issue per file)
Reference: same path with .png, rendered by resvg at width 300 with pinned fonts, oxipng-optimised
Harness (integration/main.rs):
tree = usvg::Tree::from_data(svg, Options { fontdb: pinned, resources_dir: fixture dir })
size = tree.size().scale_to_width(300); pixmap = render(tree, scale transform)
actual = demultiply_alpha(pixmap)
diff = count of pixels where (not both alpha == 0) and any channel |a - b| > 1
assert diff == 0 # generated by gen-tests.py
MAKE_REF=1 -> write/overwrite reference; failures write tests/diffs/<name>.png (expected | mask | actual)
Conformance matrix:
for each implementation impl and fixture f: result[impl][f] in {passed, failed, crashed, unknown, undefined}
aggregate per feature row and per implementation column
NUIF relevance
Borrow
- Adopt the single-issue fixture discipline (fixed canvas, unique title, one feature per file, category/feature/case paths) for the NUIF
rendersuite, because it makes failures attributable and the corpus enumerable as a feature matrix. - Adopt the exact-comparison harness pattern (threshold 1, zero differing pixels, three-panel diff artefact, explicit regenerate flag) for the CPU reference path, because resvg demonstrates cross-platform pixel identity with this policy.
- Adopt the support-table pattern (implementations × features with a five-state legend) as the public conformance matrix for NUIF profiles, because it communicates partial support without collapsing to a single score.
Adapt
- Apply the usvg idea of a lowered, fully resolved tree to NUIF’s resolved-snapshot layer, because conformance fixtures should compare the resolved scene as well as the raster; NUIF must keep authored intent alongside, which usvg discards.
- Keep separate “reference” and “regression” raster sets, because NUIF needs hand-verified references for the draft specification and implementation-rendered snapshots for regression detection.
Reject
- Do not exclude fixtures silently for SIMD rounding as
gen-tests.pydoes; NUIF should instead pin the reference path’s arithmetic (scalar or SIMD with identical rounding) or move the fixture to the tolerance tier with a recorded reason. - Do not adopt manual
vdifftriage as the source of truth for the matrix, because NUIF’s matrix must be produced by the automated suite.
Open questions
- Whether resvg-test-suite’s MIT-licensed SVG corpus can be reused directly as NUIF import fixtures for the SVG adapter, and how to attribute it.
- How NUIF should treat “undefined behavior” cells; resvg marks them but the NUIF spec would need an explicit undefined-behaviour category.