use crate::api::{
JxlDataFormat, JxlDecoder, JxlDecoderOptions, JxlOutputBuffer, JxlPixelFormat,
ProcessingResult, states,
};
use crate::image::{Image, Rect};
fn decode_jpeg_reconstruction(jxl_data: &[u8]) -> Option<Vec<u8>> {
let options = JxlDecoderOptions::default();
let mut decoder = JxlDecoder::<states::Initialized>::new(options);
let mut input = jxl_data;
let mut decoder = loop {
match decoder.process(&mut input).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
if input.is_empty() {
panic!("Unexpected end of input during header");
}
decoder = fallback;
}
}
};
let basic_info = decoder.basic_info().clone();
let (width, height) = basic_info.size;
let format = JxlPixelFormat {
color_type: crate::api::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).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
if input.is_empty() {
panic!("Unexpected end of input during frame header");
}
decoder = fallback;
}
}
};
let mut color_buffer = Image::<f32>::new((width * 3, height)).unwrap();
let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
color_buffer
.get_rect_mut(Rect {
origin: (0, 0),
size: (width * 3, height),
})
.into_raw(),
)];
let mut decoder = loop {
match decoder.process(&mut input, &mut buffers).unwrap() {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
if input.is_empty() {
panic!("Unexpected end of input during frame decode");
}
decoder = fallback;
}
}
};
decoder.take_jpeg_reconstruction()
}
fn assert_jpeg_match(reconstructed: &[u8], reference: &[u8], label: &str) {
assert!(
reconstructed.len() >= 2,
"{label}: Reconstructed JPEG too short: {} bytes",
reconstructed.len()
);
assert_eq!(
&reconstructed[..2],
&[0xFF, 0xD8],
"{label}: Should start with JPEG SOI marker"
);
if reconstructed != reference {
let min_len = reconstructed.len().min(reference.len());
for i in 0..min_len {
if reconstructed[i] != reference[i] {
let start = i.saturating_sub(4);
let end = (i + 8).min(min_len);
eprintln!(
"{label}: First difference at byte {i} (0x{i:04x}): \
got 0x{:02x}, expected 0x{:02x}",
reconstructed[i], reference[i]
);
eprintln!(
" got: {:02x?}",
&reconstructed[start..end.min(reconstructed.len())]
);
eprintln!(
" ref: {:02x?}",
&reference[start..end.min(reference.len())]
);
break;
}
}
}
assert_eq!(
reconstructed.len(),
reference.len(),
"{label}: length mismatch: got {} expected {}",
reconstructed.len(),
reference.len()
);
assert_eq!(reconstructed, reference, "{label}: byte-exact match failed");
}
#[test]
fn test_jpeg_reconstruction_3x3() {
let jxl_data = std::fs::read("resources/test/3x3_jpeg_recompression.jxl").unwrap();
let reference = std::fs::read("resources/test/3x3_jpeg_recompression_reference.jpg").unwrap();
let reconstructed =
decode_jpeg_reconstruction(&jxl_data).expect("JPEG reconstruction should succeed for 3x3");
assert_jpeg_match(&reconstructed, &reference, "3x3 (4:2:0)");
}
#[test]
fn test_jpeg_reconstruction_16x16() {
let jxl_data = std::fs::read("resources/test/test_16x16_jpeg_recompression.jxl").unwrap();
let reference = std::fs::read("resources/test/test_16x16.jpg").unwrap();
let reconstructed = decode_jpeg_reconstruction(&jxl_data)
.expect("JPEG reconstruction should succeed for 16x16");
assert_jpeg_match(&reconstructed, &reference, "16x16 (4:2:0)");
}
#[test]
fn test_jpeg_reconstruction_8x8_444() {
let jxl_data = std::fs::read("resources/test/test_8x8_444_jpeg_recompression.jxl").unwrap();
let reference = std::fs::read("resources/test/test_8x8_444.jpg").unwrap();
let reconstructed = decode_jpeg_reconstruction(&jxl_data)
.expect("JPEG reconstruction should succeed for 8x8 4:4:4");
assert_jpeg_match(&reconstructed, &reference, "8x8 (4:4:4)");
}
#[test]
fn test_jpeg_reconstruction_64x64_420() {
let jxl_data = std::fs::read("resources/test/test_64x64_420_jpeg_recompression.jxl").unwrap();
let reference = std::fs::read("resources/test/test_64x64_420.jpg").unwrap();
let reconstructed = decode_jpeg_reconstruction(&jxl_data)
.expect("JPEG reconstruction should succeed for 64x64 4:2:0");
assert_jpeg_match(&reconstructed, &reference, "64x64 (4:2:0)");
}
#[test]
fn test_jpeg_reconstruction_libjxl_128x128_444() {
let jxl_data = std::fs::read("resources/test/test_128x128_444_libjxl.jxl").unwrap();
let reference = std::fs::read("resources/test/test_128x128_444_libjxl.jpg").unwrap();
let reconstructed = decode_jpeg_reconstruction(&jxl_data)
.expect("JPEG reconstruction should succeed for libjxl 128x128 4:4:4");
assert_jpeg_match(&reconstructed, &reference, "128x128 libjxl (4:4:4)");
}
#[test]
fn test_jpeg_reconstruction_libjxl_128x128_420() {
let jxl_data = std::fs::read("resources/test/test_128x128_420_libjxl.jxl").unwrap();
let reference = std::fs::read("resources/test/test_128x128_420_libjxl.jpg").unwrap();
let reconstructed = decode_jpeg_reconstruction(&jxl_data)
.expect("JPEG reconstruction should succeed for libjxl 128x128 4:2:0");
assert_jpeg_match(&reconstructed, &reference, "128x128 libjxl (4:2:0)");
}
#[test]
fn test_no_jpeg_reconstruction_for_non_jpeg_jxl() {
let jxl_data = std::fs::read("resources/test/basic.jxl").unwrap();
let reconstructed = decode_jpeg_reconstruction(&jxl_data);
assert!(
reconstructed.is_none(),
"Non-JPEG JXL should not produce JPEG reconstruction"
);
}