use jpegli::encoder::ChromaSubsampling;
use jpegli::encoder::{EncoderConfig, PixelLayout};
use std::fs;
use std::process::Command;
fn create_gradient_image(width: usize, height: usize) -> Vec<u8> {
let mut rgb = vec![0u8; width * height * 3];
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) * 3;
rgb[idx] = ((x * 255) / width.max(1)) as u8;
rgb[idx + 1] = ((y * 255) / height.max(1)) as u8;
rgb[idx + 2] = 128;
}
}
rgb
}
fn write_ppm(path: &str, rgb: &[u8], width: usize, height: usize) -> std::io::Result<()> {
use std::io::Write;
let mut file = fs::File::create(path)?;
writeln!(file, "P6")?;
writeln!(file, "{} {}", width, height)?;
writeln!(file, "255")?;
file.write_all(rgb)?;
Ok(())
}
fn encode_cpp(ppm_path: &str, quality: u32) -> Option<Vec<u8>> {
let cjpegli_path = jpegli::test_utils::find_cjpegli()?;
let output_path = format!("/tmp/cpp_test_q{}.jpg", quality);
let output = Command::new(cjpegli_path)
.args([
"--chroma_subsampling=444",
"-p",
"0",
ppm_path,
&output_path,
"-q",
&quality.to_string(),
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
fs::read(&output_path).ok()
}
fn encode_rust(rgb: &[u8], width: u32, height: u32, quality: f32) -> Vec<u8> {
let config = EncoderConfig::ycbcr(quality, ChromaSubsampling::Quarter);
let mut enc = config
.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)
.expect("create encoder");
enc.push_packed(rgb, enough::Unstoppable)
.expect("push data");
enc.finish().expect("finish")
}
#[test]
#[ignore = "requires C++ cjpegli build"]
fn test_filesize_comparison_synthetic() {
let test_cases = [
(8, 8, "8x8"),
(16, 16, "16x16"),
(64, 64, "64x64"),
(256, 256, "256x256"),
];
for (width, height, name) in test_cases {
let rgb = create_gradient_image(width, height);
let ppm_path = format!("/tmp/test_{}.ppm", name);
write_ppm(&ppm_path, &rgb, width, height).unwrap();
for quality in [90, 80, 70] {
let cpp_jpeg = match encode_cpp(&ppm_path, quality) {
Some(j) => j,
None => {
println!("Skipping {} Q{}: C++ not available", name, quality);
continue;
}
};
let rust_jpeg = encode_rust(&rgb, width as u32, height as u32, quality as f32);
let cpp_size = cpp_jpeg.len();
let rust_size = rust_jpeg.len();
let diff_pct = 100.0 * (rust_size as f64 - cpp_size as f64) / cpp_size as f64;
let diff_bytes = (rust_size as i64 - cpp_size as i64).abs();
println!(
"{} Q{}: C++={} Rust={} ({:+.1}%, {:+} bytes)",
name,
quality,
cpp_size,
rust_size,
diff_pct,
rust_size as i64 - cpp_size as i64
);
let threshold_pct = if cpp_size < 1024 { 20.0 } else { 10.0 };
let threshold_bytes = 100;
let pass = if cpp_size < 1024 {
diff_bytes <= threshold_bytes || diff_pct.abs() < threshold_pct
} else {
diff_pct.abs() < threshold_pct
};
assert!(
pass,
"{} Q{}: file size differs by {:.1}% ({} bytes)",
name, quality, diff_pct, diff_bytes
);
}
}
}
#[test]
#[ignore = "requires C++ cjpegli build and test image"]
fn test_filesize_comparison_photo() {
let png_path = jpegli::test_utils::get_testdata_dir().join("jxl/flower/flower_small.rgb.png");
if !png_path.exists() {
println!("Skipping: test image not found. Set JPEGLI_TESTDATA env var.");
return;
}
let decoder = png::Decoder::new(fs::File::open(png_path).unwrap());
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 bytes = &buf[..info.buffer_size()];
let rgb: Vec<u8> = match info.color_type {
png::ColorType::Rgb => bytes.to_vec(),
png::ColorType::Rgba => bytes.chunks(4).flat_map(|c| [c[0], c[1], c[2]]).collect(),
_ => panic!("Unsupported color type"),
};
let ppm_path = "/tmp/test_flower.ppm";
write_ppm(ppm_path, &rgb, info.width as usize, info.height as usize).unwrap();
println!("Image: {}x{}", info.width, info.height);
for quality in [90, 80, 70, 60] {
let cpp_jpeg = match encode_cpp(ppm_path, quality) {
Some(j) => j,
None => {
println!("Skipping Q{}: C++ not available", quality);
continue;
}
};
let rust_jpeg = encode_rust(&rgb, info.width, info.height, quality as f32);
let cpp_size = cpp_jpeg.len();
let rust_size = rust_jpeg.len();
let diff_pct = 100.0 * (rust_size as f64 - cpp_size as f64) / cpp_size as f64;
println!(
"Q{}: C++={} Rust={} ({:+.1}%)",
quality, cpp_size, rust_size, diff_pct
);
}
}