photocraft/crates/codecs/tests/camera_raw.rs

135 lines
5.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Camera raw files are TIFF-structured (TIFF/EP, DNG, CR2, NEF, ARW…) but
//! hold sensor data this crate does not develop. Opening one must say so,
//! not fail with a TIFF decoder detail or silently return the embedded
//! preview. The files here are tiny synthetic TIFFs with the structural
//! markers from the public TIFF/EP and DNG specifications.
use photocraft_codecs::*;
const SHORT: u16 = 3;
const LONG: u16 = 4;
const BYTE4: u16 = 1; // BYTE, written with count 4 (the value's 4 bytes inline)
const CFA: u32 = 32803; // TIFF/EP PhotometricInterpretation: colour filter array
const LINEAR_RAW: u32 = 34892; // DNG PhotometricInterpretation: LinearRaw
/// A little-endian TIFF with the given IFDs (each a list of
/// `(tag, type, value)` entries with count 1), chained in order. Every IFD
/// gets 2×2 8-bit strip data. `header8` replaces bytes 8..12 (CR2 puts
/// "CR" + version there and starts IFD0 at 16).
fn tiff(ifds: &[Vec<(u16, u16, u32)>], header8: Option<[u8; 4]>) -> Vec<u8> {
let mut b = b"II*\0".to_vec();
let first = if header8.is_some() { 16u32 } else { 8 };
b.extend_from_slice(&first.to_le_bytes());
if let Some(h) = header8 {
b.extend_from_slice(&h);
b.extend_from_slice(&0u32.to_le_bytes());
}
// Layout: IFDs back to back, then one shared 12-byte pixel strip.
let sizes: Vec<u32> = ifds.iter().map(|e| 2 + 12 * (e.len() as u32 + 1) + 4).collect();
let strip = first + sizes.iter().sum::<u32>();
let mut at = first;
for (i, entries) in ifds.iter().enumerate() {
let mut entries = entries.clone();
entries.push((273, LONG, strip)); // StripOffsets
entries.sort_by_key(|e| e.0);
b.extend_from_slice(&(entries.len() as u16).to_le_bytes());
for (tag, ty, v) in entries {
b.extend_from_slice(&tag.to_le_bytes());
b.extend_from_slice(&ty.to_le_bytes());
let count: u32 = if ty == BYTE4 { 4 } else { 1 };
b.extend_from_slice(&count.to_le_bytes());
if ty == SHORT {
b.extend_from_slice(&(v as u16).to_le_bytes());
b.extend_from_slice(&[0, 0]);
} else {
b.extend_from_slice(&v.to_le_bytes());
}
}
at += sizes[i];
let next = if i + 1 < ifds.len() { at } else { 0 };
b.extend_from_slice(&next.to_le_bytes());
}
b.extend_from_slice(&[128; 12]);
b
}
/// A 2×2 image IFD with the given photometric interpretation.
fn image_ifd(photometric: u32, samples: u32) -> Vec<(u16, u16, u32)> {
vec![
(256, SHORT, 2), // ImageWidth
(257, SHORT, 2), // ImageLength
(258, SHORT, 8), // BitsPerSample
(259, SHORT, 1), // Compression: none
(262, SHORT, photometric), // PhotometricInterpretation
(277, SHORT, samples), // SamplesPerPixel
(278, SHORT, 2), // RowsPerStrip
(279, LONG, 4 * samples), // StripByteCounts
]
}
fn assert_camera_raw(bytes: &[u8]) {
assert_eq!(detect(bytes), Some(Format::Tiff));
match decode(bytes) {
Err(CodecError::Unsupported { reason, .. }) => {
assert!(reason.contains("camera raw"), "unexpected reason: {reason}");
}
Err(e) => panic!("expected a camera raw error, got: {e}"),
Ok(img) => panic!("expected a camera raw error, decoded {:?}", img.dimensions()),
}
}
#[test]
fn cfa_image_is_reported_as_camera_raw() {
// TIFF/EP-style raw (NEF/ARW-like): the main image is a CFA.
assert_camera_raw(&tiff(&[image_ifd(CFA, 1)], None));
}
#[test]
fn linear_raw_image_is_reported_as_camera_raw() {
assert_camera_raw(&tiff(&[image_ifd(LINEAR_RAW, 3)], None));
}
#[test]
fn raw_behind_a_preview_is_reported_as_camera_raw() {
// IFD0 is a small RGB preview, the CFA raw is a SubIFD (tag 330): decoding
// must not quietly return the preview as the photo.
let mut ifd0 = image_ifd(2, 3);
let ifd0_size = 2 + 12 * (ifd0.len() as u32 + 2) + 4;
ifd0.push((330, LONG, 8 + ifd0_size)); // SubIFDs → the next IFD
let mut b = tiff(&[ifd0, image_ifd(CFA, 1)], None);
// Unchain the raw IFD from IFD0's next-IFD pointer so it is reachable only
// as a SubIFD.
let next_at = 8 + ifd0_size as usize - 4;
b[next_at..next_at + 4].copy_from_slice(&0u32.to_le_bytes());
assert_camera_raw(&b);
}
#[test]
fn dng_is_reported_as_camera_raw() {
let mut ifd0 = image_ifd(2, 3);
ifd0.push((50706, BYTE4, 0x0000_0401)); // DNGVersion 1.4.0.0
assert_camera_raw(&tiff(&[ifd0], None));
}
#[test]
fn cr2_is_reported_as_camera_raw() {
// CR2: TIFF header + "CR", major 2, minor 0 at byte 8; IFD0 at 16.
assert_camera_raw(&tiff(&[image_ifd(6, 3)], Some(*b"CR\x02\0")));
}
#[test]
fn ordinary_tiffs_still_decode() {
let rgb = decode(&tiff(&[image_ifd(2, 3)], None)).unwrap();
assert_eq!(rgb.dimensions(), (2, 2));
let gray = decode(&tiff(&[image_ifd(1, 1)], None)).unwrap();
assert_eq!(gray.dimensions(), (2, 2));
}
#[test]
fn truncated_raw_markers_do_not_panic() {
let b = tiff(&[image_ifd(CFA, 1)], Some(*b"CR\x02\0"));
for n in 0..b.len() {
let _ = decode(&b[..n]);
}
}