garb [](https://github.com/imazen/garb/actions/workflows/ci.yml) [](https://crates.io/crates/garb) [](https://lib.rs/crates/garb) [](https://docs.rs/garb) [](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) [](#license) [](https://codecov.io/gh/imazen/garb)

June 28, 2026 · View on GitHub

Dress your pixels for the occasion.

You can't show up to a function in the wrong style. Swap your BGR for your RGB, your ARGB for your RGBA, and tie up loose ends like that unreliable alpha BGRX.

SIMD-accelerated pixel format conversions: x86-64 AVX2, ARM NEON, WASM SIMD128, with automatic scalar fallback. no_std compatible, #![forbid(unsafe_code)].

Quick start

[dependencies]
garb = "0.2.8"
use garb::bytes::{rgba_to_bgra_inplace, rgb_to_bgra};

// In place: swap R↔B across a 4-bytes-per-pixel buffer (RGBA → BGRA).
let mut pixels = vec![255u8, 0, 128, 255];
rgba_to_bgra_inplace(&mut pixels)?;
assert_eq!(pixels, [128, 0, 255, 255]);

// Copy + expand: RGB (3 bpp) → BGRA (4 bpp), alpha filled to 255.
let rgb = [255u8, 0, 128];
let mut bgra = [0u8; 4];
rgb_to_bgra(&rgb, &mut bgra)?;
assert_eq!(bgra, [128, 0, 255, 255]);
# Ok::<(), garb::SizeError>(())

Every function returns Result<(), SizeError> — no panics, no silent truncation — and ships a _strided companion for padded row layouts. Enable the rgb / imgref features to let the compiler pick the conversion from typed pixel slices (see Usage). For the full operation list, see the Function reference.

What it does

Converts between pixel layouts at the byte-slice level. Every image decoder and renderer has an opinion about channel order and pixel width, and none of them agree. garb handles the mechanical part — swapping, expanding, and stripping channels — so you can get back to the interesting work.

SIMD-optimized (contiguous and strided)

  • RGBA ↔ BGRA (in-place and copy)
  • RGB ↔ BGR (in-place and copy)
  • RGB → RGBA / BGRA
  • BGR → BGRA / RGBA
  • RGBA / BGRA → RGB / BGR (drop alpha)
  • Gray → RGBA / BGRA
  • GrayAlpha → RGBA / BGRA
  • Fill alpha (set byte 3 = 255 in each 4-byte pixel, for RGBA/BGRA layouts)
  • ARGB ↔ RGBA / BGRA / ABGR (in-place and copy)
  • RGB → ARGB / ABGR
  • BGR → ARGB / ABGR
  • ARGB / ABGR → RGB / BGR (drop alpha)
  • Gray → ARGB / ABGR
  • GrayAlpha → ARGB / ABGR
  • Fill alpha (set byte 0 = 255 in each 4-byte pixel, for ARGB/ABGR/XRGB/XBGR layouts)

Experimental (feature experimental — API may change)

  • RGB565 → RGBA / BGRA (little-endian packed 16-bit, auto-vectorized)
  • RGBA / BGRA → RGB565 (lossy compress, round-to-nearest, auto-vectorized)
  • RGBA4444 → RGBA / BGRA (little-endian packed 16-bit, auto-vectorized)
  • RGBA / BGRA → RGBA4444 (lossy compress, round-to-nearest, auto-vectorized)
  • RGBA1010102 ↔ interleaved RGBA u16 (LE packed r | g<<10 | b<<20 | a<<30, matches DXGI R10G10B10A2_UNORM / Vulkan A2B10G10R10_UNORM_PACK32 / WGPU Rgb10a2Unorm; 2-bit alpha bit-replicated to 10 bits per the graphics-API convention; transfer functions are NOT applied — chain with linear-srgb for PQ/HLG)
  • u8 alpha premultiply for RGBA / BGRA (exact integer, auto-vectorized)
  • Gray → RGB, GrayAlpha → RGB, Gray ↔ GrayAlpha
  • RGB / RGBA / BGR / BGRA → Gray (weighted luma: BT.709, BT.601, BT.2020; or identity)
  • Depth conversion: u8 ↔ u16, u8 ↔ f32, u16 ↔ f32
  • f32 alpha premultiply / unpremultiply (in-place and copy, AVX2 SIMD)

All operations have _strided variants for images with padding between rows (common in video frames and GPU textures).

Performance

Hand-written SIMD beats the naive autovectorized chunks_exact loop by up to on x86-64 (AVX2), 3.6× on aarch64 (NEON), and on WASM (SIMD128), measured on 1920×1080 buffers. Run cargo bench for hardware-specific numbers; full per-platform tables, methodology, and reproduction commands are in benchmarks/.

All benchmarks on 1920×1080 buffers. "Naive" is the obvious chunks_exact loop — what the compiler autovectorizes on its own. Numbers from GitHub Actions CI (runtime SIMD dispatch, built without -C target-cpu=native); run cargo bench locally for hardware-specific results.

x86-64 (AVX2) — Linux, Zen 4

Operationgarbnaivespeedup
RGBA ↔ BGRA (in-place)150 µs1,078 µs7.2x
RGB ↔ BGR (in-place)329 µs1,038 µs3.2x
RGB ↔ BGR (copy)209 µs1,509 µs7.2x
RGBA → RGB (strip alpha)255 µs1,556 µs6.1x
BGRA → RGB (strip + swap)260 µs1,556 µs6.0x
RGB → RGBA (expand)328 µs1,764 µs5.4x
Fill alpha138 µs329 µs2.4x

aarch64 (NEON) — Linux, Ampere Altra

Operationgarbnaivespeedup
RGBA ↔ BGRA (in-place)243 µs865 µs3.6x
RGB ↔ BGR (in-place)369 µs857 µs2.3x
Fill alpha242 µs495 µs2.0x
RGB ↔ BGR (copy)221 µs219 µs~1x
RGBA → RGB (strip alpha)279 µs278 µs~1x
BGRA → RGB (strip + swap)277 µs278 µs~1x
RGB → RGBA (expand)316 µs313 µs~1x

In-place swaps and fill use hand-written NEON and are 2–3.6x faster on all ARM hardware tested (Ampere Altra, Apple Silicon, Snapdragon X). Cross-bpp operations (3↔4 channel, 3bpp copy) use LLVM's autovectorizer, which generates optimal code for these patterns on AArch64.

WASM (SIMD128) — wasmtime

Operationgarbnaivespeedup
RGBA ↔ BGRA (in-place)230 µs1,041 µs4.5x
RGB ↔ BGR (in-place)494 µs1,027 µs2.1x
RGB ↔ BGR (copy)333 µs2,753 µs8.3x
RGBA → RGB (strip alpha)506 µs1,623 µs3.2x
BGRA → RGB (strip + swap)659 µs2,309 µs3.5x
RGB → RGBA (expand)998 µs2,271 µs2.3x
Fill alpha193 µs650 µs3.4x

Full benchmark results for all six native platforms plus WASM are available in the CI artifacts. Run cargo bench to reproduce locally.

Usage

The core &[u8] / &mut [u8] API lives in garb::bytes. Every function returns Result<(), SizeError> — no panics, no silent truncation.

use garb::bytes::{rgba_to_bgra_inplace, rgb_to_bgra};
use garb::SizeError;

// In-place: swap R↔B in a 4bpp buffer
let mut pixels = vec![255u8, 0, 128, 255,  0, 200, 100, 255];
rgba_to_bgra_inplace(&mut pixels)?;
assert_eq!(pixels, [128, 0, 255, 255,  100, 200, 0, 255]);

// Copy: RGB (3 bpp) → BGRA (4 bpp), alpha filled to 255
let rgb = vec![255u8, 0, 128];
let mut bgra = vec![0u8; 4];
rgb_to_bgra(&rgb, &mut bgra)?;
assert_eq!(bgra, [128, 0, 255, 255]);

// ARGB → RGBA: rotate bytes left [A,R,G,B] → [R,G,B,A]
let mut argb = vec![255u8, 128, 0, 64];
garb::bytes::argb_to_rgba_inplace(&mut argb)?;
assert_eq!(argb, [128, 0, 64, 255]);
# Ok::<(), SizeError>(())

Strided images

A stride is the distance between the start of one row and the start of the next, measured in units of the slice's element type. For the core &[u8] API that means bytes; for the typed imgref API it means elements of the slice's item type (e.g. pixel count for ImgRef<Rgba<u8>>). When stride > width the gap is padding — garb never reads or writes it.

All _strided functions take dimensions before strides:

  • In-place: (buf, width, height, stride)
  • Copy: (src, dst, width, height, src_stride, dst_stride)
use garb::bytes::{rgba_to_bgra_inplace_strided, rgb_to_bgra_strided};

// In-place: 60 pixels wide, stride=256 bytes, 100 rows
let mut buf = vec![0u8; 256 * 100];
rgba_to_bgra_inplace_strided(&mut buf, 60, 100, 256)?;

// Copy with different strides: RGB (stride=192) → BGRA (stride=256)
let rgb_buf = vec![0u8; 192 * 100];
let mut bgra_buf = vec![0u8; 256 * 100];
rgb_to_bgra_strided(&rgb_buf, &mut bgra_buf, 60, 100, 192, 256)?;
# Ok::<(), garb::SizeError>(())

Type-safe conversions (feature rgb)

With the rgb crate, use garb::convert and garb::convert_inplace with typed pixel slices. The right conversion is selected at compile time from the src/dst types — no need to remember function names. In-place swaps return reinterpreted references (zero-copy).

use rgb::{Rgba, Bgra, Rgb};
use garb::{convert, convert_inplace};

// In-place: type-inferred from the return binding
let mut pixels: Vec<Rgba<u8>> = vec![Rgba::new(255, 0, 128, 255); 100];
let bgra: &mut [Bgra<u8>] = convert_inplace(&mut pixels);

// Copy: type-inferred from src and dst
let rgb = vec![Rgb::new(255u8, 0, 128); 100];
let mut bgra = vec![Bgra::default(); 100];
convert(&rgb, &mut bgra).unwrap();

Whole-image conversions (feature imgref)

garb::convert_imgref and garb::convert_imgref_inplace handle strided ImgVec / ImgRef / ImgRefMut types from the imgref crate. In-place conversions consume and return the ImgVec with the buffer reinterpreted. Copy conversions take ImgRef + ImgRefMut — you own the destination buffer.

use rgb::{Rgba, Bgra};
use imgref::ImgVec;
use garb::convert_imgref_inplace;

let rgba_img = ImgVec::new(vec![Rgba::new(255, 0, 128, 200); 640 * 480], 640, 480);
let bgra_img: ImgVec<Bgra<u8>> = convert_imgref_inplace(rgba_img);

Feature flags

FeatureDefaultWhat it adds
stdyesEnables std on dependencies (e.g. archmage)
experimentalnoPacked formats (RGB565, RGBA4444), gray layout, weighted luma, depth conversion, f32 premul (API may change)
rgbnogarb::typed_rgb — conversions on Rgba<u8>, Bgra<u8>, etc.
imgrefnogarb::imgref — whole-image conversions on ImgVec / ImgRef (implies rgb)

The crate is no_std by default — the core byte-slice API, plus the experimental and rgb surfaces, need no allocator (they operate on caller-owned slices). Only the imgref feature pulls in alloc.

SIMD dispatch

garb uses archmage for runtime SIMD detection with compile-time acceleration. On each platform:

  • x86-64: AVX2 (checked at runtime via cpuid)
  • aarch64: NEON (compile-time guaranteed on AArch64)
  • wasm32: SIMD128 (compile-time via target-feature=+simd128)
  • Fallback: Scalar code on all platforms, always available

The first call to each function detects and caches the best available tier. There's no setup, no feature flags to configure, and no unsafe — archmage handles it all behind safe token types.

Function reference

Functions follow {src}_to_{dst} for copies, {src}_to_{dst}_inplace for mutations. Symmetric swaps (like RGBA↔BGRA) provide both names as aliases. Append _strided for padded row layouts.

Core API — garb::bytes (&[u8])

Every function returns Result<(), SizeError>. All have _strided variants.

FunctionOperation
rgba_to_bgra_inplaceSwap R↔B in 4bpp buffer (RGBA↔BGRA)
rgba_to_bgraCopy 4bpp, swapping R↔B
rgb_to_bgr_inplaceSwap R↔B in 3bpp buffer (RGB↔BGR)
rgb_to_bgrCopy 3bpp, swapping R↔B
rgb_to_rgba3bpp → 4bpp, alpha = 255
rgb_to_bgra3bpp → 4bpp, swap R↔B, alpha = 255
bgr_to_rgba3bpp → 4bpp, swap R↔B, alpha = 255
bgr_to_bgra3bpp → 4bpp, alpha = 255
rgba_to_rgb4bpp → 3bpp, drop alpha
bgra_to_rgb4bpp → 3bpp, swap R↔B, drop alpha
bgra_to_bgr4bpp → 3bpp, drop alpha
rgba_to_bgr4bpp → 3bpp, swap R↔B, drop alpha
gray_to_rgba1bpp → 4bpp (R=G=B=gray, A=255)
gray_alpha_to_rgba2bpp → 4bpp (R=G=B=gray, A=alpha)
fill_alpha_rgbaSet byte 3 to 255 in each 4-byte pixel (alpha-last: RGBA/BGRA)
argb_to_rgba_inplaceRotate bytes left in 4bpp buffer: [A,R,G,B]→[R,G,B,A]
argb_to_rgbaCopy 4bpp, rotating bytes left by 1 (ARGB→RGBA)
rgba_to_argb_inplaceRotate bytes right in 4bpp buffer: [R,G,B,A]→[A,R,G,B]
rgba_to_argbCopy 4bpp, rotating bytes right by 1 (RGBA→ARGB)
argb_to_bgra_inplaceReverse each pixel's 4 bytes: [A,R,G,B]→[B,G,R,A]
argb_to_bgraCopy 4bpp, reversing byte order (ARGB→BGRA)
fill_alpha_argbSet byte 0 to 255 in each 4-byte pixel (alpha-first: ARGB/ABGR)
rgb_to_argb3bpp → 4bpp, alpha=255 prepended
rgb_to_abgr3bpp → 4bpp, channels reversed, alpha=255 prepended
argb_to_rgb4bpp → 3bpp, drop leading alpha
argb_to_bgr4bpp → 3bpp, drop alpha + reverse channels
gray_to_argb1bpp → 4bpp (A=255, R=G=B=gray)
gray_alpha_to_argb2bpp → 4bpp (alpha first, R=G=B=gray)

Aliases: bgra_to_rgba_inplace, bgra_to_rgba, bgr_to_rgb_inplace, bgr_to_rgb, gray_to_bgra, gray_alpha_to_bgra, abgr_to_bgra_inplace, abgr_to_bgra, bgra_to_abgr_inplace, bgra_to_abgr, bgra_to_argb_inplace, bgra_to_argb, abgr_to_rgba_inplace, abgr_to_rgba, rgba_to_abgr_inplace, rgba_to_abgr, fill_alpha_abgr, fill_alpha_xrgb, fill_alpha_xbgr, bgr_to_argb, bgr_to_abgr, abgr_to_bgr, abgr_to_rgb, gray_to_abgr, gray_alpha_to_abgr.

Experimental (feature = "experimental")

FunctionOperation
rgb565_to_rgbaRGB565 (LE u16, 2bpp) → RGBA (4bpp), A=255
rgb565_to_bgraRGB565 (LE u16, 2bpp) → BGRA (4bpp), A=255
rgba_to_rgb565RGBA (4bpp) → RGB565 (LE u16, 2bpp), lossy, alpha dropped
bgra_to_rgb565BGRA (4bpp) → RGB565 (LE u16, 2bpp), lossy, alpha dropped
rgba4444_to_rgbaRGBA4444 (LE u16, 2bpp) → RGBA (4bpp)
rgba4444_to_bgraRGBA4444 (LE u16, 2bpp) → BGRA (4bpp)
rgba_to_rgba4444RGBA (4bpp) → RGBA4444 (LE u16, 2bpp), lossy
bgra_to_rgba4444BGRA (4bpp) → RGBA4444 (LE u16, 2bpp), lossy
rgba1010102_to_rgba16RGBA1010102 (LE u32, 4bpp) → interleaved RGBA u16 (4 channels in [0, 1023]); 2-bit alpha bit-replicated to 10 bits
rgba16_to_rgba1010102Interleaved RGBA u16 → RGBA1010102 (LE u32, 4bpp); alpha rounded to 2 bits via (a*3+511)/1023
premultiply_alpha_rgba_u8Premultiply alpha in [R,G,B,A] u8 buffer (in-place)
premultiply_alpha_rgba_u8_copyPremultiply alpha u8, copy variant
gray_to_rgb1bpp → 3bpp (R=G=B=gray)
gray_alpha_to_rgb2bpp → 3bpp (R=G=B=gray, drop alpha)
gray_to_gray_alpha1bpp → 2bpp (A=255)
gray_alpha_to_gray2bpp → 1bpp (drop alpha)
rgb_to_gray3bpp → 1bpp weighted luma (BT.709 default)
rgba_to_gray4bpp → 1bpp weighted luma (BT.709 default)
rgb_to_gray_bt7093bpp → 1bpp BT.709 luma
rgba_to_gray_bt6014bpp → 1bpp BT.601 luma (also _bt709, _bt2020)
rgb_to_gray_identity3bpp → 1bpp, take first channel (for R=G=B data)
rgba_to_gray_identity4bpp → 1bpp, take first channel (for R=G=B data)
convert_u8_to_u16Depth: u8 → u16 (0–255 → 0–65535)
convert_u16_to_u8Depth: u16 → u8
convert_u8_to_f32Depth: u8 → f32 (0–255 → 0.0–1.0)
convert_f32_to_u8Depth: f32 → u8 (clamped)
convert_u16_to_f32Depth: u16 → f32 (0–65535 → 0.0–1.0)
convert_f32_to_u16Depth: f32 → u16 (clamped)
premultiply_alpha_f32Premultiply alpha in [R,G,B,A] f32 buffer (in-place)
unpremultiply_alpha_f32Unpremultiply alpha in [R,G,B,A] f32 buffer (in-place)
premultiply_alpha_f32_copyPremultiply alpha, copy variant
unpremultiply_alpha_f32_copyUnpremultiply alpha, copy variant

Aliases: premultiply_alpha_bgra_u8, premultiply_alpha_bgra_u8_copy, bgr_to_gray, bgra_to_gray, plus _identity and BGR variants for all gray conversions. All functions have _strided variants.

Deinterleave — garb::deinterleave (feature = "experimental")

Pure identity (no transfer-function, no color matrix, no normalization) interleave/deinterleave between packed-RGB(A) buffers and f32 planes.

Two flavors of dispatch underneath, chosen per signal:

  • u8 / u16 inputs — hand-written _mm_shuffle_epi8 deinterleave + 256-bit AVX2 widening (_mm256_cvtepu8_epi32 + _mm256_storeu_ps). Verified +21–52% over LLVM autovec at L1–L3 sizes (see benchmarks/{rgb24,rgb48}_chunk_vs_autovec_2026-05-07). The _mm_shuffle_epi8 mask pattern is a deinterleave LLVM autovec can't infer from generic strided indexing, so the hand-written kernel earns its keep.
  • f32 inputs#[autoversion(v3, neon, wasm128)] over the inline scalar loop. LLVM autovec under each tier's target_feature emits 256-bit YMM (AVX2) / vld3q_f32 (NEON) / v128.load (wasm SIMD128) on the loop body. Beats the prior hand-written 128-bit-XMM chunks by 26–37% at 1024 px (see benchmarks/deinterleave_autovec_vs_chunk_2026-05-07).
FunctionOperation
rgb24_to_planes_f32RGB24 (u8$, 3\text{bpp}) → 3 \times $f32 planes (R, G, B). AVX2 chunk SIMD: vpshufb + vpmovzxbd + vcvtdq2ps. NEON: vld3q_u8.
rgb48_to_planes_f32RGB48 (u16$, 6\text{bpp}) → 3 \times $f32 planes. AVX2 chunk SIMD: 3-way vpshufb + vpmovzxwd. NEON: vld3q_u16.
`rgb_f32_to_planes_f32$\text{f32} \text{RGB} \text{interleaved} → 3 \times $f32planes (identity, no widen).#[autoversion]` autovec.
`rgba_f32_to_planes_f32$\text{f32} \text{RGBA} \text{interleaved} → 4 \times $f32` planes.
`planes_f32_to_rgb_f32$3 \times $f32` planes → f32 RGB interleaved (gather).
`planes_f32_to_rgba_f32$4 \times $f32` planes → f32 RGBA interleaved.

Chunk-level u8/u16 hooks for fusion into caller SIMD loops

For callers already inside a #[target_feature(enable = "avx2,...")] region (set up by #[arcane], #[rite], or #[magetypes]), the u8/u16 chunk-level primitives skip per-call dispatch. They're tokenless — the caller's region establishes target_feature; #[rite(v3)] on each function adds the matching #[target_feature] and inlines into the caller's body without crossing an LLVM optimization boundary.

FunctionOperation
`rgb24_chunk8_to_planes_tokenless_v3$8 \text{packed} \text{RGB24} \text{pixels} → 3 \times $[f32; 8]` (inline AVX2)
`rgb48_chunk8_to_planes_tokenless_v3$8 \text{packed} \text{RGB48} \text{pixels} → 3 \times $[f32; 8]` (inline AVX2)
rgb24_chunk8_to_planes_scalarScalar fallback (autovec'd inline by the caller's target_feature region)
rgb48_chunk8_to_planes_scalarScalar fallback for u16

(See zenanalyze::tier1 for a complete example.)

Chunk-level f32 hooks (scalar only)

For f32 input where autovec is the right answer, only the scalar chunks are exposed — callers in their own #[arcane(<tier>)] region get 256-bit YMM autovec'd by LLVM. The tokenless _v3 / _neon / _wasm128 variants don't ship: they were faster at 128-bit XMM, slower than autovec at 256-bit YMM, and no longer earn their keep.

FunctionShape
{rgb,rgba}_f32_chunk{4,8,16}_to_planes_scalarf32 chunk → planes (deinterleave)
planes_to_{rgb,rgba}_f32_chunk{4,8,16}_scalarplanes → f32 chunk (interleave)

Generic API — convert / convert_inplace (feature rgb)

Type-inferred conversions on rgb crate pixel slices. The compiler selects the right SIMD-optimized conversion from the source and destination types.

FunctionDescription
convert(&[S], &mut [D])Copy-convert between any supported pixel types
convert_inplace(&mut [S]) -> &mut [D]In-place swap for same-size types (zero-copy)
typed_rgb::fill_alpha_rgbaSet A=255 in &mut [Rgba<u8>]
typed_rgb::fill_alpha_bgraSet A=255 in &mut [Bgra<u8>]

convert_inplace pairs (same-size, zero-copy):

FromTo
Rgba<u8>Bgra<u8>
Bgra<u8>Rgba<u8>
Rgb<u8>Bgr<u8>
Bgr<u8>Rgb<u8>

convert pairs (copy):

FromTo
Rgba<u8>Bgra<u8>, Rgb<u8>, Bgr<u8>
Bgra<u8>Rgba<u8>, Bgr<u8>, Rgb<u8>
Rgb<u8>Rgba<u8>, Bgra<u8>
Bgr<u8>Rgba<u8>, Bgra<u8>
Gray<u8>Rgba<u8>, Bgra<u8>
GrayAlpha<u8>Rgba<u8>, Bgra<u8>

Additional pairs with experimental:

FromTo
Gray<u8>Rgb<u8>, Bgr<u8>, GrayAlpha<u8>
GrayAlpha<u8>Rgb<u8>, Bgr<u8>, Gray<u8>
Rgb<u8>Gray<u8> (identity)
Rgba<u8>Gray<u8> (identity)
Bgr<u8>Gray<u8> (identity)
Bgra<u8>Gray<u8> (identity)

Weighted luma conversions (rgb_to_gray_bt709_buf, etc.) and premultiply_rgba_f32 / unpremultiply_rgba_f32 remain as named functions.

ARGB/ABGR types are not in the rgb crate, so those conversions are only available through garb::bytes.

The previous named functions (rgba_to_bgra_mut, rgb_to_bgra_buf, etc.) are deprecated but still available.

Generic API — convert_imgref / convert_imgref_inplace (feature imgref)

Type-inferred conversions on ImgVec / ImgRef / ImgRefMut from the imgref crate. Same type pairs as above.

FunctionDescription
convert_imgref(ImgRef<S>, ImgRefMut<D>)Copy-convert between images
convert_imgref_inplace(ImgVec<S>) -> ImgVec<D>In-place, returns reinterpreted image

With experimental: additional pairs plus weighted luma and premultiply.

The previous named functions (swap_rgba_to_bgra, convert_rgb_to_bgra, etc.) are deprecated but still available.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Image tech I maintain

Codecs ¹zenjpeg · zenpng · zenwebp · zengif · zenavif · zenjxl · zenbitmaps · heic · zentiff · zenpdf · zensvg · zenjp2 · zenraw · ultrahdr
Codec internalszenjxl-decoder · jxl-encoder · zenrav1e · rav1d-safe · zenavif-parse · zenavif-serialize
Compressionzenflate · zenzop · zenzstd
Processingzenresize · zenquant · zenblend · zenfilters · zensally · zentone
Pixels & colorzenpixels · zenpixels-convert · linear-srgb · garb
Pipeline & frameworkzenpipe · zencodec · zencodecs · zenlayout · zennode · zenwasm · zentract
Metricszensim · fast-ssim2 · butteraugli · zenmetrics · resamplescope-rs
Pickers & MLzenanalyze · zenpredict · zenpicker
ProductsImageflow image engine (.NET · Node · Go) · Imageflow Server · ImageResizer (C#)

¹ pure-Rust, #![forbid(unsafe_code)] codecs, as of 2026

General Rust awesomeness

zenbench · archmage · magetypes · enough · whereat · cargo-copter

Open source · @imazen · @lilith · lib.rs/~lilith