The spline fitter's least-squares cubic fit (flo_curves, f64) diverges by ULPs
across architectures: on x86_64 vs arm64 an arc decomposes into slightly
different control points, changing the SVG bytes with no real geometry change
(verified with an x86_64 emulation: 0 pixels differ by >40, worst channel
delta 20 — visually identical). Byte-exact golden comparison is therefore
inappropriate for the spline output.
Render both the stored golden and the produced SVG (resvg) and diff pixels,
tolerating a tiny fraction for sub-pixel boundary flips. Encoding-agnostic and
architecture-robust, while still catching genuine regressions (which move
boundaries by whole pixels).
vtracer only decodes input, but image's default features pulled a full AV1
encoder (ravif/rav1e) and OpenEXR into the CLI binary and the Python wheel.
Restrict to decode-only input formats (png, jpeg, gif, bmp, webp, tiff, ico,
pnm, tga, qoi). The release binary drops from ~3.23 MB to ~2.46 MB and builds
faster; supported inputs are unchanged in practice (avif decode was never in
image's defaults anyway).
New crates/vtracer-py (pyo3 + maturin, abi3) wrapping the vtracer framework.
Rather than a thin CLI-style wrapper, it exposes a mutable `Config` class with
named properties and `bw`/`poster`/`photo` preset constructors, plus three
input paths — `convert_file`, `convert_bytes` (encoded image, optional format),
and `convert_pixels` (raw RGBA8) — available as `Config` methods and
module-level functions. Palette is a list of `#rrggbb` strings; bad inputs
raise ValueError.
The core crate stays pure: image decoding lives here. The crate is excluded
from the cargo workspace (pyo3 extension-module cdylibs don't link libpython,
which breaks `cargo test` at the root) and is built with maturin. Ships a
vtracer.pyi type stub. README updated.
`vtracer in.png out.svg` now works alongside the `-i/--input` and
`-o/--output` flags. Input/output become optional positionals plus the
existing flags; an explicit flag wins over the positional, and a clear error
is shown if neither is given.
Feature — mosaic spline segment fitter (crates/vtracer/src/mosaic/fit.rs):
open-path cubic fitting for boundary segments, reusing the now-public
visioncortex primitives (PathSimplify::limit_penalties for symmetric,
gap-free staircase removal; open-path SubdivideSmooth::{find_corners,
subdivide_keep_corners,find_splice_points}; fit_points_with_bezier per splice
slice). Matches stacked spline curve quality; endpoints pinned to lattice
nodes so shared boundaries stay seam-free.
Fix — stacked mode punched holes in cluster masks (to_image_with_hole .. true);
stacked must trace solid layers and occlude by paint-order overdraw (false).
Holes left the layer below exposed as hairline seams.
Fix — the relative SVG writer measured a subpath's opening `m` from the last
vertex instead of the subpath start (SVG resets the current point to the start
after Z), misplacing holes / extra subpaths at optimize=1/2.
Tests — new tests/equivalence.rs: stacked-vs-mosaic interior agreement (all
fitters) and a seam guard (a full-coverage image must render fully opaque).
svg round-trip test (absolute vs relative encode identical geometry). mosaic
spline endpoint-pinning test. Regenerated goldens; added disc_mosaic_spline.
resvg added as a dev-dependency (test-only; not compiled for wasm).
Drop unused MosaicOptions placeholder
The strict/seam-stroke mitigations aren't needed — the mosaic geometry is
already gapless and seam-free. Remove the no-op MosaicOptions struct and thread
it out of Compositing::Mosaic and compose_mosaic.
Implements the topological mosaic pipeline from docs/design/mosaic.md, turning
`--hierarchical cutout` into a true gapless tessellation instead of the old
re-cluster-and-retrace fake.
LabelMap (flatten Segmentation top-down)
→ boundary-graph extraction (integer-exact: corners, node rule, segment
and ring tracing on the pixel-corner lattice)
→ face assembly (left-region successor rule; winding falls out,
so each region is one nonzero-fill path)
→ fit each segment ONCE (shared by both adjacent faces, reversed
exactly → byte-identical shared boundaries)
→ compose per-region paths
Backends: PixelSegmentFitter (exact reference) and PolygonSegmentFitter
(symmetric open Douglas-Peucker collapsing staircases to the crack midline).
The spline segment fitter is still pending; mosaic + spline currently falls
back to polygon.
Compositing now owns its fitter (Stacked(CurveFitter) / Mosaic(SegmentFitter)).
Tests: single region, vertical split, T-junction, checkerboard pinch, nested
rings, border-touching, and a pixel round-trip property test over 40 random
maps (rasterize composed faces == input label map). Plus two mosaic goldens.
12 synthetic-image cases covering every stage: all three fitters, holes,
region adjacency, hierarchical layering, binary mode, fixed-palette and
auto-quantize color fitting, and the three optimizer/writer levels.
Fixtures are built from in-code images, not the JPEG samples, because JPEG
decoding is image-crate-version dependent and would make goldens fragile.
Regenerate after an intentional change with VTRACER_BLESS=1.