Files
photocraft/crates/codecs/tests/camera_raw.rs
Brandon ThomasandClaude Opus 5.5 f7eb6ea9e7 Format: adopt the craft-family rustfmt.toml and format the workspace
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:31:17 -04:00

135 lines
5.1 KiB
Rust
Raw Permalink 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]);
}
}