17 Commits

Author SHA1 Message Date
Chris Tsang 74c29b1a8c Release app 2026-08-29 22:27:11 +01:00
Chris Tsang 2500df76b5 Release 1.0.0-alpha.4
Rust / test (push) Has been cancelled
Rust / wasm-safety (core) (push) Has been cancelled
Rust / Node package (push) Has been cancelled
Bump every package version to 1.0.0-alpha.4 (PyPI 1.0.0a4) and promote the
CHANGELOG [Unreleased] section to 1.0.0-alpha.4. Refresh the tracked lockfile.
App download links are intentionally left at 1.0.0-alpha.3 (desktop app
release lands separately).
2026-08-29 01:02:58 +01:00
Chris Tsang 74da0efb84 Uncap watershed detail in the Node binding + sync docs
`6f163fc` widened watershed_detail from u8 to u32 in the core, CLI, and
Python binding but not the Node/wasm binding, which still assigned a u8 to
config.watershed_detail — a wasm build error (E0308). Widen the Node
Options field to u32 and refresh every "0..=255" watershed doc (CLI help,
Config field, .pyi, index.d.ts, and all three READMEs) to reflect that the
level is now uncapped (default 128; higher = more regions).
2026-08-08 21:50:55 +01:00
Chris Tsang 6f163fcf7d Uncap watershed details 2026-08-06 16:14:44 +01:00
Chris Tsang bf9858e359 Readme 2026-08-02 02:24:15 +01:00
Chris Tsang 58221025d5 Prepare 1.0.0-alpha.3 release
- version 1.0.0-alpha.3 across the workspace, Python, and Node packages
  (and the vtracer dep pins, install snippets, and docs.rs link)
- CHANGELOG: date the section; note the new vtracer-bench crate and the
  watershed blurred-crack filament fix

(Supersampling was explored after alpha.2 and reverted -- it did not
improve output enough to keep.)
2026-08-01 21:22:33 +01:00
Chris Tsang f9112c5ba9 Ship the Python README as the PyPI description
Rust / test (push) Has been cancelled
Rust / wasm-safety (core) (push) Has been cancelled
Rust / Node package (push) Has been cancelled
The `vtracer` package never declared a `readme`, so its PyPI page rendered
an empty description. Point `[project].readme` at the crate's README, and
mirror it in Cargo.toml alongside the core crate's convention.

Pin the documented install to 1.0.0a2 while the 1.0 line is pre-release:
`--pre` would resolve it, but a bare `pip install vtracer` still lands on
0.6.15, whose cp314 wheels segfault on any keyword argument (pyo3 0.19
predates CPython 3.14 moving `PyTupleObject::ob_item`, #124).
2026-07-31 16:35:01 +01:00
Chris Tsang 222cc6cbff Prepare 1.0.0-alpha.2 release
Rust / test (push) Has been cancelled
Rust / wasm-safety (core) (push) Has been cancelled
Rust / Node package (push) Has been cancelled
- version 1.0.0-alpha.2 across the workspace, Python, and Node packages
- CHANGELOG: date the unreleased section
- README: bump install snippets and docs.rs link
2026-07-27 23:20:26 +01:00
Chris Tsang f4fe428038 Expose simplify in the Python and Node bindings
Config(simplify=...) / property in Python; simplify option in Node,
with the tolerance documented in .pyi, index.d.ts, and both READMEs.
The Node README's option list also catches up with the clustering
rename and the binary/watershed options, and test.js drops the stale
colorMode key (silently ignored since the rename, so its bw assertion
was testing the default path) and asserts simplify shrinks output.
2026-07-27 23:19:42 +01:00
Chris Tsang ef9496f792 Add a curve-simplification stage (--simplify), paper.js style
A new pipeline slot between curve fitting and composition: CurvePasses
rewrite each fitted contour, so mosaic mode transforms every shared
boundary segment exactly once and the tessellation stays seam-free by
construction. SimplifyCurves re-fits each smooth run between corners
with the fewest cubics within the tolerance (Schneider's algorithm via
a current flo_curves — visioncortex's copy is pinned to an old one and
block-splits at 200 points), with tangents from the chain's own ends,
corners kept in place, junction endpoints pinned bit-for-bit, and rings
seamed at their sharpest junction. Off by default; polylines pass
through untouched. Cityscape at tolerance 1: 229 -> 138 KB stacked,
103 -> 36 KB watershed cutout, with render diffs under golden noise.

CurveFitter now returns Vec<FittedGeom> (promoted from mosaic::fit) so
stacked contours flow through the same pass machinery; the optimizer's
SimplifyPass is renamed CleanupPass to free the word.
2026-07-27 22:30:05 +01:00
Chris Tsang fb75738328 Rust edition 2024; drop mod.rs for module-named files
Workspace, vtracer-py, and the nodejs wasm crate move from edition 2021
to 2024 (no code changes needed — clean build and test run on 1.95).
Module directories switch from foo/mod.rs to the modern foo.rs layout;
single-file modules (svg, optimize, compose, fitter) collapse from
directories into plain files. Pure git renames, history preserved.
2026-07-27 15:41:30 +01:00
Chris Tsang 46a1b90ccd Add watershed clustering: hierarchical watershed frontend
An alternative region-forming frontend selected by --clustering watershed
(the color_mode field is replaced by clustering: color-cluster | bw |
watershed across CLI, Rust, Python, and Node — it selects the algorithm,
not a color space).

The algorithm is the watershed hierarchy by volume on the 4-adjacency
pixel graph, implemented from the papers:

  Cousty, Bertrand, Najman, Couprie, "Watershed Cuts: Minimum Spanning
  Forests and the Drop of Water Principle", IEEE TPAMI 31(8), 2009.
  Najman, Cousty, Perret, "Playing with Kruskal", ISMM 2013.

Edge weights are the max per-channel color difference between adjacent
pixels (no gradient image); a counting-sorted Kruskal pass builds the
binary partition tree as a flat parents array; a leaves-to-root pass
computes subtree area and volume; each merge's persistence (the volume of
the smaller side, plateau-corrected) becomes its MST edge's saliency; and
cutting the hierarchy is single-linkage over MST edges below the cut
level. Watershed cuts label every pixel — no watershed-line pixel class —
so the output is a strict, gapless partition that drops straight into
both stacked and cutout modes. Integer arithmetic and flat u32 arrays
throughout; deterministic across platforms; ~66 ms on a 1400x775 photo.

The one dial, --watershed-detail (0..=255), maps exponentially to a
target region count (each +25.5 doubles it) since the persistence
distribution is far too skewed for a linear threshold. filter_speckle
absorbs undersized basins into their most color-similar neighbour rather
than dropping them, preserving the partition. The largest region is
emitted first as a solid full-canvas background layer so stacked mode
keeps its seam-free overdraw; the mosaic flatten is unaffected.

Tests: partition invariant (disjoint masks tiling the canvas), detail
monotonicity, min-area absorption, determinism, watershed cases in the
pipeline/golden suites, stacked-vs-cutout interior equivalence, a
watershed seam test, and SegmentKey coverage for the new params.
2026-07-27 14:45:25 +01:00
Chris Tsang f4872fcfe3 Shield the hand-formatted core crate from stray cargo fmt runs
Rust / test (push) Has been cancelled
Rust / wasm-safety (core) (push) Has been cancelled
Rust / Node package (push) Has been cancelled
disable_all_formatting in crates/vtracer/rustfmt.toml makes rustfmt a no-op
for every file under the crate, so an accidental workspace fmt cannot
rewrite it.
2026-07-27 13:45:12 +01:00
Chris Tsang a350e2532a Enrich binary thresholding: tunable fixed + Bradley–Roth adaptive
BinaryFrontend gains a Threshold enum: Fixed(u8) (now tunable, was
hardcoded to 128) and Adaptive { window, t } — Bradley–Roth adaptive
thresholding computed via visioncortex's SummedAreaTable, O(pixels)
regardless of window size, for images with uneven lighting. Both use a
shared (r+g+b)/3 intensity so they agree on "dark"; the grayscale
checker_bw golden is unaffected.

Exposed through Config and all bindings: CLI (--threshold, --adaptive,
--adaptive-window, --adaptive-t), Python (constructor kwargs + getters/
setters), and the Node package (binaryThreshold, adaptive, adaptiveWindow,
adaptiveT). Adds tests covering fixed tunability and adaptive recovering
locally-dark marks under a brightness gradient that a global cutoff can't.
2026-07-25 00:26:05 +01:00
Chris Tsang 5743912da6 Trim image codec features in the CLI and Python crates
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).
2026-07-24 15:56:28 +01:00
Chris Tsang 170b0322a4 Bump pyo3 to 0.26 in vtracer-py 2026-07-24 13:37:25 +01:00
Chris Tsang f76aed78b2 Add vtracer-py: Python bindings with a rich API
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.
2026-07-24 11:53:47 +01:00