A Rust library and CLI for image downscaling with automatic post-resize sharpness adjustment.
The sharpening strength is selected automatically by fitting a cubic model of artifact ratios and solving for a target out-of-gamut threshold — not by a generic sharpness heuristic.
Live demo — runs entirely in the browser via WebAssembly.
cargo build --release -p r3sizer-cli
./target/release/r3sizer \
--input photo.jpg \
--output out.png \
--width 800 \
--height 600 \
--diagnostics diag.jsonOutput:
Output size : 800x600
Sharpen mode : lightness (CIE Y)
Sharpen model : practical USM
Metric mode : relative (sharpening-added artifacts)
Artifact metric : channel clipping ratio
Baseline artifact ratio : 0.000000
Selected strength : 1.8472
Target metric value : 0.003000
Measured metric value : 0.002987
Measured artifact ratio : 0.002987
Budget reachable : yes
Fit status : success
Crossing status : found
Selection mode : polynomial root
Fit quality:
R² : 0.999834
Residual sum of squares : 1.23e-09
Max residual : 2.45e-05
Min pivot : 3.67e+01
Robustness:
Monotonic : yes
Quasi-monotonic : yes
R² ok : yes
Well conditioned : yes
LOO stable : yes
Max LOO root change : 0.0312
Timing (us):
Resize : 12450
Contrast : 0
Baseline : 890
Probing : 45230
Fit : 15
Robustness : 98
Final sharpen : 6120
Clamp : 340
Total : 65143
Use --preserve-aspect-ratio (-p) when only one dimension is known:
r3sizer -i photo.jpg -o out.png --width 800 -pProcess a directory of images and produce an aggregate summary:
r3sizer \
--sweep-dir ./photos \
--sweep-output-dir ./out \
--sweep-summary summary.json \
--width 800 --height 600The summary JSON includes per-file results (selected strength, selection mode, timing) and aggregate statistics (mean/median strength, fit success rate, selection mode histogram).
crates/
r3sizer-core/ pure processing (color, resize, sharpen, metrics, fit, solve, pipeline)
r3sizer-io/ image I/O (PNG/JPEG load/save via the `image` crate)
r3sizer-cli/ command-line interface
r3sizer-wasm/ WebAssembly bindings for browser use
web/ React/Vite diagnostic UI (talks to r3sizer-wasm via Web Worker)
r3sizer-core has no I/O or CLI dependencies and can be embedded in a Tauri GUI or
compiled to WASM without modification.
input (sRGB file)
↓ load + normalize
↓ sRGB → linear RGB (IEC 61966-2-1)
↓ downscale (Lanczos3 or content-adaptive kernel)
↓ classify regions (Flat, Textured, StrongEdge, Microtexture, RiskyHaloZone)
↓ optional contrast leveling
↓ measure baseline artifact ratio P(base)
↓ extract CIE Y luminance, build per-pixel gain map
↓ two-pass adaptive probing:
coarse scan → find P0 crossing → dense refinement
for each s_i:
sharpen luminance(s_i) × gain → reconstruct RGB → chroma guard → measure P(s_i)
↓ fit cubic P_hat(s) = a·s³ + b·s² + c·s + d (with fit quality: R², residuals)
↓ robustness checks (monotonicity, LOO stability, R², condition)
↓ solve P_hat(s*) = P0 (Photo: P0=0.003, Precision: P0=0.001)
↓ adaptive backoff if budget exceeded
↓ apply final sharpening(s*) → chroma guard
↓ clamp to [0,1]
↓ linear RGB → sRGB → save + recommendations
See docs/algorithm.md for a full pipeline description.
| Flag | Short | Default | Description |
|---|---|---|---|
--input |
-i |
required | Input image path |
--output |
-o |
required | Output image path |
--width |
-W |
— | Target width (px) |
--height |
-H |
— | Target height (px) |
--preserve-aspect-ratio |
-p |
off | Compute the missing dimension from the input aspect ratio |
--target-artifact-ratio |
0.003 |
P0 threshold (fraction, not percent) | |
--preset |
— | Named preset: photo (default), precision |
|
--diagnostics |
— | Path to write a JSON diagnostics file | |
--diagnostics-level |
summary |
summary or full (per-probe breakdowns) |
|
--probe-strengths |
two-pass | Comma-separated explicit probe list | |
--sharpen-sigma |
1.0 |
Gaussian sigma for unsharp mask | |
--sharpen-mode |
lightness |
lightness (CIE Y) or rgb |
|
--metric-mode |
relative |
relative (sharpening-added) or absolute (total) |
|
--artifact-metric |
channel-clipping |
channel-clipping or pixel-out-of-gamut |
|
--metric-weights |
1.0,0.3,0.3,0.1 |
Composite weights: gamut, halo, overshoot, texture | |
--selection-policy |
gamut-only |
gamut-only, hybrid, or composite-only |
|
--enable-contrast-leveling |
off | Enable contrast leveling stage (placeholder) | |
--sweep-dir |
— | Directory of images to process in batch mode | |
--sweep-output-dir |
— | Output directory for processed images (sweep mode) | |
--sweep-summary |
— | Path to write sweep summary JSON | |
--sweep-diff |
— | Compare two sweep summaries: BASE,CANDIDATE |
|
--generate-corpus |
— | Generate synthetic benchmark corpus in directory |
In single-file mode, --input and --output are required. Both --width and --height
are required unless --preserve-aspect-ratio is set, in which case only one is needed.
In sweep mode, --sweep-dir replaces --input/--output. The sweep flags
(--sweep-output-dir, --sweep-summary) require --sweep-dir.
# Build all crates
cargo build --workspace
# Run all tests
cargo test --workspace
# Lint (warnings are errors)
cargo clippy --workspace -- -D warnings
# Benchmarks
cargo bench -p r3sizer-coreTypeScript types for the web UI are auto-generated from the Rust types in r3sizer-core
using ts-rs. The generated file lives at
web/src/types/generated.ts and should be regenerated whenever types in types.rs change:
cargo test -p r3sizer-core --features typegen export_typescript_bindings -- --nocaptureThis also serializes Rust Default impls as TypeScript constants (DEFAULT_PARAMS, etc.)
so the two sides never drift. The web app re-exports everything through
web/src/types/wasm-types.ts, which adds WASM-specific types (ProcessResult) and
any web-only overrides.
The web UI is deployed to GitHub Pages at alvytsk.github.io/r3sizer via a GitHub Actions workflow (.github/workflows/deploy.yml). Every push to main triggers a build (Rust → WASM → Vite bundle) and deploys automatically.
For local development:
cd web
# Local development (requires WASM package to be built first)
npm run build:wasm
npm run dev
# Production build (builds WASM + TS check + Vite bundle)
npm run build
# Docker (from repo root)
docker build -f web/Dockerfile -t r3sizer-web .
docker run -p 8080:80 r3sizer-webr3sizer-core exposes the pipeline as a single function (one-shot) or as a two-phase
API for interactive use:
use r3sizer_core::{AutoSharpParams, ProcessOutput, process_auto_sharp_downscale};
// One-shot (CLI / batch)
let params = AutoSharpParams::photo(800, 600);
let ProcessOutput { image, diagnostics } =
process_auto_sharp_downscale(&input_linear_rgb, ¶ms)?;
// Two-phase (interactive / WASM)
use r3sizer_core::{prepare_base, process_from_prepared};
let base = prepare_base(&input_linear_rgb, ¶ms, &|_| {})?;
// ... user adjusts params (non-base-affecting) ...
let output = process_from_prepared(&base, ¶ms, &|_| {})?;AutoSharpDiagnostics is Serialize-able for JSON export and contains the full probe
data, fit coefficients, selection mode, fit quality metrics (R², residuals), solver
robustness flags (monotonicity, LOO stability), typed fallback reasons, per-stage timing,
composite metric breakdowns, region coverage, chroma guard statistics, evaluator results,
and parameter recommendations.
docs/algorithm.md— implemented pipelinedocs/pipeline_implementation.md— detailed walkthrough with data flow and allocationsdocs/assumptions.md— confirmed vs engineering approximationsdocs/future_work.md— next steps and Tauri integration
MIT — see LICENSE.