use edgefirst_codec::{peek_info, CodecError, ImageDecoder, ImageLoad, UnsupportedFeature};
use edgefirst_tensor::{PixelFormat, Tensor, TensorMemory, TensorTrait};
fn testdata(name: &str) -> Vec<u8> {
let root = std::env::var("EDGEFIRST_TESTDATA_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("testdata")
});
let path = root.join(name);
std::fs::read(&path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()))
}
#[test]
fn decode_zidane_nv12() {
let jpeg = testdata("zidane.jpg");
let mut tensor =
Tensor::<u8>::image(1280, 720, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let info = tensor.load_image(&mut decoder, &jpeg).unwrap();
assert_eq!(info.width, 1280);
assert_eq!(info.height, 720);
assert_eq!(info.format, PixelFormat::Nv12);
let map = tensor.map().unwrap();
let pixels: &[u8] = ↦
let nonzero = pixels[..info.width * info.height]
.iter()
.filter(|&&v| v != 0)
.count();
assert!(
nonzero > 1000,
"expected many non-zero Y-plane bytes, got {nonzero}"
);
}
#[test]
fn decode_grey_jpeg() {
let jpeg = testdata("grey.jpg");
let mut tensor =
Tensor::<u8>::image(1024, 681, PixelFormat::Grey, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let info = tensor.load_image(&mut decoder, &jpeg).unwrap();
assert!(info.width > 0 && info.height > 0);
assert_eq!(info.format, PixelFormat::Grey);
let map = tensor.map().unwrap();
let pixels: &[u8] = ↦
let nonzero = pixels[..info.width * info.height]
.iter()
.filter(|&&v| v != 0)
.count();
assert!(
nonzero > 1000,
"expected non-zero grey pixels, got {nonzero}"
);
}
#[test]
fn decode_person_rejected() {
let jpeg = testdata("person.jpg");
let mut tensor =
Tensor::<u8>::image(4256, 2532, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let result = tensor.load_image(&mut decoder, &jpeg);
assert!(
result.is_err(),
"progressive JPEG should be rejected by baseline decoder"
);
}
#[test]
fn decode_f32_jpeg_unsupported_dtype() {
let jpeg = testdata("zidane.jpg");
let mut tensor =
Tensor::<f32>::image(1280, 720, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let result = tensor.load_image(&mut decoder, &jpeg);
assert!(
matches!(result, Err(CodecError::UnsupportedDtype(_))),
"expected UnsupportedDtype for f32 JPEG decode, got {result:?}"
);
}
#[test]
fn decode_capacity_error() {
let jpeg = testdata("zidane.jpg"); let mut tensor =
Tensor::<u8>::image(100, 100, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let result = tensor.load_image(&mut decoder, &jpeg);
assert!(result.is_err());
match result.unwrap_err() {
CodecError::InsufficientCapacity { image, tensor } => {
assert_eq!(image, (1280, 720));
assert_eq!(tensor, (100, 100));
}
other => panic!("expected InsufficientCapacity, got {other}"),
}
}
#[test]
fn decode_reuse_pattern() {
let mut tensor =
Tensor::<u8>::image(1280, 720, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let images = ["zidane.jpg", "giraffe.jpg"];
for name in &images {
let jpeg = testdata(name);
let info = tensor.load_image(&mut decoder, &jpeg).unwrap();
assert_eq!(
info.format,
PixelFormat::Nv12,
"{name} should decode to NV12"
);
assert!(info.width > 0 && info.height > 0, "failed to decode {name}");
}
}
#[test]
fn pixel_accuracy_grey_vs_image_crate() {
let jpeg_data = testdata("grey.jpg");
let mut tensor =
Tensor::<u8>::image(1024, 681, PixelFormat::Grey, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let info = tensor.load_image(&mut decoder, &jpeg_data).unwrap();
assert_eq!(info.format, PixelFormat::Grey);
let ref_img = image::load_from_memory(&jpeg_data).unwrap().to_luma8();
let (ref_w, ref_h) = ref_img.dimensions();
assert_eq!(info.width, ref_w as usize);
assert_eq!(info.height, ref_h as usize);
let map = tensor.map().unwrap();
let our_pixels: &[u8] = unsafe { std::slice::from_raw_parts(map.as_ptr(), map.len()) };
let ref_pixels = ref_img.as_raw();
let w = info.width;
let h = info.height;
let stride = info.row_stride;
let mut max_diff: u32 = 0;
let mut total_diff: u64 = 0;
for y in 0..h {
for x in 0..w {
let our_val = our_pixels[y * stride + x] as i32;
let ref_val = ref_pixels[y * w + x] as i32;
let diff = (our_val - ref_val).unsigned_abs();
max_diff = max_diff.max(diff);
total_diff += diff as u64;
}
}
let pixel_count = (w * h) as f64;
let mae = total_diff as f64 / pixel_count;
eprintln!("Grey accuracy: MAE={mae:.3}, max_diff={max_diff}");
assert!(
max_diff <= 4,
"max grey diff {max_diff} exceeds tolerance 4"
);
assert!(mae < 1.0, "grey MAE {mae:.3} exceeds tolerance 1.0");
}
#[test]
fn decode_strided_oversized_tensor() {
let jpeg = testdata("zidane.jpg");
let mut tensor =
Tensor::<u8>::image(1920, 1080, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
{
let mut map = tensor.map().unwrap();
let bytes: &mut [u8] =
unsafe { std::slice::from_raw_parts_mut(map.as_mut_ptr(), map.len()) };
bytes.fill(0xAA);
}
let mut decoder = ImageDecoder::new();
let info = tensor.load_image(&mut decoder, &jpeg).unwrap();
assert_eq!(info.width, 1280);
assert_eq!(info.height, 720);
assert_eq!(info.format, PixelFormat::Nv12);
let stride = info.row_stride;
let map = tensor.map().unwrap();
let bytes: &[u8] = unsafe { std::slice::from_raw_parts(map.as_ptr(), map.len()) };
let decoded_row_bytes = 1280;
for y in 0..720 {
let row = &bytes[y * stride..y * stride + decoded_row_bytes];
let non_sentinel = row.iter().filter(|&&b| b != 0xAA).count();
assert!(
non_sentinel > 0,
"Y row {y} appears to be all sentinel values"
);
}
}
#[test]
fn decode_truncated_jpeg() {
let jpeg = testdata("zidane.jpg");
let truncated = &jpeg[..100];
let mut tensor =
Tensor::<u8>::image(1280, 720, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let result = tensor.load_image(&mut decoder, truncated);
assert!(matches!(result, Err(CodecError::InvalidData(_))));
}
#[test]
fn decode_not_jpeg() {
let mut bogus = testdata("zidane.png");
bogus[..4].copy_from_slice(&[0xFF, 0xD8, 0xFF, 0xE0]);
let mut tensor =
Tensor::<u8>::image(1280, 720, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let result = tensor.load_image(&mut decoder, &bogus);
assert!(matches!(result, Err(CodecError::InvalidData(_))));
}
#[test]
fn decode_empty_data() {
let mut tensor =
Tensor::<u8>::image(1280, 720, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let result = tensor.load_image(&mut decoder, &[]);
assert!(matches!(result, Err(CodecError::InvalidData(_))));
}
#[test]
fn decode_corrupt_markers() {
let corrupt = [0xFF, 0xD8, 0xFF, 0x00];
let mut tensor =
Tensor::<u8>::image(1280, 720, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let result = tensor.load_image(&mut decoder, &corrupt);
assert!(matches!(result, Err(CodecError::InvalidData(_))));
}
#[test]
fn decode_exact_size_tensor() {
use image::ImageDecoder as _;
let jpeg = testdata("giraffe.jpg");
let header =
image::codecs::jpeg::JpegDecoder::new(std::io::Cursor::new(jpeg.as_slice())).unwrap();
let (width, height) = header.dimensions();
let mut tensor = Tensor::<u8>::image(
width as usize,
height as usize,
PixelFormat::Nv12,
Some(TensorMemory::Mem),
)
.unwrap();
let mut decoder = ImageDecoder::new();
let info = tensor.load_image(&mut decoder, &jpeg).unwrap();
assert_eq!(info.width, width as usize);
assert_eq!(info.height, height as usize);
assert_eq!(info.format, PixelFormat::Nv12);
}
#[test]
fn decode_nv12_output() {
let jpeg = testdata("zidane.jpg");
let width = 1280usize;
let height = 720usize;
let mut tensor =
Tensor::<u8>::image(width, height, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let info = tensor.load_image(&mut decoder, &jpeg).unwrap();
assert_eq!(info.width, width);
assert_eq!(info.height, height);
assert_eq!(info.format, PixelFormat::Nv12);
let map = tensor.map().unwrap();
let bytes: &[u8] = ↦
let y_nonzero = bytes[..width * height].iter().filter(|&&v| v != 0).count();
assert!(
y_nonzero > 1000,
"Y plane should have many non-zero pixels, got {y_nonzero}"
);
let uv_start = height * width;
let uv_size = width * height / 2;
if uv_start + uv_size <= bytes.len() {
let uv_non128 = bytes[uv_start..uv_start + uv_size]
.iter()
.filter(|&&v| v != 128)
.count();
assert!(
uv_non128 > 100,
"UV plane should have varied chrominance, non-128 count: {uv_non128}"
);
}
}
#[test]
fn unsupported_progressive_jpeg_returns_typed_variant() {
let jpeg = testdata("person.jpg");
match peek_info(&jpeg) {
Err(CodecError::Unsupported(UnsupportedFeature::ProgressiveJpeg)) => {}
Err(other) => {
panic!("expected Unsupported(ProgressiveJpeg), got {other:?}");
}
Ok(_) => panic!("progressive JPEG should not decode"),
}
}
#[test]
fn jpeg_decode_tags_jfif_colorimetry() {
use edgefirst_tensor::Colorimetry;
let mut t = Tensor::<u8>::image(1280, 720, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
let mut d = ImageDecoder::new();
t.load_image(&mut d, &testdata("zidane.jpg")).unwrap();
assert_eq!(t.colorimetry(), Some(Colorimetry::jfif()));
}
#[test]
fn odd_width_nv12_jpeg_decodes_with_logical_width() {
let jpeg = testdata("jaguar.jpg");
let info = peek_info(&jpeg).unwrap();
assert_eq!((info.width, info.height), (789, 384));
assert_eq!(
info.format,
PixelFormat::Nv12,
"colour JPEG should report native NV12 format"
);
let mut tensor =
Tensor::<u8>::image(789, 384, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
assert_eq!(tensor.width(), Some(789), "logical width must be preserved");
assert_eq!(tensor.height(), Some(384));
let s = tensor
.effective_row_stride()
.expect("semi-planar must always have a stride");
assert_eq!(s % 64, 0, "stride must be 64-byte aligned, got {s}");
assert!(s >= 790, "stride must be >= even(width)=790, got {s}");
let mut decoder = ImageDecoder::new();
let info = tensor
.load_image(&mut decoder, &jpeg)
.expect("odd-width NV12 JPEG should decode into the strided buffer");
assert_eq!((info.width, info.height), (789, 384));
assert_eq!(tensor.width(), Some(789));
assert_eq!(tensor.height(), Some(384));
let s2 = tensor.effective_row_stride().unwrap();
assert_eq!(s2 % 64, 0, "post-decode stride must be 64-byte aligned");
assert!(s2 >= 790, "post-decode stride must be >= 790");
}
fn rgb_to_cbcr(r: f32, g: f32, b: f32) -> (f32, f32) {
let cb = 128.0 - 0.168_736 * r - 0.331_264 * g + 0.5 * b;
let cr = 128.0 + 0.5 * r - 0.418_688 * g - 0.081_312 * b;
(cb, cr)
}
fn check_native_decode(fixture: &str, expect_fmt: PixelFormat) {
check_native_decode_dims(fixture, expect_fmt, 1280, 720);
}
fn check_native_decode_dims(
fixture: &str,
expect_fmt: PixelFormat,
expect_w: usize,
expect_h: usize,
) {
let jpeg = testdata(fixture);
let alloc_w = expect_w.max(1280);
let alloc_h = expect_h.max(720);
let mut tensor =
Tensor::<u8>::image(alloc_w, alloc_h, expect_fmt, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let info = tensor.load_image(&mut decoder, &jpeg).unwrap();
assert_eq!(info.format, expect_fmt, "{fixture}: native format");
assert_eq!(
(info.width, info.height),
(expect_w, expect_h),
"{fixture}: dims"
);
let ref_rgb = image::load_from_memory(&jpeg).unwrap().to_rgb8();
assert_eq!(ref_rgb.dimensions(), (expect_w as u32, expect_h as u32));
let w = info.width;
let h = info.height;
let stride = info.row_stride;
let map = tensor.map().unwrap();
let buf: &[u8] = ↦
let ref_luma = image::load_from_memory(&jpeg).unwrap().to_luma8();
let mut y_max: u32 = 0;
let mut y_total: u64 = 0;
for y in 0..h {
for x in 0..w {
let ours = buf[y * stride + x] as i32;
let refv = ref_luma.get_pixel(x as u32, y as u32)[0] as i32;
let d = (ours - refv).unsigned_abs();
y_max = y_max.max(d);
y_total += d as u64;
}
}
let y_mae = y_total as f64 / (w * h) as f64;
eprintln!("{fixture} luma: MAE={y_mae:.3}, max={y_max}");
assert!(y_max <= 24, "{fixture}: luma max diff {y_max} > 24");
assert!(y_mae < 2.0, "{fixture}: luma MAE {y_mae:.3} > 2.0");
let uv_off = h * stride;
let chroma_at = |x: usize, y: usize| -> (i32, i32) {
let byte_off = match expect_fmt {
PixelFormat::Nv16 => uv_off + y * stride + (x / 2) * 2,
_ => uv_off + y * 2 * stride + x * 2,
};
(buf[byte_off] as i32, buf[byte_off + 1] as i32)
};
let mut c_max: u32 = 0;
let mut c_total: u64 = 0;
let mut samples: u64 = 0;
for y in (0..h).step_by(2) {
for x in (0..w).step_by(2) {
let p = ref_rgb.get_pixel(x as u32, y as u32);
let (ecb, ecr) = rgb_to_cbcr(p[0] as f32, p[1] as f32, p[2] as f32);
let (cb, cr) = chroma_at(x, y);
let dcb = (cb - ecb.round() as i32).unsigned_abs();
let dcr = (cr - ecr.round() as i32).unsigned_abs();
c_max = c_max.max(dcb).max(dcr);
c_total += (dcb + dcr) as u64;
samples += 2;
}
}
let c_mae = c_total as f64 / samples as f64;
eprintln!("{fixture} chroma: MAE={c_mae:.3}, max={c_max}");
assert!(
c_mae < 6.0,
"{fixture}: chroma MAE {c_mae:.3} > 6.0 (layout?)"
);
assert!(
c_max <= 40,
"{fixture}: chroma max diff {c_max} > 40 (layout?)"
);
}
#[test]
fn decode_zidane_nv16() {
check_native_decode("zidane_422.jpg", PixelFormat::Nv16);
}
#[test]
fn decode_zidane_nv24() {
check_native_decode("zidane_444.jpg", PixelFormat::Nv24);
}
#[test]
fn decode_jaguar_nv16() {
check_native_decode_dims("jaguar_422.jpg", PixelFormat::Nv16, 789, 384);
}
#[test]
fn decode_jaguar_nv24() {
check_native_decode_dims("jaguar_444.jpg", PixelFormat::Nv24, 789, 384);
}
#[test]
fn decode_coco_grey_odd_width() {
let jpeg = testdata("coco_grey_odd.jpg");
let (w, h) = (595usize, 438usize);
let mut tensor = Tensor::<u8>::image(w, h, PixelFormat::Grey, Some(TensorMemory::Mem)).unwrap();
let mut decoder = ImageDecoder::new();
let info = tensor.load_image(&mut decoder, &jpeg).unwrap();
assert_eq!(info.format, PixelFormat::Grey, "native greyscale format");
assert_eq!((info.width, info.height), (w, h), "odd dims preserved");
let stride = info.row_stride;
let ref_luma = image::load_from_memory(&jpeg).unwrap().to_luma8();
assert_eq!(ref_luma.dimensions(), (w as u32, h as u32));
let map = tensor.map().unwrap();
let buf: &[u8] = ↦
let mut y_max: u32 = 0;
let mut y_total: u64 = 0;
for y in 0..h {
for x in 0..w {
let ours = buf[y * stride + x] as i32;
let refv = ref_luma.get_pixel(x as u32, y as u32)[0] as i32;
let d = (ours - refv).unsigned_abs();
y_max = y_max.max(d);
y_total += d as u64;
}
}
let y_mae = y_total as f64 / (w * h) as f64;
eprintln!("coco_grey_odd luma: MAE={y_mae:.3}, max={y_max}");
assert!(y_max <= 4, "grey luma max diff {y_max} > 4");
assert!(y_mae < 1.0, "grey luma MAE {y_mae:.3} > 1.0");
}