#![cfg(feature = "jpeg-reencoding")]
use jxl_encoder::jpeg::encode_jbrd;
use jxl_encoder::jpeg::{encode_jpeg_to_jxl, encode_jpeg_to_jxl_container, read_jpeg};
fn decode_jxl_rs(data: &[u8]) -> (usize, usize, Vec<f32>) {
use jxl::api::{
JxlColorType, JxlDataFormat, JxlDecoder, JxlDecoderOptions, JxlOutputBuffer,
JxlPixelFormat, ProcessingResult, states,
};
use jxl::image::{Image, Rect};
let mut input = data;
let options = JxlDecoderOptions::default();
let mut decoder = JxlDecoder::<states::Initialized>::new(options);
let mut decoder = loop {
match decoder.process(&mut input) {
Ok(ProcessingResult::Complete { result }) => break result,
Ok(ProcessingResult::NeedsMoreInput { fallback, .. }) => {
if input.is_empty() {
panic!("jxl-rs: unexpected end of input during header");
}
decoder = fallback;
}
Err(e) => panic!("jxl-rs header decode error: {:?}", e),
}
};
let basic_info = decoder.basic_info().clone();
let (width, height) = basic_info.size;
let channels = 3;
let format = JxlPixelFormat {
color_type: JxlColorType::Rgb,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![],
};
decoder.set_pixel_format(format);
let mut decoder = loop {
match decoder.process(&mut input) {
Ok(ProcessingResult::Complete { result }) => break result,
Ok(ProcessingResult::NeedsMoreInput { fallback, .. }) => {
if input.is_empty() {
panic!("jxl-rs: unexpected end of input before frame");
}
decoder = fallback;
}
Err(e) => panic!("jxl-rs frame info error: {:?}", e),
}
};
let mut output_image =
Image::<f32>::new((width * channels, height)).expect("alloc output buffer");
let mut buffers = vec![JxlOutputBuffer::from_image_rect_mut(
output_image
.get_rect_mut(Rect {
origin: (0, 0),
size: (width * channels, height),
})
.into_raw(),
)];
loop {
match decoder.process(&mut input, &mut buffers) {
Ok(ProcessingResult::Complete { .. }) => break,
Ok(ProcessingResult::NeedsMoreInput { fallback, .. }) => {
if input.is_empty() {
panic!("jxl-rs: unexpected end of input during decode");
}
decoder = fallback;
}
Err(e) => panic!("jxl-rs decode error: {:?}", e),
}
}
let mut pixels = Vec::with_capacity(width * height * channels);
for y in 0..height {
pixels.extend_from_slice(output_image.row(y));
}
(width, height, pixels)
}
fn decode_jxl_rs_gray(data: &[u8]) -> (usize, usize, Vec<f32>) {
use jxl::api::{
JxlColorType, JxlDataFormat, JxlDecoder, JxlDecoderOptions, JxlOutputBuffer,
JxlPixelFormat, ProcessingResult, states,
};
use jxl::image::{Image, Rect};
let mut input = data;
let options = JxlDecoderOptions::default();
let mut decoder = JxlDecoder::<states::Initialized>::new(options);
let mut decoder = loop {
match decoder.process(&mut input) {
Ok(ProcessingResult::Complete { result }) => break result,
Ok(ProcessingResult::NeedsMoreInput { fallback, .. }) => {
if input.is_empty() {
panic!("jxl-rs: unexpected end of input during header");
}
decoder = fallback;
}
Err(e) => panic!("jxl-rs header decode error: {:?}", e),
}
};
let basic_info = decoder.basic_info().clone();
let (width, height) = basic_info.size;
let channels = 1;
let format = JxlPixelFormat {
color_type: JxlColorType::Grayscale,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: vec![],
};
decoder.set_pixel_format(format);
let mut decoder = loop {
match decoder.process(&mut input) {
Ok(ProcessingResult::Complete { result }) => break result,
Ok(ProcessingResult::NeedsMoreInput { fallback, .. }) => {
if input.is_empty() {
panic!("jxl-rs: unexpected end of input before frame");
}
decoder = fallback;
}
Err(e) => panic!("jxl-rs frame info error: {:?}", e),
}
};
let mut output_image =
Image::<f32>::new((width * channels, height)).expect("alloc output buffer");
let mut buffers = vec![JxlOutputBuffer::from_image_rect_mut(
output_image
.get_rect_mut(Rect {
origin: (0, 0),
size: (width * channels, height),
})
.into_raw(),
)];
loop {
match decoder.process(&mut input, &mut buffers) {
Ok(ProcessingResult::Complete { .. }) => break,
Ok(ProcessingResult::NeedsMoreInput { fallback, .. }) => {
if input.is_empty() {
panic!("jxl-rs: unexpected end of input during decode");
}
decoder = fallback;
}
Err(e) => panic!("jxl-rs decode error: {:?}", e),
}
}
let mut pixels = Vec::with_capacity(width * height * channels);
for y in 0..height {
pixels.extend_from_slice(output_image.row(y));
}
(width, height, pixels)
}
fn verify_jxl_rs_decodes(jpeg_path: &str, label: &str) {
let jpeg_data = std::fs::read(jpeg_path)
.unwrap_or_else(|e| panic!("{label}: failed to read {jpeg_path}: {e}"));
let jpeg = read_jpeg(&jpeg_data).unwrap_or_else(|e| panic!("{label}: failed to parse: {e}"));
let jxl_bytes = encode_jpeg_to_jxl(&jpeg).unwrap_or_else(|e| panic!("{label}: encode: {e}"));
let is_gray = jpeg.components.len() == 1;
let w = jpeg.width as usize;
let h = jpeg.height as usize;
if is_gray {
let (dw, dh, pixels) = decode_jxl_rs_gray(&jxl_bytes);
assert_eq!(dw, w, "{label}: width mismatch");
assert_eq!(dh, h, "{label}: height mismatch");
assert_eq!(pixels.len(), w * h, "{label}: pixel count mismatch");
for (i, &p) in pixels.iter().enumerate() {
assert!(
(-0.5..=1.5).contains(&p),
"{label}: pixel {i} out of range: {p}"
);
}
eprintln!("{label}: jxl-rs decoded {dw}x{dh} grayscale OK");
} else {
let (dw, dh, pixels) = decode_jxl_rs(&jxl_bytes);
assert_eq!(dw, w, "{label}: width mismatch");
assert_eq!(dh, h, "{label}: height mismatch");
assert_eq!(pixels.len(), w * h * 3, "{label}: pixel count mismatch");
for (i, &p) in pixels.iter().enumerate() {
assert!(
(-0.5..=1.5).contains(&p),
"{label}: pixel {i} out of range: {p}"
);
}
eprintln!("{label}: jxl-rs decoded {dw}x{dh} RGB OK");
}
}
#[test]
fn test_encode_small_jpeg() {
let path = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64_444.jpg");
let data = std::fs::read(&path).expect("failed to read test JPEG");
let jpeg = read_jpeg(&data).expect("failed to parse JPEG");
let jxl_bytes = encode_jpeg_to_jxl(&jpeg).expect("failed to encode JPEG to JXL");
eprintln!(
"Encoded {}x{} JPEG ({} components) to {} bytes JXL",
jpeg.width,
jpeg.height,
jpeg.components.len(),
jxl_bytes.len()
);
assert_eq!(jxl_bytes[0], 0xFF, "bad signature byte 0");
assert_eq!(jxl_bytes[1], 0x0A, "bad signature byte 1");
let out_path = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64.jxl");
std::fs::write(&out_path, &jxl_bytes).expect("failed to write JXL");
eprintln!("Saved to {}", out_path.display());
}
#[test]
fn test_decode_small_jpeg_oxide() {
let path = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64_444.jpg");
let path_str = path.to_string_lossy().into_owned();
let data = std::fs::read(&path).expect("failed to read test JPEG");
let jpeg = read_jpeg(&data).expect("failed to parse JPEG");
let jxl_bytes = encode_jpeg_to_jxl(&jpeg).expect("failed to encode JPEG to JXL");
eprintln!(
"Encoded {}x{} JPEG ({} components) to {} bytes JXL",
jpeg.width,
jpeg.height,
jpeg.components.len(),
jxl_bytes.len()
);
let out_path = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64.jxl");
std::fs::write(&out_path, &jxl_bytes).expect("failed to write JXL");
let reader = std::io::Cursor::new(&jxl_bytes);
let image = jxl_oxide::JxlImage::builder()
.read(reader)
.unwrap_or_else(|e| {
panic!("jxl-oxide failed to parse: {e}");
});
let render = image.render_frame(0).unwrap_or_else(|e| {
panic!("jxl-oxide failed to render: {e}");
});
let fb = render.image_all_channels();
let pixels = fb.buf();
let w = jpeg.width as usize;
let h = jpeg.height as usize;
let num_pixels = w * h;
eprintln!("jxl-oxide decoded: {} f32 values", pixels.len());
assert!(pixels.len() >= num_pixels * 3);
let _jpeg_data = std::fs::read(&path).unwrap();
let djpeg = std::process::Command::new("djpeg")
.args(["-pnm", &path_str])
.output()
.expect("failed to run djpeg");
assert!(djpeg.status.success(), "djpeg failed");
let ppm = &djpeg.stdout;
let header_end = {
let mut newlines = 0;
let mut pos = 0;
for (i, &b) in ppm.iter().enumerate() {
if b == b'\n' {
newlines += 1;
if newlines == 3 {
pos = i + 1;
break;
}
}
}
pos
};
let jpeg_rgb = &ppm[header_end..];
let mut sum_sq_err = 0.0f64;
let mut max_diff = 0i32;
let mut diff_histogram = [0u32; 20]; for i in 0..num_pixels {
for ch in 0..3 {
let jxl_val = (pixels[i * 3 + ch] * 255.0).round().clamp(0.0, 255.0) as i32;
let jpeg_val = jpeg_rgb[i * 3 + ch] as i32;
let d = (jxl_val - jpeg_val).abs();
sum_sq_err += (d * d) as f64;
max_diff = max_diff.max(d);
diff_histogram[d.min(19) as usize] += 1;
}
}
let rmse = (sum_sq_err / (num_pixels as f64 * 3.0)).sqrt();
eprintln!("64x64: RMSE={rmse:.4}, max_diff={max_diff}");
eprintln!("Diff histogram (abs diff → count):");
for (d, &count) in diff_histogram.iter().enumerate() {
if count > 0 {
eprintln!(
" diff={d}: {count} values ({:.2}%)",
count as f64 / (num_pixels * 3) as f64 * 100.0
);
}
}
}
#[test]
fn test_decode_landscape_jpeg_oxide() {
let path = &format!(
"{}/imageflow/test_inputs/orientation/Landscape_1.jpg",
jxl_encoder::test_helpers::corpus_dir().display()
);
let data = std::fs::read(path).expect("failed to read test JPEG");
let jpeg = read_jpeg(&data).expect("failed to parse JPEG");
let jxl_bytes = encode_jpeg_to_jxl(&jpeg).expect("failed to encode JPEG to JXL");
eprintln!(
"Encoded {}x{} JPEG to {} bytes JXL",
jpeg.width,
jpeg.height,
jxl_bytes.len()
);
let out_path = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "landscape1.jxl");
std::fs::write(&out_path, &jxl_bytes).expect("failed to write JXL");
let reader = std::io::Cursor::new(&jxl_bytes);
let image = jxl_oxide::JxlImage::builder()
.read(reader)
.unwrap_or_else(|e| {
panic!("jxl-oxide failed to parse: {e}");
});
let render = image.render_frame(0).unwrap_or_else(|e| {
panic!("jxl-oxide failed to render: {e}");
});
let fb = render.image_all_channels();
let pixels = fb.buf();
let w = jpeg.width as usize;
let h = jpeg.height as usize;
let num_pixels = w * h;
eprintln!("jxl-oxide decoded: {} f32 values", pixels.len());
assert!(pixels.len() >= num_pixels * 3);
let djpeg = std::process::Command::new("djpeg")
.args(["-pnm", path])
.output()
.expect("failed to run djpeg");
assert!(djpeg.status.success(), "djpeg failed");
let ppm = &djpeg.stdout;
let header_end = {
let mut newlines = 0;
let mut pos = 0;
for (i, &b) in ppm.iter().enumerate() {
if b == b'\n' {
newlines += 1;
if newlines == 3 {
pos = i + 1;
break;
}
}
}
pos
};
let jpeg_rgb = &ppm[header_end..];
let mut sum_sq_err = 0.0f64;
let mut max_diff = 0i32;
let mut diff_histogram = [0u32; 20];
let mut worst_pixels: Vec<(usize, usize, i32, i32, i32)> = Vec::new(); for i in 0..num_pixels {
let px = i % w;
let py = i / w;
let mut this_max = 0i32;
let mut diffs = [0i32; 3];
for ch in 0..3 {
let jxl_val = (pixels[i * 3 + ch] * 255.0).round().clamp(0.0, 255.0) as i32;
let jpeg_val = jpeg_rgb[i * 3 + ch] as i32;
let d = jxl_val - jpeg_val;
diffs[ch] = d;
sum_sq_err += (d * d) as f64;
let ad = d.abs();
max_diff = max_diff.max(ad);
this_max = this_max.max(ad);
diff_histogram[ad.min(19) as usize] += 1;
}
if this_max >= 5 {
worst_pixels.push((px, py, diffs[0], diffs[1], diffs[2]));
}
}
let rmse = (sum_sq_err / (num_pixels as f64 * 3.0)).sqrt();
eprintln!("Multi-group {w}x{h}: RMSE={rmse:.4}, max_diff={max_diff}");
eprintln!("Diff histogram (abs diff → count):");
for (d, &count) in diff_histogram.iter().enumerate() {
if count > 0 {
eprintln!(
" diff={d}: {count} values ({:.2}%)",
count as f64 / (num_pixels * 3) as f64 * 100.0
);
}
}
worst_pixels.sort_by_key(|&(_, _, dr, dg, db)| -(dr.abs().max(dg.abs()).max(db.abs())));
eprintln!(
"\nPixels with max_abs_diff >= 5 ({} total):",
worst_pixels.len()
);
for &(px, py, dr, dg, db) in worst_pixels.iter().take(30) {
let block_x = px / 8;
let block_y = py / 8;
let dc_group_x = block_x / 32;
let dc_group_y = block_y / 32;
let in_block_x = px % 8;
let in_block_y = py % 8;
eprintln!(
" ({px:3},{py:3}) blk=({block_x},{block_y}) dcg=({dc_group_x},{dc_group_y}) ib=({in_block_x},{in_block_y}) diff=({dr:+},{dg:+},{db:+})"
);
}
assert!(rmse < 2.0, "RMSE too high: {rmse}");
assert!(max_diff <= 12, "Max pixel diff too high: {max_diff}");
}
#[test]
fn test_jbrd_roundtrip_small() {
let path = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64_444.jpg");
let jpeg_data = std::fs::read(&path).expect("failed to read test JPEG");
let jpeg = read_jpeg(&jpeg_data).expect("failed to parse JPEG");
let jxl_bytes =
encode_jpeg_to_jxl_container(&jpeg).expect("failed to encode JPEG to JXL container");
eprintln!(
"Encoded {}x{} JPEG ({} bytes) to {} bytes JXL container (with JBRD)",
jpeg.width,
jpeg.height,
jpeg_data.len(),
jxl_bytes.len()
);
assert_eq!(
&jxl_bytes[..4],
&[0x00, 0x00, 0x00, 0x0C],
"bad container signature size"
);
assert_eq!(&jxl_bytes[4..8], b"JXL ", "bad container signature type");
let out_dir = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "");
let out_dir = out_dir.to_string_lossy();
let jxl_path = format!("{out_dir}/test64_jbrd.jxl");
std::fs::write(&jxl_path, &jxl_bytes).expect("failed to write JXL");
eprintln!("Saved to {jxl_path}");
let reconstructed_path = format!("{out_dir}/test64_reconstructed.jpg");
let djxl = std::process::Command::new(jxl_encoder::test_helpers::djxl_path())
.args([&jxl_path, &reconstructed_path, "--reconstruct_jpeg"])
.output()
.expect("failed to run djxl");
let stderr = String::from_utf8_lossy(&djxl.stderr);
eprintln!("djxl stderr: {stderr}");
if djxl.status.success() {
let reconstructed =
std::fs::read(&reconstructed_path).expect("failed to read reconstructed");
if jpeg_data == reconstructed {
eprintln!(
"BYTE-EXACT JPEG RECONSTRUCTION: PASS ({} bytes)",
jpeg_data.len()
);
} else {
eprintln!(
"Reconstructed JPEG differs: original {} bytes, reconstructed {} bytes",
jpeg_data.len(),
reconstructed.len()
);
let min_len = jpeg_data.len().min(reconstructed.len());
for i in 0..min_len {
if jpeg_data[i] != reconstructed[i] {
eprintln!(
"First diff at byte {i} (0x{i:x}): original=0x{:02x}, reconstructed=0x{:02x}",
jpeg_data[i], reconstructed[i]
);
break;
}
}
panic!("JPEG reconstruction not byte-exact!");
}
} else {
let exit_code = djxl.status.code().unwrap_or(-1);
eprintln!("djxl --reconstruct_jpeg failed (exit code {exit_code})");
eprintln!("This is expected initially — JBRD serialization may need debugging.");
}
}
#[test]
fn test_jbrd_roundtrip_landscape() {
let path = &format!(
"{}/imageflow/test_inputs/orientation/Landscape_1.jpg",
jxl_encoder::test_helpers::corpus_dir().display()
);
let jpeg_data = std::fs::read(path).expect("failed to read test JPEG");
let jpeg = read_jpeg(&jpeg_data).expect("failed to parse JPEG");
let jxl_bytes =
encode_jpeg_to_jxl_container(&jpeg).expect("failed to encode JPEG to JXL container");
eprintln!(
"Encoded {}x{} JPEG ({} bytes) to {} bytes JXL container (with JBRD)",
jpeg.width,
jpeg.height,
jpeg_data.len(),
jxl_bytes.len()
);
let out_dir = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "");
let out_dir = out_dir.to_string_lossy();
let jxl_path = format!("{out_dir}/landscape1_jbrd.jxl");
std::fs::write(&jxl_path, &jxl_bytes).expect("failed to write JXL");
let reconstructed_path = format!("{out_dir}/landscape1_reconstructed.jpg");
let djxl = std::process::Command::new(jxl_encoder::test_helpers::djxl_path())
.args([&jxl_path, &reconstructed_path, "--reconstruct_jpeg"])
.output()
.expect("failed to run djxl");
let stderr = String::from_utf8_lossy(&djxl.stderr);
eprintln!("djxl stderr: {stderr}");
if djxl.status.success() {
let reconstructed =
std::fs::read(&reconstructed_path).expect("failed to read reconstructed");
if jpeg_data == reconstructed {
eprintln!(
"BYTE-EXACT JPEG RECONSTRUCTION: PASS ({} bytes)",
jpeg_data.len()
);
} else {
eprintln!(
"Reconstructed differs: original {} bytes, reconstructed {} bytes",
jpeg_data.len(),
reconstructed.len()
);
let min_len = jpeg_data.len().min(reconstructed.len());
for i in 0..min_len {
if jpeg_data[i] != reconstructed[i] {
eprintln!(
"First diff at byte {i} (0x{i:x}): original=0x{:02x}, reconstructed=0x{:02x}",
jpeg_data[i], reconstructed[i]
);
break;
}
}
panic!("JPEG reconstruction not byte-exact!");
}
} else {
let exit_code = djxl.status.code().unwrap_or(-1);
eprintln!("djxl --reconstruct_jpeg failed (exit code {exit_code})");
}
}
#[test]
#[ignore = "VarDCT codestream issue for roof_test (not JBRD)"]
fn test_jbrd_roundtrip_large_photos() {
let test_images = [
&format!(
"{}/imageflow/test_inputs/roof_test_800x600.jpg",
jxl_encoder::test_helpers::corpus_dir().display()
), ];
for path in test_images {
let basename = std::path::Path::new(path)
.file_name()
.unwrap()
.to_string_lossy();
let jpeg_data =
std::fs::read(path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"));
let jpeg =
read_jpeg(&jpeg_data).unwrap_or_else(|e| panic!("failed to parse {basename}: {e}"));
let jxl_bytes = encode_jpeg_to_jxl_container(&jpeg)
.unwrap_or_else(|e| panic!("failed to encode {basename}: {e}"));
eprintln!(
"{basename}: {}x{} JPEG ({} bytes) -> {} bytes JXL ({:.1}% of original)",
jpeg.width,
jpeg.height,
jpeg_data.len(),
jxl_bytes.len(),
jxl_bytes.len() as f64 / jpeg_data.len() as f64 * 100.0
);
let out_dir = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "");
let out_dir = out_dir.to_string_lossy();
let stem = basename.trim_end_matches(".jpg").trim_end_matches(".jpeg");
let jxl_path = format!("{out_dir}/{stem}_jbrd.jxl");
std::fs::write(&jxl_path, &jxl_bytes).unwrap();
let reconstructed_path = format!("{out_dir}/{stem}_reconstructed.jpg");
let djxl = std::process::Command::new(jxl_encoder::test_helpers::djxl_path())
.args([&jxl_path, &reconstructed_path, "--reconstruct_jpeg"])
.output()
.expect("failed to run djxl");
let stderr = String::from_utf8_lossy(&djxl.stderr);
assert!(
djxl.status.success(),
"{basename}: djxl --reconstruct_jpeg failed: {stderr}"
);
let reconstructed = std::fs::read(&reconstructed_path).unwrap();
assert_eq!(
jpeg_data,
reconstructed,
"{basename}: JPEG reconstruction not byte-exact (orig={}, recon={})",
jpeg_data.len(),
reconstructed.len()
);
eprintln!("{basename}: BYTE-EXACT RECONSTRUCTION OK");
}
}
#[test]
fn test_jbrd_parse_oxide() {
let path = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64_444.jpg");
let jpeg_data = std::fs::read(&path).expect("failed to read test JPEG");
let jpeg = read_jpeg(&jpeg_data).expect("failed to parse JPEG");
let jbrd_bytes = encode_jbrd(&jpeg).expect("failed to encode JBRD");
eprintln!("JBRD box: {} bytes", jbrd_bytes.len());
match jxl_jbr::JpegBitstreamData::try_parse(&jbrd_bytes) {
Ok(Some(mut jbrd)) => {
eprintln!("jxl-jbr: JBRD header parsed successfully");
let header = jbrd.header();
eprintln!(" expected_icc_len: {}", header.expected_icc_len());
eprintln!(" expected_exif_len: {}", header.expected_exif_len());
eprintln!(" expected_xmp_len: {}", header.expected_xmp_len());
match jbrd.finalize() {
Ok(()) => eprintln!("jxl-jbr: Data stream finalized OK (length match)"),
Err(e) => {
eprintln!("jxl-jbr: Data stream finalize error: {e:?}");
panic!("JBRD data stream length mismatch: {e:?}");
}
}
}
Ok(None) => {
eprintln!("jxl-jbr: JBRD parse returned None (insufficient data?)");
panic!("JBRD parse returned None");
}
Err(e) => {
eprintln!("jxl-jbr: JBRD parse error: {e:?}");
let dump_len = jbrd_bytes.len().min(64);
eprintln!("JBRD hex dump (first {dump_len} bytes):");
for (i, chunk) in jbrd_bytes[..dump_len].chunks(16).enumerate() {
let hex: Vec<String> = chunk.iter().map(|b| format!("{b:02x}")).collect();
eprintln!(" {:04x}: {}", i * 16, hex.join(" "));
}
panic!("JBRD parse failed: {e:?}");
}
}
}
fn djxl_bin() -> String {
jxl_encoder::test_helpers::djxl_path()
}
fn out_dir() -> String {
jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "")
.to_string_lossy()
.into_owned()
}
fn roundtrip_jpeg_byteexact(jpeg_path: &str, label: &str) {
let jpeg_data = std::fs::read(jpeg_path)
.unwrap_or_else(|e| panic!("{label}: failed to read {jpeg_path}: {e}"));
let jpeg = read_jpeg(&jpeg_data).unwrap_or_else(|e| panic!("{label}: failed to parse: {e}"));
for (i, comp) in jpeg.components.iter().enumerate() {
eprintln!(
" Component {i}: id={}, h_samp={}, v_samp={}, {}x{} blocks",
comp.id,
comp.h_samp_factor,
comp.v_samp_factor,
comp.width_in_blocks,
comp.height_in_blocks,
);
}
let jxl_bytes = encode_jpeg_to_jxl_container(&jpeg)
.unwrap_or_else(|e| panic!("{label}: failed to encode: {e}"));
let compression = jxl_bytes.len() as f64 / jpeg_data.len() as f64 * 100.0;
eprintln!(
"{label}: {}x{} JPEG ({} bytes) -> {} bytes JXL ({compression:.1}%)",
jpeg.width,
jpeg.height,
jpeg_data.len(),
jxl_bytes.len(),
);
let out = out_dir();
let jxl_path = format!("{out}/{label}.jxl");
std::fs::write(&jxl_path, &jxl_bytes).unwrap();
let reconstructed_path = format!("{out}/{label}_reconstructed.jpg");
let djxl = std::process::Command::new(djxl_bin())
.args([&jxl_path, &reconstructed_path, "--reconstruct_jpeg"])
.output()
.expect("failed to run djxl");
let stderr = String::from_utf8_lossy(&djxl.stderr);
assert!(
djxl.status.success(),
"{label}: djxl --reconstruct_jpeg failed (exit {}): {stderr}",
djxl.status.code().unwrap_or(-1),
);
let reconstructed = std::fs::read(&reconstructed_path).unwrap();
if jpeg_data == reconstructed {
eprintln!(
"{label}: BYTE-EXACT RECONSTRUCTION OK ({} bytes)",
jpeg_data.len()
);
} else {
let min_len = jpeg_data.len().min(reconstructed.len());
for i in 0..min_len {
if jpeg_data[i] != reconstructed[i] {
eprintln!(
"{label}: First diff at byte {i} (0x{i:x}): orig=0x{:02x}, recon=0x{:02x}",
jpeg_data[i], reconstructed[i],
);
break;
}
}
panic!(
"{label}: JPEG reconstruction not byte-exact (orig={}, recon={})",
jpeg_data.len(),
reconstructed.len(),
);
}
}
#[test]
fn test_subsamp_444_64x64() {
let path = jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64_444.jpg");
roundtrip_jpeg_byteexact(&path.to_string_lossy(), "subsamp_444_64x64");
}
#[test]
fn test_subsamp_444_128x128() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test128_444.jpg")
.to_string_lossy(),
"subsamp_444_128x128",
);
}
#[test]
fn test_subsamp_420_64x64() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64_420.jpg")
.to_string_lossy(),
"subsamp_420_64x64",
);
}
#[test]
fn test_subsamp_420_128x128() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test128_420.jpg")
.to_string_lossy(),
"subsamp_420_128x128",
);
}
#[test]
fn test_subsamp_420_512x512() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test512_420.jpg")
.to_string_lossy(),
"subsamp_420_512x512",
);
}
#[test]
fn test_subsamp_420_odd_size() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test_odd_420.jpg")
.to_string_lossy(),
"subsamp_420_odd_100x75",
);
}
#[test]
fn test_subsamp_420_real_photo() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test_real_420_stripped.jpg")
.to_string_lossy(),
"subsamp_420_real_stripped",
);
}
#[test]
fn test_subsamp_420_real_photo_icc() {
roundtrip_jpeg_byteexact(
&format!(
"{}/imageflow/test_inputs/orientation/Landscape_2.jpg",
jxl_encoder::test_helpers::corpus_dir().display()
),
"subsamp_420_landscape2",
);
}
#[test]
fn test_subsamp_422_64x64() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64_422.jpg")
.to_string_lossy(),
"subsamp_422_64x64",
);
}
#[test]
fn test_subsamp_422_128x128() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test128_422.jpg")
.to_string_lossy(),
"subsamp_422_128x128",
);
}
#[test]
fn test_subsamp_440_64x64() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64_440.jpg")
.to_string_lossy(),
"subsamp_440_64x64",
);
}
#[test]
fn test_subsamp_440_128x128() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test128_440.jpg")
.to_string_lossy(),
"subsamp_440_128x128",
);
}
#[test]
fn test_subsamp_gray_128x128() {
roundtrip_jpeg_byteexact(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test128_gray.jpg")
.to_string_lossy(),
"subsamp_gray_128x128",
);
}
#[test]
fn test_jxlrs_444_64x64() {
verify_jxl_rs_decodes(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test64_444.jpg")
.to_string_lossy(),
"jxlrs_444_64x64",
);
}
#[test]
fn test_jxlrs_420_128x128() {
verify_jxl_rs_decodes(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test128_420.jpg")
.to_string_lossy(),
"jxlrs_420_128x128",
);
}
#[test]
fn test_jxlrs_422_128x128() {
verify_jxl_rs_decodes(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test128_422.jpg")
.to_string_lossy(),
"jxlrs_422_128x128",
);
}
#[test]
fn test_jxlrs_440_128x128() {
verify_jxl_rs_decodes(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test128_440.jpg")
.to_string_lossy(),
"jxlrs_440_128x128",
);
}
#[test]
fn test_jxlrs_gray_128x128() {
verify_jxl_rs_decodes(
&jxl_encoder::test_helpers::output_dir_for("jpeg-reencoding", "test128_gray.jpg")
.to_string_lossy(),
"jxlrs_gray_128x128",
);
}
#[test]
fn test_jxlrs_420_real_icc() {
verify_jxl_rs_decodes(
&format!(
"{}/imageflow/test_inputs/orientation/Landscape_2.jpg",
jxl_encoder::test_helpers::corpus_dir().display()
),
"jxlrs_420_landscape2",
);
}