zenresize

zenresize is a SIMD-accelerated image resampling library with crop, resize, and canvas padding in streaming or fullframe modes.
[]
= "0.1"
Quick Start
use ;
let input = vec!; // RGBA pixels
let config = builder
.filter
.format
.build;
let output = new.resize;
assert_eq!;
Operations
All operations work in the streaming API. Crop and padding also work independently (without resize) by setting output dimensions equal to crop/content dimensions.
| Operation | What it does | Builder method |
|---|---|---|
| Resize | Resample to new dimensions with a choice of 31 filters | .filter(Filter::Lanczos) |
| Fit | Aspect-preserving resize to a target box | .fit(FitMode::Fit, max_w, max_h) |
| Crop | Extract a rectangular region from the input | .crop(x, y, w, h) |
| Pad | Add solid-color border around the output | .padding(top, right, bottom, left) |
| Orient | Apply EXIF orientation (rotate/flip) post-resize | stream.with_orientation(OrientOutput::Rotate90) |
| Crop + Resize | Extract region, then resize it | .crop(...) on a config with different output dims |
| Resize + Pad | Resize, then add padding | .padding(...) on a config with different input/output dims |
| Crop + Resize + Pad | All three in sequence | .crop(...) + .padding(...) |
The pipeline order is always: crop (input side) -> resize -> pad (output side).
Features
- Crop, resize, and pad -- independently or combined, streaming or fullframe
- 31 resampling filters (Lanczos, Mitchell, Robidoux, Ginseng, etc.)
- sRGB-aware linear-light processing for correct gamma handling
- Row-at-a-time streaming API for pipeline integration
Resizerstruct for amortizing weight computation across repeated resizes- Alpha premultiply/unpremultiply built into the pipeline
- Channel-order-agnostic: RGBA, BGRA, ARGB, BGRX all work without swizzling
- u8, u16, and f32 pixel I/O; cross-format resize (e.g., u8 in, f32 out)
no_std+alloccompatible (std optional)- SIMD-accelerated via archmage: AVX2+FMA on x86-64, NEON on ARM, WASM SIMD, scalar fallback
- Optional AVX-512 V-filter kernel (
avx512feature)
Resizer
Resizer pre-computes weight tables from the config. Reusing one across images with the same dimensions and filter saves the weight computation cost.
use ;
let config = builder
.filter
.format
.build;
let mut resizer = new;
// Allocating -- returns a new Vec<u8>
let output: = resizer.resize;
// Non-allocating -- writes into your buffer
let mut buf = vec!;
resizer.resize_into;
For pipelines that already work in linear f32:
let config = builder
.filter
.format
.build;
let mut resizer = new;
let output_f32: = resizer.resize_f32;
Cross-format resizing (u8 sRGB input, f32 linear output, or any combination):
let mut resizer = new;
let output_f32: = resizer.resize_u8_to_f32;
StreamingResize
Push input rows one at a time, pull output rows as they become available. Uses a V-first pipeline internally: the H-filter runs only out_height times (once per output row) instead of in_height times.
use ;
let config = builder
.filter
.format
.build;
let mut stream = new;
for y in 0..800
stream.finish;
// Drain remaining output rows
while let Some = stream.next_output_row
assert!;
assert_eq!;
Zero-copy output
Write output directly into an encoder's buffer:
let row_len = stream.output_row_len;
let mut enc_buf = vec!;
while stream.next_output_row_into
f32 streaming
stream.push_row_f32.unwrap;
// Or write directly into the resizer's internal buffer (saves a memcpy):
stream.push_row_f32_with.unwrap;
while let Some = stream.next_output_row_f32
Compositing
Resize foreground images onto a background in a single pass. Compositing happens in premultiplied linear f32 space between the vertical filter and unpremultiply -- no extra buffer copy.
use ;
let config = builder
.filter
.format
.build;
let bg = white;
let mut stream = with_background
.expect
.with_blend_mode; // default; 31 modes available
for y in 0..600
Background types: SolidBackground (constant color), SliceBackground (borrow a buffer), StreamedBackground (push rows), or implement the Background trait yourself. NoBackground (the default) eliminates all composite code at compile time.
Masking
Apply per-pixel masks to control where the foreground is visible. Masks are applied between resize and compositing, so rounded corners over a white background produce white corners (not transparent-over-black).
use ;
let config = builder
.format
.build;
let bg = white;
let mask = new;
let stream = with_background
.expect
.with_mask;
Mask types re-exported from zenblend: RoundedRectMask, LinearGradientMask, RadialGradientMask, or implement MaskSource.
Source Region (Crop)
Extract a rectangular region from the input before resizing. The streaming API accepts full-width input rows; the resizer skips rows outside the vertical range and extracts the horizontal region internally.
use ;
// Crop a 400x300 region starting at (100, 50), resize to 200x150
let config = builder
.filter
.format
.crop
.build;
let mut stream = new;
// Push full-width rows -- rows outside [50..350) are skipped automatically
for y in 0..800
Crop without resize (extract only):
// Extract 400x300 at (100, 50), no resize
let config = builder
.format
.crop
.build;
Fit Modes (Aspect-Ratio Constraints)
Four common ways to fit an input into a target box, preserving aspect ratio
where appropriate. One call sets out_width/out_height (and, for Cover,
a center-anchored source crop) without reaching for a separate layout crate.
| Mode | Behavior | Typical use |
|---|---|---|
FitMode::Fit |
Aspect-preserving, fit entirely inside bounds. Output ≤ bounds on both axes, == on one. May up- or down-scale. |
Thumbnail letterbox |
FitMode::Within |
Like Fit, but never upscales past input size. |
Thumbnails that stay sharp when source is small |
FitMode::Cover |
Aspect-preserving, fills the bounds exactly. Source is center-cropped to target aspect, then resized. Output is exactly max_w × max_h. |
Hero images, cover art, imageflow fit=crop |
FitMode::Stretch |
Ignores aspect, stretches to exact bounds. | Non-photo UI assets |
use ;
// 1600×900 source, fit into 800×600 letterbox → 800×450, no crop.
let config = builder
.filter
.format
.fit
.build;
assert_eq!;
// Same source, Cover: center-cropped to 4:3, output exactly 800×600.
let config = builder
.format
.fit
.build;
assert_eq!;
// `.fit(Cover, ...)` also sets `source_region` for the crop — no extra call.
For raw dimension math without the builder:
use ;
// What output dims would FitMode produce?
assert_eq!;
assert_eq!;
assert_eq!;
// What source crop does Cover apply?
// Target 4:3 from 16:9 source → crop to 1200×900 centered.
assert_eq!;
The math is a port of zenlayout's
fit_inside / crop_to_aspect including snap-to-target rounding — verified
byte-identical across a ~6M-case brute-force sweep
(tests/vs_zenlayout.rs). Callers migrating from zenlayout for simple
fit/within/cover cases see no pixel-level drift.
EXIF Orientation
OrientOutput is the 8-element D4 dihedral group (EXIF orientations 1–8),
applied post-resize by the streaming pipeline. If you already hold a
zenpixels::Orientation from metadata
parsing, it converts directly:
use ;
let exif_tag: u8 = 6; // Rotate 90° CW
let orient = from_exif.unwrap_or_default;
let mut resizer = new.with_orientation;
Orientation (re-exported from zenpixels) has the full group algebra —
compose, inverse, from_exif, to_exif, swaps_axes — so you can
build up composed transforms (e.g. EXIF orient + explicit 180°) and hand
the result to zenresize with one .into().
Output Padding
Add a solid-color border around the resized output. The total output becomes (left + width + right) by (top + height + bottom).
use ;
// Resize 1000x800 -> 500x400, then add 20px black border
let config = builder
.filter
.format
.padding_uniform
.padding_color
.build;
let mut stream = new;
// output_row_len() is (20 + 500 + 20) * 4 = 2160
// total_output_height() is 20 + 400 + 20 = 440
// Top padding rows are available before any input is pushed
for y in 0..800
Asymmetric letterboxing:
let config = builder
.format
.padding // 40px top/bottom only
.padding_color
.build;
// Total output: 500 x 480
Padding without resize:
let config = builder
.format
.padding_uniform
.padding_color // white border
.build;
// Total output: 520 x 420
Padding color
The padding_color values are 0.0-1.0 in the output's color space. For sRGB u8 output, 0.5 maps to value 128. For linear f32, 0.5 maps to 0.5. Only the first N channels are used (N = channel count of the output format).
Works with all output types: u8 (next_output_row), f32 (next_output_row_f32), u16 (next_output_row_u16).
Crop + Resize + Pad
All three operations compose naturally:
// Extract 800x600 region, resize to 400x300, add 10px white border
let config = builder
.filter
.format
.crop
.padding_uniform
.padding_color
.build;
// Pipeline: crop 800x600 -> resize to 400x300 -> pad to 420x320
ResizeConfig
All resize operations take a ResizeConfig built with the builder pattern.
use ;
let config = builder
.filter // resampling filter (default: Robidoux)
.format // sets both input and output format
.input // or set them separately
.output
.linear // resize in linear light (default)
.srgb // resize in sRGB space (faster, slight quality loss)
.resize_sharpen // sharpen during resampling (% negative lobe, default: 0)
.post_sharpen // post-resize unsharp mask (default: 0.0)
.crop // source region (default: full input)
.padding // output padding (default: none)
.padding_color // padding fill color
.in_stride // input row stride in elements (default: tightly packed)
.out_stride // output row stride in elements (default: tightly packed)
.build;
Defaults
If you call .build() with no other methods:
- Filter:
Robidoux - Format:
RGBA8_SRGBfor both input and output - Linear:
true(sRGB u8 -> linear f32 -> resize -> sRGB u8) - Resize sharpen:
0.0(natural filter ratio) - Post sharpen:
0.0 - Stride: tightly packed (width * channels)
Config fields
ResizeConfig fields are public (#[non_exhaustive]):
config.filter // Filter
config.in_width // u32 (full source width)
config.in_height // u32 (full source height)
config.out_width // u32 (content output width, before padding)
config.out_height // u32 (content output height, before padding)
config.input // PixelDescriptor
config.output // PixelDescriptor
config.linear // bool
config.post_sharpen // f32
config.post_blur_sigma // f32
config.kernel_width_scale // Option<f64>
config.lobe_ratio // LobeRatio
config.in_stride // usize (0 = tightly packed)
config.out_stride // usize (0 = tightly packed)
config.source_region // Option<SourceRegion> (crop rectangle)
config.padding // Option<Padding> (output padding)
Helper methods:
config.resize_in_width // crop width if set, else in_width
config.resize_in_height // crop height if set, else in_height
config.total_output_width // out_width + left + right padding
config.total_output_height // out_height + top + bottom padding
config.total_output_row_len // total_output_width * channels
Pixel Formats
PixelDescriptor (from zenpixels) describes pixel format, channel layout, alpha mode, and transfer function in one value.
Supported formats
| Format | Channels | Type | Transfer | Constant |
|---|---|---|---|---|
| RGBA sRGB | 4 (straight alpha) | u8 | sRGB | RGBA8_SRGB |
| RGBX sRGB | 4 (no alpha) | u8 | sRGB | RGBX8_SRGB |
| RGB sRGB | 3 | u8 | sRGB | RGB8_SRGB |
| Gray sRGB | 1 | u8 | sRGB | GRAY8_SRGB |
| BGRA sRGB | 4 (straight alpha) | u8 | sRGB | BGRA8_SRGB |
| RGBA linear | 4 (straight alpha) | f32 | Linear | RGBAF32_LINEAR |
| RGB linear | 3 | f32 | Linear | RGBF32_LINEAR |
| RGBA sRGB | 4 (straight alpha) | u16 | sRGB | RGBA16_SRGB |
| RGB sRGB | 3 | u16 | sRGB | RGB16_SRGB |
Cross-format resize is supported: any input type to any output type (u8 <-> u16 <-> f32).
Transfer functions
All five transfer functions work with all channel types and layouts:
| Transfer | Description |
|---|---|
Srgb |
Standard sRGB gamma (default) |
Linear |
Linear light (identity) |
Bt709 |
BT.709 broadcast gamma |
Pq |
HDR10 Perceptual Quantizer |
Hlg |
Hybrid Log-Gamma (HDR) |
Channel order
Channel order doesn't matter. The sRGB transfer function is the same for R, G, and B, and the convolution kernels operate on N floats per pixel. Pass BGRA data as RGBA8_SRGB -- no swizzling needed. (Use BGRA8_SRGB if you want the descriptor to be semantically accurate, but the resize output is identical either way.)
Color space (.linear() / .srgb())
- Linear (default): sRGB u8 -> linear f32 -> resize -> sRGB u8. Correct on gradients, avoids darkening halos. Uses f32 intermediate buffers.
- sRGB: Resize directly in gamma space. Uses an i16 integer pipeline with 14-bit fixed-point weights for 4-channel formats. Faster; slightly incorrect on gradients; good enough for thumbnails.
Filters
31 filters covering a range of sharpness/smoothness tradeoffs:
| Filter | Category | Window | Notes |
|---|---|---|---|
Lanczos |
Sinc | 3.0 | Sharp, some ringing. Good for photos. |
Lanczos2 |
Sinc | 2.0 | Less ringing than Lanczos-3. |
Robidoux |
Cubic | 2.0 | Default. Balanced sharpness/smoothness. |
RobidouxSharp |
Cubic | 2.0 | More detail, slight ringing. |
Mitchell |
Cubic | 2.0 | Mitchell-Netravali (B=1/3, C=1/3). Balanced blur/ringing. |
CatmullRom |
Cubic | 2.0 | Catmull-Rom spline (B=0, C=0.5). |
Ginseng |
Jinc-sinc | 3.0 | Jinc-windowed sinc. Excellent for upscaling. |
Hermite |
Cubic | 1.0 | Smooth interpolation. |
CubicBSpline |
Cubic | 2.0 | Very smooth, blurs. B-spline (B=1, C=0). |
Triangle |
Linear | 1.0 | Bilinear interpolation. |
Box |
Nearest | 0.5 | Nearest neighbor. Fastest, blocky. |
Fastest |
Cubic | 0.74 | Minimal quality, maximum speed. |
Plus LanczosSharp, Lanczos2Sharp, RobidouxFast, GinsengSharp, CubicFast, Cubic, CubicSharp, CatmullRomFast, CatmullRomFastSharp, MitchellFast, NCubic, NCubicSharp, RawLanczos2, RawLanczos2Sharp, RawLanczos3, RawLanczos3Sharp, Jinc, Linear, LegacyIDCTFilter.
Sharp variants use a slightly reduced blur factor for tighter kernels. Fast variants use smaller windows.
use Filter;
let f = default; // Robidoux
let all = all; // &[Filter] -- all 31 variants
imgref Integration
Typed wrappers for the imgref + rgb crates. These accept any pixel type implementing ComponentSlice (RGBA, BGRA, etc. from the rgb crate).
use ;
use ;
use ImgVec;
use RGBA8;
let config = builder // dimensions overridden by imgref
.filter
.build;
// 4-channel: pass a PixelDescriptor to control alpha handling
let output: = resize_4ch;
// 3-channel
let output_rgb: = resize_3ch;
// Grayscale
let output_gray: = resize_gray8;
The imgref functions override the config's dimensions, formats, and stride. Filter, linear mode, and sharpen are preserved.
Feature Flags
| Feature | Default | Description |
|---|---|---|
std |
yes | Enables std library. Disable for no_std + alloc. |
layout |
yes | Layout negotiation and pipeline execution via zenlayout. |
avx512 |
no | Native AVX-512 V-filter kernel (x86-64 only). |
zennode |
no | Self-documenting node definitions for zennode pipeline integration. |
pretty-safe |
no | Replaces bounds-checked indexing with get_unchecked in SIMD kernels where bounds are proven by prior guards. ~17% fewer instructions on x86-64. Introduces unsafe; the default build is #![forbid(unsafe_code)]. |
Benchmarks
The benches/ directory contains 19 benchmark binaries covering throughput, precision, and profiling:
| Benchmark | What it measures |
|---|---|
paired_bench |
Interleaved paired comparison against pic-scale, fast_image_resize, resize. Statistical diff with 95% CI. |
resize_bench |
Criterion throughput at 50%, 25%, and 200% scale across image sizes. |
tango_bench |
Regression detection across code changes. |
sweep_bench |
Performance across sizes (64–7680 px) and ratios (12.5%–300%). CSV output. |
precision |
f32/u8 accuracy vs f64 reference and cross-library comparison. |
transfer_bench |
sRGB/BT.709/PQ/HLG transfer function speed vs powf and colorutils-rs. |
planar_bench |
Interleaved vs planar resize strategies at 0.5–24 MP. |
profile_* |
Minimal binaries for callgrind/perf (sRGB, linear, f32, f16, streaming). |
The bench-simd-competitors feature enables SIMD on pic-scale for fair comparison (off by default, so pic-scale runs scalar-only).
Limitations
- No f16 channel type (f32 and u16 cover HDR use cases)
- No narrow/video signal range -- full range only
- Premultiplied input is incompatible with compositing (unpremultiply first, or the pipeline returns
CompositeError::PremultipliedInput) - GrayAlpha and Oklab pixel layouts are not supported
Image tech I maintain
| State of the art codecs* | zenjpeg · zenpng · zenwebp · zengif · zenavif (rav1d-safe · zenrav1e · zenavif-parse · zenavif-serialize) · zenjxl (jxl-encoder · zenjxl-decoder) · zentiff · zenbitmaps · heic · zenraw · zenpdf · ultrahdr · mozjpeg-rs · webpx |
| Compression | zenflate · zenzop |
| Processing | zenresize · zenfilters · zenquant · zenblend |
| Metrics | zensim · fast-ssim2 · butteraugli · resamplescope-rs · codec-eval · codec-corpus |
| Pixel types & color | zenpixels · zenpixels-convert · linear-srgb · garb |
| Pipeline | zenpipe · zencodec · zencodecs · zenlayout · zennode |
| ImageResizer | ImageResizer (C#) — 24M+ NuGet downloads across all packages |
| Imageflow | Image optimization engine (Rust) — .NET · node · go — 9M+ NuGet downloads across all packages |
| Imageflow Server | The fast, safe image server (Rust+C#) — 552K+ NuGet downloads, deployed by Fortune 500s and major brands |
* as of 2026
General Rust awesomeness
archmage · magetypes · enough · whereat · zenbench · cargo-copter
And other projects · GitHub @imazen · GitHub @lilith · lib.rs/~lilith · NuGet (over 30 million downloads / 87 packages)
License
Dual-licensed: AGPL-3.0 or commercial.
I've maintained and developed open-source image server software — and the 40+ library ecosystem it depends on — full-time since 2011. Fifteen years of continual maintenance, backwards compatibility, support, and the (very rare) security patch. That kind of stability requires sustainable funding, and dual-licensing is how we make it work without venture capital or rug-pulls. Support sustainable and secure software; swap patch tuesday for patch leap-year.
Your options:
- Startup license — $1 if your company has under $1M revenue and fewer than 5 employees. Get a key →
- Commercial subscription — Governed by the Imazen Site-wide Subscription License v1.1 or later. Apache 2.0-like terms, no source-sharing requirement. Sliding scale by company size. Pricing & 60-day free trial →
- AGPL v3 — Free and open. Share your source if you distribute.
See LICENSE-COMMERCIAL for details.