zenlayout

zenlayout is a pure-geometry image layout engine for computing resize dimensions, crop regions, and canvas placement.
no_std compatible, #![forbid(unsafe_code)].
[]
= "0.2"
# With RIAPI query string parsing:
= { = "0.2", = ["riapi"] }
What it does
Given source dimensions and a set of commands (orient, crop, region, constrain, pad), zenlayout computes every dimension, crop rect, and placement offset needed to produce the output. It handles EXIF orientation, aspect-ratio-aware scaling, codec alignment (JPEG MCU boundaries), and gain map / secondary plane spatial locking.
What it doesn't do: touch pixels. That's your resize engine's job.
Quick start
use ;
let = new
.auto_orient // EXIF orientation 6 = 90 CW
.fit // fit within 800x600
.output_limits
.plan
.unwrap;
// Pass `request` to your decoder, get back what it actually did
let offer = full_decode;
let plan = ideal.finalize;
// plan.resize_to, plan.canvas, plan.remaining_orientation, etc.
// contain everything the resize engine needs
RIAPI query strings
With the riapi feature, parse URL query strings directly:
use riapi;
let result = parse;
let pipeline = result.instructions
.to_pipeline
.expect;
let = pipeline.plan.expect;
assert_eq!;
assert_eq!;
Supported parameters: w/h, maxwidth/maxheight, mode, scale, crop, anchor, zoom/dpr, srotate/rotate, flip/sflip, autorotate, bgcolor. Non-layout keys (format, quality, effects) are preserved in Instructions::extras() for downstream consumers.
Processing pipeline
The Pipeline builder processes operations in a fixed order, regardless of the order setters are called. Last-setter-wins: calling the same category twice replaces the previous value (standard builder pattern). Orientation is the exception — it always composes algebraically.
Pipeline processing order
1. ORIENT -- All orientation commands (auto_orient, rotate, flip) compose
into a single source transform via D4 group algebra. This happens
regardless of where they appear -- there is no "post-resize flip."
Source dimensions transform to post-orientation space.
.auto_orient(6).rotate_90() = Rotate90 . Rotate90 = Rotate180
.fit(800, 600).flip_h() = flip source, then fit (not "fit then flip")
2. REGION or CROP -- Define the effective source. Crop and region share a
single slot; setting either replaces the other.
- Crop: select a rectangle within the source (origin + size)
- Region: viewport into infinite canvas (edge coords; can crop, pad, or both)
Crop converts to Region internally.
3. CONSTRAIN -- Resize the effective source to target dimensions. The 9
constraint modes control aspect ratio handling.
- Fit/FitCrop/FitPad/Distort will upscale small images
- Within/WithinCrop/WithinPad will not
- PadWithin never upscales but always pads to target canvas
- Single-axis constraints derive the missing dimension from aspect ratio
4. PAD -- Add explicit padding around the constrained result. Additive on
canvas dimensions. Padding does NOT collapse -- pad_uniform(10, color)
always adds exactly 10px on each side regardless of other commands.
5. LIMITS -- Safety limits applied to the final canvas:
a. max -- proportional downscale if canvas exceeds max (security cap)
b. min -- proportional upscale if canvas below min (quality floor)
c. align -- snap to codec multiples (may slightly exceed max/drop below min)
Max always wins over min.
Sequential mode (compute_layout_sequential()): same operations, but commands execute in order. Orient still fuses into a source transform. Multiple crop/region compose (each refines the previous). Last constrain wins. Post-constrain crop/pad adjusts the output canvas.
Two-phase layout
Layout computation splits into two phases to support decoder negotiation (JPEG prescaling, partial decode, hardware orientation):
Commands + Source
|
v
+--------------+ +--------------+
|compute_layout|---->|DecoderRequest|----> Decoder
+--------------+ +--------------+ |
| |
v v
+-----------+ +-------------+ +------------+
|IdealLayout|------>| finalize() |<---|DecoderOffer|
+-----------+ +-------------+ +------------+
|
v
+----------+
|LayoutPlan| -- final operations
+----------+
Phase 1 (Pipeline::plan() or compute_layout()) computes the ideal layout assuming a full decode. It returns an IdealLayout (what the output should look like) and a DecoderRequest (hints for the decoder — crop region, target size, orientation).
Phase 2 (IdealLayout::finalize()) takes a DecoderOffer describing what the decoder actually did (maybe it prescaled to 1/8, applied orientation, or cropped to MCU boundaries). It compensates for the difference and returns a LayoutPlan with the remaining work: what to trim, resize, orient, and place on the canvas.
If your decoder doesn't support any of that, pass DecoderOffer::full_decode(w, h).
Constraint modes
Nine modes control how source dimensions map to target dimensions:
| Mode | Behavior | Aspect ratio | May upscale |
|---|---|---|---|
Fit |
Scale to fit within target | Preserved | Yes |
Within |
Like Fit, but never upscales | Preserved | No |
FitCrop |
Scale to fill target, crop overflow | Preserved | Yes |
WithinCrop |
Like FitCrop, but never upscales | Preserved | No |
FitPad |
Scale to fit, pad to exact target | Preserved | Yes |
WithinPad |
Like FitPad, but never upscales | Preserved | No |
PadWithin |
Never upscale, always pad to target canvas | Preserved | No |
Distort |
Scale to exact target dimensions | Stretched | Yes |
AspectCrop |
Crop to target aspect ratio, no scaling | Preserved | No |
WithinPad vs PadWithin: when the source is smaller than the target, WithinPad returns the image at its original size (identity — no canvas expansion). PadWithin always returns the target canvas dimensions with the image centered on it.
Source 4:3, Target 1:1 (square):
Fit Within FitCrop FitPad
+---+ +---+ +---+ +-----+
| | | | | # | | |
| | | | | # | | ### |
| | | |(smaller) | # | | |
+---+ +---+ +---+ +-----+
exact size <= source fills+crops fits+pads
Single-axis constraints are supported: Constraint::width_only() and Constraint::height_only() derive the other dimension from the source aspect ratio.
Orientation
Orientation models the D4 dihedral group — 4 rotations x 2 flip states = 8 elements, matching EXIF orientations 1-8.
| Orientation | = Rotation | + FlipH? | Swaps axes? |
|---|---|---|---|
| Identity | 0 | no | no |
| FlipH | 0 | yes | no |
| Rotate180 | 180 | no | no |
| FlipV | 180 | yes | no |
| Transpose | 90 CW | yes | yes |
| Rotate90 | 90 CW | no | yes |
| Transverse | 270 CW | yes | yes |
| Rotate270 | 270 CW | no | yes |
Orientations compose algebraically and are verified against the D4 Cayley table:
use Orientation;
let exif6 = from_exif.unwrap; // 90 CW
let combined = exif6.compose;
assert_eq!; // EXIF 5
// Inverse undoes:
assert_eq!;
All orientation commands fuse into a single source transform, regardless of where they appear in the pipeline. There is no "post-resize flip" — orientation is always applied to the source. In sequential mode, if an axis-swapping orientation (Rotate90/270, Transpose, Transverse) appears after a constraint, the constraint's target dimensions are swapped to compensate, producing correct output geometry.
Region
Region defines a viewport rectangle in source coordinates. It unifies crop and pad into a single concept:
- Viewport smaller than source = crop
- Viewport extending beyond source = pad (filled with
color) - Viewport entirely outside source = blank canvas
Coordinates use edge positions (left, top, right, bottom), not origin + size. Region::crop(10, 10, 90, 90) selects an 80x80 area. This differs from SourceCrop::pixels(10, 10, 80, 80) which uses origin + size for the same region.
Each edge is a RegionCoord: a percentage of source dimension plus a pixel offset. This allows expressions like "10% from the left edge" or "50 pixels past the right edge".
use ;
// 50px padding on all sides
let = new
.region
.plan
.unwrap;
// Canvas: 900x700, source at (50, 50)
// Mixed crop+pad: extend left, crop right
let = new
.region_viewport
.plan
.unwrap;
// Canvas: 650x600, 600x600 of source at (50, 0)
// Percentage-based crop: 10% from each edge
let reg = Region ;
SourceCrop converts to Region internally via to_region(). Region and Crop share a single slot — setting either replaces the other.
When a Region is combined with a constraint, the constraint operates on the overlap between the viewport and the source. The viewport's padding areas scale proportionally.
Sequential mode
For scripting use cases where command order matters, use compute_layout_sequential() with a Command slice:
use ;
let commands = ;
let = compute_layout_sequential.unwrap;
Sequential mode differences from fixed mode:
- Orient: still fuses into a single source transform regardless of position
- Crop/Region: compose sequentially (second crop refines the first)
- Constrain: last one wins
- Post-constrain crop/pad: adjusts the output canvas, not the source
- Limits: applied once at the end (same as fixed)
Both modes produce a single Layout — one crop, one resize, one canvas. "Sequential" refers to the command evaluation order, not multi-pass pixel processing. Requires alloc feature.
Secondary planes
For gain maps, depth maps, or alpha planes that share spatial extent with the primary image but live at a different resolution:
use ;
// SDR: 4000x3000, gain map: 1000x750 (1/4 scale)
let = new
.auto_orient
.crop_pixels
.fit
.plan
.unwrap;
// Derive gain map plan -- automatically maintains the source ratio
let = sdr_ideal.derive_secondary;
// Each decoder independently handles its capabilities
let sdr_plan = sdr_ideal.finalize;
let gm_plan = gm_ideal.finalize;
// Both plans produce spatially-locked results
assert_eq!;
Source crop coordinates are scaled from primary to secondary space with round-outward logic (origin floors, extent ceils) to ensure full spatial coverage.
Codec layout
CodecLayout computes per-plane geometry for YCbCr encoders:
use ;
let codec = new;
// Luma plane
assert_eq!;
assert_eq!; // 800 / 8
// Chroma plane (half resolution for 4:2:0)
assert_eq!;
// MCU grid
assert_eq!;
assert_eq!;
// Feed rows in chunks of this size to the encoder
assert_eq!;
Feature flags
| Flag | Default | Implies | Description |
|---|---|---|---|
std |
yes | alloc |
Standard library. Enables Error impl for LayoutError. |
alloc |
via std |
— | Heap allocation (Vec, BTreeMap). Enables compute_layout_sequential. |
riapi |
no | alloc |
RIAPI query string parsing (?w=800&h=600&mode=crop). |
svg |
no | std |
SVG visualization of layout pipeline steps. |
smart-crop |
no | alloc |
Content-aware cropping (experimental, API unstable). |
The core API (Pipeline, Constraint::compute(), compute_layout()) works with zero features — no_std, no heap. Pipeline::plan() makes zero heap allocations.
Error handling
LayoutError is returned from Constraint::compute(), Pipeline::plan(), and Instructions::to_pipeline():
| Variant | Cause |
|---|---|
ZeroSourceDimension |
Source image has zero width or height |
ZeroTargetDimension |
Target width or height is zero |
ZeroRegionDimension |
Region viewport has zero or negative area |
NonFiniteFloat |
A float parameter is NaN or infinity |
NaN/Inf values are rejected at all API boundaries — in the RIAPI parser, at Constraint::compute() entry, and at Instructions::to_pipeline() entry.
Limitations
- Only integer coordinates (no subpixel positioning)
- Sequential mode requires
allocfeature smart-cropfeature is experimental, API unstable- No pixel operations — geometry only
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.