mozjpeg-rs
Pure Rust JPEG encoder with byte-exact C mozjpeg parity and trellis quantization for optimal compression. With trellis enabled, produces smaller files than C mozjpeg while being 7% faster.
Encoder Only
mozjpeg-rs is a JPEG encoder only. It does not decode JPEG files.
For decoding, use one of these excellent crates:
| Crate | Type | Notes |
|---|---|---|
| jpeg-decoder | Pure Rust | Widely used, reliable |
| zune-jpeg | Pure Rust | Fast, SIMD-optimized |
| mozjpeg | C bindings | Safe wrapper for C mozjpeg (encode + decode) |
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 tablesset_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-rs | C mozjpeg | libjpeg-turbo | |
|---|---|---|---|
| Language | Pure Rust | C | C/asm |
| Memory safety | Compile-time guaranteed | Manual | Manual |
| Trellis quantization | Yes (7% faster than C) | Yes | No |
| Build complexity | cargo add |
cmake + nasm + C toolchain | cmake + nasm |
| Output parity | Byte-exact with C mozjpeg | — | Different 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 Kodak corpus (24 images), 4:2:0 subsampling, exact color match (default). Positive delta = Rust files are larger.
Reproduce with: cargo test --release --test parity_benchmark -- --nocapture
| Config | Q | Size Δ | Max Dev |
|---|---|---|---|
| Baseline | 75 | 0.00% | 0.00% |
| Baseline | 85 | 0.00% | 0.00% |
| Baseline | 90 | 0.00% | 0.00% |
| Baseline | 95 | 0.00% | 0.00% |
| Progressive | 75 | 0.00% | 0.00% |
| Progressive | 85 | 0.00% | 0.00% |
| Progressive | 90 | 0.00% | 0.00% |
| Progressive | 95 | 0.00% | 0.00% |
| Baseline+Trellis | 75 | -0.47% | 1.26% |
| Baseline+Trellis | 85 | -0.22% | 0.74% |
| Baseline+Trellis | 90 | -0.12% | 0.75% |
| Baseline+Trellis | 95 | -0.05% | 0.64% |
| Progressive+Trellis | 75 | -0.41% | 1.10% |
| Progressive+Trellis | 85 | -0.21% | 0.76% |
| Progressive+Trellis | 90 | -0.15% | 0.48% |
| Progressive+Trellis | 95 | -0.08% | 0.61% |
| MaxCompression | 75 | +0.01% | 0.96% |
| MaxCompression | 85 | +0.15% | 1.24% |
| MaxCompression | 90 | +0.21% | 0.85% |
| MaxCompression | 95 | +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
Usage
use ;
// Default: trellis quantization + Huffman optimization
let jpeg = new
.quality
.encode_rgb?;
// Maximum compression: progressive + trellis + deringing
let jpeg = max_compression
.quality
.encode_rgb?;
// Fastest: no optimizations (libjpeg-turbo compatible output)
let jpeg = fastest
.quality
.encode_rgb?;
// Custom configuration
let jpeg = new
.quality
.progressive
.subsampling
.optimize_huffman
.encode_rgb?;
// Faster color conversion (trades exact C parity for ~40% faster RGB→YCbCr)
let jpeg = new
.quality
.fast_color // Uses yuv crate, ±1 rounding difference
.encode_rgb?;
Type-Safe Encoding with imgref (default feature)
The imgref feature (enabled by default) provides type-safe encoding with automatic stride handling:
use Encoder;
use ImgVec;
use RGB8;
// Type-safe: dimensions baked in, can't mix up width/height
let pixels: = vec!;
let img = new;
let jpeg = new.quality.encode_imgref?;
// Subimages work automatically (stride handled internally)
let crop = img.sub_image;
let jpeg = encoder.encode_imgref?;
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!;
let jpeg = encoder.encode_rgb_strided?;
// Crop without copy - point into larger buffer
let crop_data = &full_image;
let jpeg = encoder.encode_rgb_strided?;
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
- Safe Rust -
#![deny(unsafe_code)]with exceptions only for SIMD intrinsics
Encoder Settings Matrix
All combinations of settings are supported and tested:
| Setting | Baseline | Progressive | Notes |
|---|---|---|---|
| Subsampling | |||
| ├─ 4:4:4 | ✅ | ✅ | No chroma subsampling |
| ├─ 4:2:2 | ✅ | ✅ | Horizontal subsampling |
| └─ 4:2:0 | ✅ | ✅ | Full subsampling (default) |
| Trellis Quantization | |||
| ├─ AC trellis | ✅ | ✅ | Rate-distortion optimized AC coefficients |
| └─ DC trellis | ✅ | ✅ | Cross-block DC optimization |
| Huffman | |||
| ├─ Default tables | ✅ | ✅ | Fast, slightly larger files |
| └─ Optimized tables | ✅ | ✅ | 2-pass, smaller files |
| Progressive-only | |||
| └─ optimize_scans | ❌ | ✅ | Per-scan Huffman tables |
| Other | |||
| ├─ Deringing | ✅ | ✅ | Reduce overshoot artifacts |
| ├─ Grayscale | ✅ | ✅ | Single-component encoding |
| ├─ EOB optimization | ✅ | ✅ | Cross-block EOB runs (opt-in) |
| └─ Smoothing | ✅ | ✅ | Noise reduction filter (for dithered images) |
Presets:
Encoder::new()- Trellis (AC+DC) + Huffman optimization + DeringingEncoder::max_compression()- Above + Progressive + optimize_scansEncoder::fastest()- No optimizations (libjpeg-turbo compatible)
Quantization Tables
| Table | Description |
|---|---|
Robidoux |
Default. Nicolas Robidoux's psychovisual tables (used by ImageMagick) |
JpegAnnexK |
Standard JPEG tables (libjpeg default) |
Flat |
Uniform quantization |
MssimTuned |
MSSIM-optimized quantization tables |
PsnrHvsM |
PSNR-HVS-M tuned |
Klein |
Klein, Silverstein, Carney (1992) |
Watson |
DCTune (Watson, Taylor, Borthwick 1997) |
Ahumada |
Ahumada, Watson, Peterson (1993) |
Peterson |
Peterson, Ahumada, Watson (1993) |
use ;
let jpeg = new
.qtable // or .quant_tables()
.encode_rgb?;
Method Aliases
For CLI-style naming (compatible with rimage conventions):
| Alias | Equivalent |
|---|---|
.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:
| Configuration | Rust | C mozjpeg | |
|---|---|---|---|
| Trellis (AC + DC) | 202 ms | 217 ms | 7% faster |
| Baseline (huffman opt) | 48 ms | 10 ms | 5x 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 multiversion for automatic vectorization by default. Optional hand-written SIMD intrinsics are available:
[]
= { = "0.7", = ["simd-intrinsics"] }
In benchmarks, the difference is minimal (~2%) as multiversion autovectorization works well for DCT and color conversion.
Differences from C mozjpeg
mozjpeg-rs aims for compatibility with C mozjpeg but has some differences:
| Feature | mozjpeg-rs | C mozjpeg |
|---|---|---|
| Progressive scan script | 9-scan with successive approximation (or optimize_scans) | 9-scan with successive approximation |
| optimize_scans | Per-scan Huffman tables | Per-scan Huffman tables |
| Trellis EOB optimization | Available (opt-in) | Available (rarely used) |
| Smoothing filter | Available | Available |
| Multipass trellis | Not implemented (poor tradeoff) | Available |
| Arithmetic coding | Not implemented | Available (rarely used) |
| Grayscale progressive | Yes | Yes |
Why multipass (use_scans_in_trellis) is not implemented
C mozjpeg's multipass option makes trellis quantization "scan-aware" for progressive encoding by optimizing low and high frequency AC coefficients separately. Benchmarks on the test corpus (Q85, progressive) show this is a poor tradeoff:
| Metric | Without Multipass | With Multipass | Difference |
|---|---|---|---|
| File size | 1,760 KB | 1,770 KB | +0.52% larger |
| Quality (butteraugli) | 2.59 | 2.54 | -0.05 (imperceptible) |
| Encoding time | ~7ms | ~8.5ms | ~20% slower |
Multipass produces larger files, is slower, and provides no perceptible quality improvement.
Where does the remaining gap come from?
The consistent +0.21% gap in non-trellis modes comes from the fast-yuv feature, which uses the yuv crate for SIMD color conversion (AVX-512/AVX2/SSE/NEON). It has ±1 level rounding differences vs C mozjpeg's color conversion, producing slightly different DCT coefficients. This is invisible after JPEG quantization. Without fast-yuv, Rust matches or beats C at all quality levels.
With trellis enabled, Rust's trellis optimizer finds slightly better rate-distortion tradeoffs at Q75, producing smaller files than C.
Matching C mozjpeg output exactly
For near byte-identical output to C mozjpeg, use baseline mode with matching settings:
- Use baseline (non-progressive) mode with Huffman optimization
- Match all encoder settings via
TestEncoderConfig - Use the same quantization tables (Robidoux/ImageMagick, the default for both)
The FFI comparison tests in tests/ffi_comparison.rs verify component-level parity.
Development
Running CI Locally
# Format check
# Clippy lints
# Build
# Unit tests
# Codec comparison tests
# FFI validation tests (requires mozjpeg-sys from crates.io)
Reproduce Benchmarks
# Fetch test corpus (CID22 images)
# Run full corpus comparison
# Run pareto benchmark
Test Coverage
# Install cargo-llvm-cov
# Generate coverage report
# Open report
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 248 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.