mozjpeg-rs

June 16, 2026 · View on GitHub

100% safe Rust JPEG encoder (#![forbid(unsafe_code)]) with byte-identical output to C mozjpeg in baseline and progressive modes (0.00% avg diff). Trellis modes produce 0.05-0.80% smaller files than C mozjpeg while being 6% faster at 2048×2048. Uses safe SIMD (archmage) on x86_64 (AVX2) and aarch64 (NEON).

Crates.io Documentation CI codecov License

Encoder Only

mozjpeg-rs is a JPEG encoder only. It does not decode JPEG files.

For Encoding & Decoding

zenjpeg — Pure Rust encoder/decoder combining the best of mozjpeg and jpegli innovations with far better size/quality balance than either. Supports JPEG decoding, HDR, gain maps, f32 precision, and highly optimized integer paths. Recommended for new projects.

For Decoding Only

CrateTypeNotes
jpeg-decoderPure RustMost mature, widely deployed
zune-jpegPure RustFast, SIMD-optimized
mozjpegC bindingsWrapper over libjpeg-turbo (world's most widely deployed JPEG decoder)

Note on C mozjpeg bindings: If using the mozjpeg crate, be careful with parameter setting order. Several methods internally call jpeg_set_defaults() which silently resets previously-set values:

  • set_scan_optimization_mode(), set_fastest_defaults() reset: quality, smoothing, pixel density, subsampling, Huffman settings, quantization tables
  • set_color_space() resets: sampling factors, quantization/Huffman table assignments (e.g., 4:2:2 subsampling reverts to 4:2:0)

Call these methods first, then set quality, subsampling, and other options.

Why mozjpeg-rs?

mozjpeg-rsC mozjpeglibjpeg-turbo
LanguagePure RustCC/asm
Memory safetyCompile-time guaranteedManualManual
Trellis quantizationYes (6% faster than C)YesNo
Build complexitycargo addcmake + nasm + C toolchaincmake + nasm
Output parityByte-exact with C mozjpegDifferent output

Choose mozjpeg-rs when you want:

  • Memory-safe JPEG encoding without C dependencies
  • Byte-exact parity with C mozjpeg (or opt into faster color conversion)
  • Smaller files than libjpeg-turbo via trellis quantization
  • Simple integration via Cargo

Choose C mozjpeg when you need:

  • Maximum baseline encoding speed (hand-tuned SIMD entropy coding)
  • Established C ABI for FFI
  • Arithmetic coding (rarely used)

Compression Results vs C mozjpeg

Tested on CID22 corpus (validation subset), 4:2:0 subsampling, exact color match (default). Positive delta = Rust files are larger.

Reproduce with: cargo run --release --example cid22_bench

ConfigQSize ΔMax Dev
Baseline750.00%0.00%
Baseline850.00%0.00%
Baseline900.00%0.00%
Baseline950.00%0.00%
Progressive750.00%0.00%
Progressive850.00%0.00%
Progressive900.00%0.00%
Progressive950.00%0.00%
Baseline+Trellis75-0.47%1.26%
Baseline+Trellis85-0.22%0.74%
Baseline+Trellis90-0.12%0.75%
Baseline+Trellis95-0.05%0.64%
Progressive+Trellis75-0.41%1.10%
Progressive+Trellis85-0.21%0.76%
Progressive+Trellis90-0.15%0.48%
Progressive+Trellis95-0.08%0.61%
MaxCompression75+0.01%0.96%
MaxCompression85+0.15%1.24%
MaxCompression90+0.21%0.85%
MaxCompression95+0.17%1.15%

Configs: Baseline = huffman opt only. +Trellis = AC+DC trellis + deringing. MaxCompression = Progressive + Trellis + optimize_scans.

Highlights:

  • Byte-exact parity — Baseline and Progressive modes produce identical output to C mozjpeg
  • Smaller files with trellis — Rust produces 0.05–0.47% smaller files than C mozjpeg
  • MaxCompression — Within ±0.21% of C, with per-image variance due to different scan optimization choices
SSIMULACRA2 vs BPP Pareto curve

Usage

use mozjpeg_rs::{Encoder, Subsampling};

// Signatures:
//   fn quality(self, quality: u8) -> Encoder         // 1–100, higher = better quality (clamped)
//   fn encode_rgb(&self, rgb: &[u8], width: u32, height: u32) -> Result<Vec<u8>>
// `rgb` is tightly-packed 3-bytes-per-pixel, row-major; `rgb.len()` must equal width*height*3.

// Default: trellis quantization + Huffman optimization
let (width, height): (u32, u32) = (640, 480);
let jpeg = Encoder::default()
    .quality(85) // u8, 1–100, higher is better
    .encode_rgb(&pixels, width, height)?;

// Maximum compression: progressive + trellis + deringing
let jpeg = Encoder::max_compression()
    .quality(85)
    .encode_rgb(&pixels, width, height)?;

// Fastest: no optimizations (libjpeg-turbo compatible output)
let jpeg = Encoder::fastest()
    .quality(85)
    .encode_rgb(&pixels, width, height)?;

// Custom configuration
let jpeg = Encoder::default()
    .quality(75)
    .progressive(true)
    .subsampling(Subsampling::S420)
    .optimize_huffman(true)
    .encode_rgb(&pixels, width, height)?;

// Faster color conversion (trades exact C parity for ~40% faster RGB→YCbCr)
let jpeg = Encoder::default()
    .quality(85)
    .fast_color(true)  // Uses zenyuv, ±1 rounding difference
    .encode_rgb(&pixels, width, height)?;

// Encode 4-channel input directly (alpha ignored, no intermediate buffer)
let jpeg = Encoder::default()
    .quality(85)
    .encode_rgba(&rgba_pixels, width, height)?;

// For BGRA input, swizzle to RGBA first with the `garb` crate:
//   garb::bytes::bgra_to_rgba_inplace(&mut bgra_buf).unwrap();
//   let jpeg = Encoder::default().encode_rgba(&bgra_buf, w, h)?;

Type-Safe Encoding with imgref (default feature)

The imgref feature (enabled by default) provides type-safe encoding with automatic stride handling:

use mozjpeg_rs::Encoder;
use imgref::ImgVec;
use rgb::RGB8;

// Type-safe: dimensions baked in, can't mix up width/height
let pixels: Vec<RGB8> = vec![RGB8::new(128, 64, 32); 640 * 480];
let img = ImgVec::new(pixels, 640, 480);
let jpeg = Encoder::default().quality(85).encode_imgref(img.as_ref())?;

// Subimages work automatically (stride handled internally)
let crop = img.sub_image(100, 100, 200, 200);
let jpeg = encoder.encode_imgref(crop)?;

Supported pixel types: RGB<u8>, RGBA<u8> (alpha discarded), Gray<u8>, [u8; 3], [u8; 4], u8.

Strided Encoding

For memory-aligned buffers or cropping without copy:

// Memory-aligned buffer (rows padded to 256 bytes)
let stride = 256;
let buffer: Vec<u8> = vec![128; stride * height];
let jpeg = encoder.encode_rgb_strided(&buffer, width, height, stride)?;

// Crop without copy - point into larger buffer
let crop_data = &full_image[crop_offset..];
let jpeg = encoder.encode_rgb_strided(crop_data, crop_w, crop_h, full_stride)?;

Cancellation (servers)

For server use, every encode entry point has a *_with_stop variant that takes a cooperative cancellation token as the last argument. The encoder checks it before, during (per scan / per MCU row), and after encoding, so a long encode can be aborted when a client disconnects or a deadline elapses:

fn encode_rgb_with_stop(
    &self,
    rgb: &[u8],
    width: u32,
    height: u32,
    stop: &dyn enough::Stop,
) -> Result<Vec<u8>>;

The Stop trait comes from the enough crate and is re-exported as mozjpeg_rs::Stop. On cancellation the call returns Err(Error::Cancelled); a deadline-based stop that has elapsed returns Err(Error::TimedOut).

No-op (cancellation not needed). Unstoppable is re-exported from this crate, so no extra dependency is required:

use mozjpeg_rs::{Encoder, Unstoppable};

let jpeg = Encoder::default()
    .quality(85)
    .encode_rgb_with_stop(&pixels, width, height, &Unstoppable)?;

Real cancel (timeout / client disconnect). enough itself ships only the no-op Unstoppable; for a triggerable token add the companion crate almost-enough, whose Stopper is a cheap Arc-backed, Clone/Send/Sync flag that implements Stop:

[dependencies]
mozjpeg-rs = "0.9"
almost-enough = "0.4.4"
use mozjpeg_rs::{Encoder, Error};
use almost_enough::Stopper;

let stopper = Stopper::new();

// Hand a clone to a watchdog thread / request-cancellation handler:
let watch = stopper.clone();
// e.g. on client disconnect or after a deadline:  watch.cancel();

let result = Encoder::default()
    .quality(85)
    .encode_rgb_with_stop(&pixels, width, height, &stopper);

match result {
    Ok(jpeg) => { /* send it */ }
    Err(Error::Cancelled) | Err(Error::TimedOut) => { /* client gone / deadline hit */ }
    Err(e) => return Err(e),
}

Grayscale and other inputs have matching *_with_stop entry points (e.g. encode_gray_with_stop). Any type implementing enough::Stop works, so you can wire in your own deadline/atomic-flag token instead of pulling in almost-enough.

Resource limits

Untrusted dimensions can be bounded before any pixel buffer is allocated. Limits is all-off by default (every cap zero = unlimited); set only the caps you need and apply them with .limits(...). Oversized inputs are rejected up front with an error rather than allocating:

use mozjpeg_rs::{Encoder, Limits};

let limits = Limits::default()
    .max_width(20_000)
    .max_height(20_000)
    .max_pixel_count(50_000_000)        // reject before allocating
    .max_alloc_bytes(512 * 1024 * 1024);

let jpeg = Encoder::default()
    .quality(85)
    .limits(limits)
    .encode_rgb(&pixels, width, height)?;

Features

  • Trellis quantization - Rate-distortion optimized coefficient selection (AC + DC)
  • Progressive JPEG - Multi-scan encoding with spectral selection
  • Huffman optimization - 2-pass encoding for optimal entropy coding
  • Overshoot deringing - Reduces ringing artifacts at sharp edges
  • Chroma subsampling - 4:4:4, 4:2:2, 4:2:0 modes
  • Type-safe imgref integration - Encode ImgRef<RGB8> directly with automatic stride handling
  • Strided encoding - Memory-aligned buffers, crop without copy
  • 100% Safe Rust - #![forbid(unsafe_code)] with zero exceptions (archmage + safe_unaligned_simd for SIMD)

Encoder Settings Matrix

All combinations of settings are supported and tested:

SettingBaselineProgressiveNotes
Subsampling
├─ 4:4:4No chroma subsampling
├─ 4:2:2Horizontal subsampling
└─ 4:2:0Full subsampling (default)
Trellis Quantization
├─ AC trellisRate-distortion optimized AC coefficients
└─ DC trellisCross-block DC optimization
Huffman
├─ Default tablesFast, slightly larger files
└─ Optimized tables2-pass, smaller files
Progressive-only
└─ optimize_scansPer-scan Huffman tables
Other
├─ DeringingReduce overshoot artifacts
├─ GrayscaleSingle-component encoding
├─ EOB optimizationCross-block EOB runs (opt-in)
└─ SmoothingNoise reduction filter (for dithered images)

Presets:

  • Encoder::default() - Trellis (AC+DC) + Huffman optimization + Deringing
  • Encoder::max_compression() - Above + Progressive + optimize_scans
  • Encoder::fastest() - No optimizations (libjpeg-turbo compatible)

Quantization Tables

TableDescription
RobidouxDefault. Nicolas Robidoux's psychovisual tables (used by ImageMagick)
JpegAnnexKStandard JPEG tables (libjpeg default)
FlatUniform quantization
MssimTunedMSSIM-optimized quantization tables
PsnrHvsMPSNR-HVS-M tuned
KleinKlein, Silverstein, Carney (1992)
WatsonDCTune (Watson, Taylor, Borthwick 1997)
AhumadaAhumada, Watson, Peterson (1993)
PetersonPeterson, Ahumada, Watson (1993)
use mozjpeg_rs::{Encoder, QuantTableIdx};

let jpeg = Encoder::default()
    .qtable(QuantTableIdx::Robidoux)  // or .quant_tables()
    .encode_rgb(&pixels, width, height)?;

Method Aliases

For CLI-style naming (compatible with rimage conventions):

AliasEquivalent
.baseline(true).progressive(false)
.optimize_coding(true).optimize_huffman(true)
.chroma_subsampling(mode).subsampling(mode)
.qtable(idx).quant_tables(idx)

Performance

Benchmarked on 2048x2048 image (4 megapixels), 30 iterations, release mode with AVX2/NEON:

ConfigurationRustC mozjpeg
Trellis (AC + DC)197 ms210 ms6% faster
Baseline (huffman opt)42 ms9 ms4.6x slower

Reproduce: cargo test --release --test bench_2k -- --nocapture

With trellis quantization (recommended for quality), Rust is faster than C mozjpeg. Baseline-only encoding is slower due to entropy coding; future releases will address this gap.

SIMD Support

mozjpeg-rs uses archmage for safe SIMD with runtime CPU detection:

  • x86_64: AVX2 (automatic, no feature flag needed)
  • aarch64: NEON (automatic, no feature flag needed)
  • Fallback: multiversion autovectorization on other platforms

All SIMD code uses safe Rust intrinsics via archmage and safe_unaligned_simd — no unsafe blocks.

Differences from C mozjpeg

mozjpeg-rs aims for compatibility with C mozjpeg but has some differences:

Featuremozjpeg-rsC mozjpeg
Progressive scan script9-scan with successive approximation (or optimize_scans)9-scan with successive approximation
optimize_scansPer-scan Huffman tablesPer-scan Huffman tables
Trellis EOB optimizationAvailable (opt-in)Available (rarely used)
Smoothing filterAvailableAvailable
Multipass trellisNot implemented (poor tradeoff)Available
Arithmetic codingNot implementedAvailable (rarely used)
Grayscale progressiveYesYes

Output Parity with C mozjpeg

Baseline and Progressive modes: Byte-identical output (0.00% difference) when using default color conversion.

With trellis quantization: Rust produces 0.05-0.80% smaller files than C mozjpeg due to slightly better rate-distortion optimization.

With fast_color(true): ±1 rounding difference in color conversion (uses yuv crate for ~40% faster RGB→YCbCr), producing slightly different but visually identical output.

The FFI comparison tests in tests/ffi_validation.rs verify component-level parity against C mozjpeg.

Development

Running CI Locally

# Format check
cargo fmt --all -- --check

# Clippy lints
cargo clippy --workspace --all-targets -- -D warnings

# Build
cargo build --workspace

# Unit tests
cargo test --lib

# Codec comparison tests
cargo test --test codec_comparison

# FFI validation tests (requires mozjpeg-sys from crates.io)
cargo test --test ffi_validation

Reproduce Benchmarks

# Fetch test corpus (CID22, CLIC, and other images)
./scripts/fetch-corpus.sh

# CID22 benchmark (recommended)
cargo run --release --example cid22_bench

# Performance benchmark (2048×2048)
cargo test --release --test bench_2k -- --nocapture

# Full corpus comparison
cargo run --release --example comprehensive_comparison

Test Coverage

# Install cargo-llvm-cov
cargo install cargo-llvm-cov

# Generate coverage report
cargo llvm-cov --lib --html

# Open report
open target/llvm-cov/html/index.html

License

BSD-3-Clause - Same license as the original mozjpeg.

Acknowledgments

Based on Mozilla's mozjpeg, which builds on libjpeg-turbo and the Independent JPEG Group's libjpeg.

AI-Generated Code Notice

This crate was developed with significant assistance from Claude (Anthropic). While the code has been tested against the C mozjpeg reference implementation and passes 300+ tests including FFI validation, not all code has been manually reviewed or human-audited.

Before using in production:

  • Review critical code paths for your use case
  • Run your own validation against expected outputs
  • Consider the encoder's test suite coverage for your specific requirements

The FFI comparison tests in tests/ffi_comparison.rs and tests/ffi_validation.rs provide confidence in correctness by comparing outputs against C mozjpeg.