use butteraugli::ButteraugliParams;
use dssim::Dssim;
use jpegli::decoder::Decoder;
use jpegli::decoder::PixelFormat;
use jpegli::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
use rgb::RGBA8;
use std::fs;
const CPP_S444_OPT: &[(u8, usize)] = &[
(5, 16110),
(10, 19304),
(15, 23287),
(20, 27343),
(25, 31260),
(30, 34005),
(35, 35436),
(40, 36936),
(45, 38923),
(50, 40756),
(55, 43031),
(60, 46112),
(65, 49261),
(70, 53576),
(75, 58543),
(80, 65486),
(85, 75782),
(90, 93897),
(95, 131380),
(100, 311516),
];
const CPP_S444_FIXED: &[(u8, usize)] = &[
(5, 16179),
(10, 19230),
(15, 23054),
(20, 27062),
(25, 31019),
(30, 33848),
(35, 35351),
(40, 36831),
(45, 38883),
(50, 40752),
(55, 43028),
(60, 46246),
(65, 49393),
(70, 53870),
(75, 58935),
(80, 66177),
(85, 76758),
(90, 95490),
(95, 134545),
(100, 323934),
];
const CPP_S420_OPT: &[(u8, usize)] = &[
(5, 13683),
(10, 16496),
(15, 19728),
(20, 23001),
(25, 26184),
(30, 28175),
(35, 29512),
(40, 30758),
(45, 32278),
(50, 33895),
(55, 36134),
(60, 38419),
(65, 41460),
(70, 45148),
(75, 49778),
(80, 55515),
(85, 64179),
(90, 80096),
(95, 111770),
(100, 208485),
];
const CPP_S422_OPT: &[(u8, usize)] = &[
(5, 14803),
(10, 17666),
(15, 21170),
(20, 24753),
(25, 28169),
(30, 30548),
(35, 31831),
(40, 33193),
(45, 34995),
(50, 36634),
(55, 38642),
(60, 41527),
(65, 44266),
(70, 48286),
(75, 52723),
(80, 58927),
(85, 68134),
(90, 84008),
(95, 115980),
(100, 247442),
];
const RUST_S444_OPT: &[(u8, usize, f64, f64)] = &[
(5, 16249, 0.016157, 8.99133492),
(10, 19463, 0.011888, 7.43367863),
(15, 23405, 0.008844, 6.82907391),
(20, 27462, 0.006670, 5.58060598),
(25, 31364, 0.005155, 5.49073267),
(30, 34153, 0.004404, 5.54542303),
(35, 35580, 0.004093, 5.06215096),
(40, 37044, 0.003811, 4.84684324),
(45, 39113, 0.003430, 4.51536274),
(50, 40943, 0.003119, 4.02269220),
(55, 43221, 0.002818, 3.90750265),
(60, 46292, 0.002488, 3.80471468),
(65, 49444, 0.002176, 3.73590970),
(70, 53785, 0.001837, 3.48123431),
(75, 58803, 0.001539, 2.88890886),
(80, 65775, 0.001221, 2.56708694),
(85, 76014, 0.000872, 2.18441653),
(90, 94015, 0.000512, 1.63023186),
(95, 131676, 0.000218, 1.06197035),
(100, 320049, 0.000033, 0.46010166),
];
const RUST_S444_FIXED: &[(u8, usize, f64, f64)] = &[
(5, 18915, 0.016157, 8.99133492),
(10, 21721, 0.011888, 7.43367863),
(15, 25340, 0.008844, 6.82907391),
(20, 29279, 0.006670, 5.58060598),
(25, 33243, 0.005155, 5.49073267),
(30, 36054, 0.004404, 5.54542303),
(35, 37604, 0.004093, 5.06215096),
(40, 39070, 0.003811, 4.84684324),
(45, 41196, 0.003430, 4.51536274),
(50, 43154, 0.003119, 4.02269220),
(55, 45445, 0.002818, 3.90750265),
(60, 48594, 0.002488, 3.80471468),
(65, 51795, 0.002176, 3.73590970),
(70, 56464, 0.001837, 3.48123431),
(75, 61708, 0.001539, 2.88890886),
(80, 69089, 0.001221, 2.56708694),
(85, 80058, 0.000872, 2.18441653),
(90, 99667, 0.000512, 1.63023186),
(95, 141472, 0.000218, 1.06197035),
(100, 363235, 0.000033, 0.46010166),
];
const RUST_S420_OPT: &[(u8, usize, f64, f64)] = &[
(5, 13868, 0.017425, 9.54254341),
(10, 16660, 0.013046, 9.35945320),
(15, 19868, 0.009856, 7.23111439),
(20, 23168, 0.007753, 7.04672050),
(25, 26316, 0.006330, 5.87719584),
(30, 28303, 0.005587, 6.07315016),
(35, 29626, 0.005191, 6.03578424),
(40, 30872, 0.004842, 5.57302475),
(45, 32394, 0.004449, 5.08180952),
(50, 34014, 0.004138, 5.13703966),
(55, 36259, 0.003698, 5.10817671),
(60, 38536, 0.003316, 5.10311937),
(65, 41559, 0.002880, 4.87986374),
(70, 45154, 0.002481, 5.13368177),
(75, 49751, 0.002093, 4.76221800),
(80, 55528, 0.001709, 4.93946362),
(85, 64258, 0.001314, 4.74678612),
(90, 80107, 0.000869, 4.59267759),
(95, 111795, 0.000516, 4.62422276),
(100, 210147, 0.000353, 4.57556915),
];
const RUST_S422_OPT: &[(u8, usize, f64, f64)] = &[
(5, 14944, 0.018970, 11.55516815),
(10, 17822, 0.014209, 9.45995903),
(15, 21309, 0.010870, 7.92480278),
(20, 24877, 0.008354, 7.35234022),
(25, 28256, 0.006632, 6.86862469),
(30, 30726, 0.005707, 6.13680649),
(35, 31983, 0.005325, 5.85240746),
(40, 33300, 0.004988, 6.59444666),
(45, 35165, 0.004545, 6.05249786),
(50, 36810, 0.004145, 6.12209034),
(55, 38829, 0.003815, 5.18920040),
(60, 41667, 0.003368, 5.20370579),
(65, 44436, 0.002988, 4.85558844),
(70, 48452, 0.002560, 4.45603371),
(75, 52930, 0.002162, 3.74262619),
(80, 59206, 0.001775, 3.39873624),
(85, 68420, 0.001335, 2.92167902),
(90, 84123, 0.000842, 3.01503849),
(95, 116246, 0.000420, 2.81292272),
(100, 250267, 0.000159, 2.79607034),
];
const SIZE_TOLERANCE_444: f64 = 3.0;
const SIZE_TOLERANCE_SUBSAMPLED: f64 = 2.0;
const SIZE_REGRESSION_TOLERANCE: f64 = 0.5;
const DSSIM_REGRESSION_TOLERANCE: f64 = 0.003;
const BUTTERAUGLI_REGRESSION_TOLERANCE: f64 = 0.25;
fn load_test_image() -> (Vec<u8>, u32, u32) {
let png_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/images/1.png");
let png_data = fs::read(&png_path).expect("Failed to read test image");
let decoder = png::Decoder::new(&png_data[..]);
let mut reader = decoder.read_info().unwrap();
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf).unwrap();
let rgb: Vec<u8> = match info.color_type {
png::ColorType::Rgb => buf[..info.buffer_size()].to_vec(),
png::ColorType::Rgba => buf[..info.buffer_size()]
.chunks(4)
.flat_map(|c| [c[0], c[1], c[2]])
.collect(),
_ => panic!("Unsupported color type"),
};
(rgb, info.width, info.height)
}
fn rgb_to_rgba(data: &[u8]) -> Vec<RGBA8> {
data.chunks(3)
.map(|c| RGBA8::new(c[0], c[1], c[2], 255))
.collect()
}
fn compute_dssim(original: &[u8], decoded: &[u8], width: usize, height: usize) -> f64 {
let dssim = Dssim::new();
let orig_rgba = rgb_to_rgba(original);
let dec_rgba = rgb_to_rgba(decoded);
let orig = dssim
.create_image_rgba(&orig_rgba, width, height)
.expect("create orig image");
let comp = dssim
.create_image_rgba(&dec_rgba, width, height)
.expect("create comp image");
let (dssim_val, _) = dssim.compare(&orig, comp);
f64::from(dssim_val)
}
fn compute_butteraugli(original: &[u8], decoded: &[u8], width: usize, height: usize) -> f64 {
let params = ButteraugliParams::default();
butteraugli::compute_butteraugli(original, decoded, width, height, ¶ms)
.expect("butteraugli computation failed")
.score
}
fn encode_jpeg(
rgb: &[u8],
width: u32,
height: u32,
quality: f32,
subsampling: ChromaSubsampling,
optimize_huffman: bool,
) -> Vec<u8> {
let config = EncoderConfig::ycbcr(quality, subsampling)
.progressive(optimize_huffman) .optimize_huffman(optimize_huffman);
let mut enc = config
.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)
.expect("encoder setup");
enc.push_packed(rgb, enough::Unstoppable)
.expect("push data");
enc.finish().expect("Encoding failed")
}
#[test]
fn test_cpp_parity_s444_optimized() {
let (rgb, width, height) = load_test_image();
for &(quality, cpp_size) in CPP_S444_OPT {
let jpeg = encode_jpeg(
&rgb,
width,
height,
quality as f32,
ChromaSubsampling::None,
true,
);
let diff_pct = 100.0 * (jpeg.len() as f64 - cpp_size as f64) / cpp_size as f64;
assert!(
diff_pct.abs() < SIZE_TOLERANCE_444,
"Q{}: Rust={} C++={} diff={:+.2}% (limit: {}%)",
quality,
jpeg.len(),
cpp_size,
diff_pct,
SIZE_TOLERANCE_444
);
}
}
#[test]
#[ignore = "cjpegli has no fixed Huffman mode - always optimizes"]
fn test_cpp_parity_s444_fixed() {
let (rgb, width, height) = load_test_image();
for &(quality, cpp_size) in CPP_S444_FIXED {
let jpeg = encode_jpeg(
&rgb,
width,
height,
quality as f32,
ChromaSubsampling::None,
false,
);
let diff_pct = 100.0 * (jpeg.len() as f64 - cpp_size as f64) / cpp_size as f64;
assert!(
diff_pct.abs() < SIZE_TOLERANCE_444,
"Q{} fixed: Rust={} C++={} diff={:+.2}% (limit: {}%)",
quality,
jpeg.len(),
cpp_size,
diff_pct,
SIZE_TOLERANCE_444
);
}
}
#[test]
fn test_cpp_parity_s420() {
let (rgb, width, height) = load_test_image();
for &(quality, cpp_size) in CPP_S420_OPT {
let jpeg = encode_jpeg(
&rgb,
width,
height,
quality as f32,
ChromaSubsampling::Quarter,
true,
);
let diff_pct = 100.0 * (jpeg.len() as f64 - cpp_size as f64) / cpp_size as f64;
assert!(
diff_pct.abs() < SIZE_TOLERANCE_SUBSAMPLED,
"Q{} 4:2:0: Rust={} C++={} diff={:+.2}% (limit: {}%)",
quality,
jpeg.len(),
cpp_size,
diff_pct,
SIZE_TOLERANCE_SUBSAMPLED
);
}
}
#[test]
fn test_cpp_parity_s422() {
let (rgb, width, height) = load_test_image();
for &(quality, cpp_size) in CPP_S422_OPT {
let jpeg = encode_jpeg(
&rgb,
width,
height,
quality as f32,
ChromaSubsampling::HalfHorizontal,
true,
);
let diff_pct = 100.0 * (jpeg.len() as f64 - cpp_size as f64) / cpp_size as f64;
assert!(
diff_pct.abs() < SIZE_TOLERANCE_SUBSAMPLED,
"Q{} 4:2:2: Rust={} C++={} diff={:+.2}% (limit: {}%)",
quality,
jpeg.len(),
cpp_size,
diff_pct,
SIZE_TOLERANCE_SUBSAMPLED
);
}
}
#[test]
fn test_regression_s444_optimized() {
let (rgb, width, height) = load_test_image();
for &(quality, expected_size, expected_dssim, expected_bfly) in RUST_S444_OPT {
let jpeg = encode_jpeg(
&rgb,
width,
height,
quality as f32,
ChromaSubsampling::None,
true,
);
let size_diff_pct =
100.0 * (jpeg.len() as f64 - expected_size as f64) / expected_size as f64;
assert!(
size_diff_pct.abs() < SIZE_REGRESSION_TOLERANCE,
"Q{}: size regressed: got {} expected {} ({:+.2}%)",
quality,
jpeg.len(),
expected_size,
size_diff_pct
);
let decoded = Decoder::new()
.output_format(PixelFormat::Rgb)
.decode(&jpeg)
.expect("Decoding failed");
let dssim = compute_dssim(&rgb, &decoded.data, width as usize, height as usize);
assert!(
(dssim - expected_dssim).abs() < DSSIM_REGRESSION_TOLERANCE,
"Q{}: DSSIM regressed: got {:.6} expected {:.6}",
quality,
dssim,
expected_dssim
);
let bfly = compute_butteraugli(&rgb, &decoded.data, width as usize, height as usize);
let bfly_diff_pct = (bfly - expected_bfly).abs() / expected_bfly;
assert!(
bfly_diff_pct < BUTTERAUGLI_REGRESSION_TOLERANCE,
"Q{}: Butteraugli regressed: got {:.8} expected {:.8} ({:+.6}%)",
quality,
bfly,
expected_bfly,
bfly_diff_pct * 100.0
);
}
}
#[test]
fn test_regression_s444_fixed() {
let (rgb, width, height) = load_test_image();
for &(quality, expected_size, expected_dssim, expected_bfly) in RUST_S444_FIXED {
let jpeg = encode_jpeg(
&rgb,
width,
height,
quality as f32,
ChromaSubsampling::None,
false,
);
let size_diff_pct =
100.0 * (jpeg.len() as f64 - expected_size as f64) / expected_size as f64;
assert!(
size_diff_pct.abs() < SIZE_REGRESSION_TOLERANCE,
"Q{} fixed: size regressed: got {} expected {} ({:+.2}%)",
quality,
jpeg.len(),
expected_size,
size_diff_pct
);
let decoded = Decoder::new()
.output_format(PixelFormat::Rgb)
.decode(&jpeg)
.expect("Decoding failed");
let dssim = compute_dssim(&rgb, &decoded.data, width as usize, height as usize);
assert!(
(dssim - expected_dssim).abs() < DSSIM_REGRESSION_TOLERANCE,
"Q{} fixed: DSSIM regressed: got {:.6} expected {:.6}",
quality,
dssim,
expected_dssim
);
let bfly = compute_butteraugli(&rgb, &decoded.data, width as usize, height as usize);
let bfly_diff_pct = (bfly - expected_bfly).abs() / expected_bfly;
assert!(
bfly_diff_pct < BUTTERAUGLI_REGRESSION_TOLERANCE,
"Q{} fixed: Butteraugli regressed: got {:.8} expected {:.8} ({:+.6}%)",
quality,
bfly,
expected_bfly,
bfly_diff_pct * 100.0
);
}
}
#[test]
fn test_regression_s420() {
let (rgb, width, height) = load_test_image();
for &(quality, expected_size, expected_dssim, expected_bfly) in RUST_S420_OPT {
let jpeg = encode_jpeg(
&rgb,
width,
height,
quality as f32,
ChromaSubsampling::Quarter,
true,
);
let size_diff_pct =
100.0 * (jpeg.len() as f64 - expected_size as f64) / expected_size as f64;
assert!(
size_diff_pct.abs() < SIZE_REGRESSION_TOLERANCE,
"Q{} 4:2:0: size regressed: got {} expected {} ({:+.2}%)",
quality,
jpeg.len(),
expected_size,
size_diff_pct
);
let decoded = Decoder::new()
.output_format(PixelFormat::Rgb)
.decode(&jpeg)
.expect("Decoding failed");
let dssim = compute_dssim(&rgb, &decoded.data, width as usize, height as usize);
assert!(
(dssim - expected_dssim).abs() < DSSIM_REGRESSION_TOLERANCE,
"Q{} 4:2:0: DSSIM regressed: got {:.6} expected {:.6}",
quality,
dssim,
expected_dssim
);
let bfly = compute_butteraugli(&rgb, &decoded.data, width as usize, height as usize);
let bfly_diff_pct = (bfly - expected_bfly).abs() / expected_bfly;
assert!(
bfly_diff_pct < BUTTERAUGLI_REGRESSION_TOLERANCE,
"Q{} 4:2:0: Butteraugli regressed: got {:.8} expected {:.8} ({:+.6}%)",
quality,
bfly,
expected_bfly,
bfly_diff_pct * 100.0
);
}
}
#[test]
fn test_regression_s422() {
let (rgb, width, height) = load_test_image();
for &(quality, expected_size, expected_dssim, expected_bfly) in RUST_S422_OPT {
let jpeg = encode_jpeg(
&rgb,
width,
height,
quality as f32,
ChromaSubsampling::HalfHorizontal,
true,
);
let size_diff_pct =
100.0 * (jpeg.len() as f64 - expected_size as f64) / expected_size as f64;
assert!(
size_diff_pct.abs() < SIZE_REGRESSION_TOLERANCE,
"Q{} 4:2:2: size regressed: got {} expected {} ({:+.2}%)",
quality,
jpeg.len(),
expected_size,
size_diff_pct
);
let decoded = Decoder::new()
.output_format(PixelFormat::Rgb)
.decode(&jpeg)
.expect("Decoding failed");
let dssim = compute_dssim(&rgb, &decoded.data, width as usize, height as usize);
assert!(
(dssim - expected_dssim).abs() < DSSIM_REGRESSION_TOLERANCE,
"Q{} 4:2:2: DSSIM regressed: got {:.6} expected {:.6}",
quality,
dssim,
expected_dssim
);
let bfly = compute_butteraugli(&rgb, &decoded.data, width as usize, height as usize);
let bfly_diff_pct = (bfly - expected_bfly).abs() / expected_bfly;
assert!(
bfly_diff_pct < BUTTERAUGLI_REGRESSION_TOLERANCE,
"Q{} 4:2:2: Butteraugli regressed: got {:.8} expected {:.8} ({:+.6}%)",
quality,
bfly,
expected_bfly,
bfly_diff_pct * 100.0
);
}
}
#[test]
#[ignore = "utility for updating reference values"]
fn print_current_values() {
let (rgb, width, height) = load_test_image();
println!("Image: {}x{}", width, height);
let configs = [
(ChromaSubsampling::None, true, "S444_OPT"),
(ChromaSubsampling::None, false, "S444_FIXED"),
(ChromaSubsampling::Quarter, true, "S420_OPT"),
(ChromaSubsampling::HalfHorizontal, true, "S422_OPT"),
];
let dssim = Dssim::new();
let rgba_orig = rgb_to_rgba(&rgb);
let orig_img = dssim
.create_image_rgba(&rgba_orig, width as usize, height as usize)
.unwrap();
let bfly_params = ButteraugliParams::default();
for (subsampling, optimize, name) in configs {
println!("\n/// RUST_{} - (quality, size, dssim, butteraugli)", name);
println!("const RUST_{}: &[(u8, usize, f64, f64)] = &[", name);
for q in (1..=20).map(|i| i * 5) {
let jpeg = encode_jpeg(&rgb, width, height, q as f32, subsampling, optimize);
let decoded = Decoder::new()
.output_format(PixelFormat::Rgb)
.decode(&jpeg)
.expect("Decoding failed");
let rgba_dec = rgb_to_rgba(&decoded.data);
let dec_img = dssim
.create_image_rgba(&rgba_dec, width as usize, height as usize)
.unwrap();
let (dssim_val, _) = dssim.compare(&orig_img, dec_img);
let bfly = butteraugli::compute_butteraugli(
&rgb,
&decoded.data,
width as usize,
height as usize,
&bfly_params,
)
.expect("butteraugli")
.score;
println!(
" ({}, {}, {:.6}, {:.8}),",
q,
jpeg.len(),
f64::from(dssim_val),
bfly
);
}
println!("];");
}
}
#[test]
#[ignore = "utility for viewing summary"]
fn print_summary() {
let (rgb, width, height) = load_test_image();
println!("=== C++ vs Rust Parity Summary ===\n");
println!("Test image: {}x{}\n", width, height);
println!("4:4:4 Optimized Huffman:");
println!("{:>5} {:>10} {:>10} {:>10}", "Q", "C++", "Rust", "Diff");
for &(q, cpp_size) in CPP_S444_OPT {
let jpeg = encode_jpeg(&rgb, width, height, q as f32, ChromaSubsampling::None, true);
let diff_pct = 100.0 * (jpeg.len() as f64 - cpp_size as f64) / cpp_size as f64;
println!(
"{:>5} {:>10} {:>10} {:>+9.2}%",
q,
cpp_size,
jpeg.len(),
diff_pct
);
}
println!("\n4:2:0 Optimized Huffman:");
println!("{:>5} {:>10} {:>10} {:>10}", "Q", "C++", "Rust", "Diff");
for &(q, cpp_size) in CPP_S420_OPT {
let jpeg = encode_jpeg(
&rgb,
width,
height,
q as f32,
ChromaSubsampling::Quarter,
true,
);
let diff_pct = 100.0 * (jpeg.len() as f64 - cpp_size as f64) / cpp_size as f64;
println!(
"{:>5} {:>10} {:>10} {:>+9.2}%",
q,
cpp_size,
jpeg.len(),
diff_pct
);
}
}