Files
lightcraft/crates/raw/src/tests.rs
T

613 lines
29 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.
use super::*;
#[test]
fn cfa_helpers() {
let c = Cfa::bayer("RGGB").unwrap();
assert!(c.is_bayer());
assert_eq!(c.color_at(0, 0), 0);
assert_eq!(c.color_at(3, 3), 2);
assert_eq!(c.shifted(1, 0).name(), "GRBG");
assert_eq!(c.shifted(1, 1).name(), "BGGR");
assert!(Cfa::bayer("RGGX").is_none());
for name in ["RGGB", "BGGR", "GRBG", "GBRG"] {
assert_eq!(Cfa::bayer_static(name).name(), name);
}
let x = Cfa::xtrans();
assert!(!x.is_bayer());
let greens = x.pattern.iter().filter(|&&p| p == 1).count();
assert_eq!(greens, 20);
// every 3×3 block of the X-Trans layout has 5 greens
for by in 0..2 {
for bx in 0..2 {
let g = (0..9).filter(|i| x.color_at(bx * 3 + i % 3, by * 3 + i / 3) == 1).count();
assert_eq!(g, 5);
}
}
}
#[test]
fn a_picture_made_elsewhere_is_finished_like_develop_does() {
let (w, h) = (12usize, 10usize);
let raw = RawImage {
format: RawFormat::Dng,
width: w,
height: h,
cpp: 1,
data: RawData::U16((0..w * h).map(|i| (100 + (i * 37) % 900) as u16).collect()),
cfa: Cfa::bayer("RGGB"),
bits: 12,
black: BlackLevel::uniform(64.0),
white: vec![1023.0],
active_area: Rect::new(2, 2, 8, 6),
crop: Rect::new(1, 1, 5, 4),
orientation: Orientation::Normal,
color: ColorData::default(),
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata: Metadata::default(),
lens_metadata: Default::default(),
};
let direct = raw.develop(Method::Bilinear).unwrap();
let by_hand = raw.finish_demosaiced(demosaic(&raw.normalized().unwrap(), Method::Bilinear));
assert_eq!((direct.width, direct.height), (5, 4));
assert_eq!(direct, by_hand);
// the crop is what develop keeps; no usable crop keeps everything
assert_eq!(raw.develop_crop(8, 6), Rect::new(1, 1, 5, 4));
let mut whole = raw.clone();
whole.crop = Rect::default();
assert_eq!(whole.develop_crop(8, 6), Rect::new(0, 0, 8, 6));
assert_eq!(whole.develop(Method::Bilinear).unwrap().width, 8);
// a crop that pokes out is clipped to the picture
whole.crop = Rect::new(6, 4, 10, 10);
assert_eq!(whole.develop_crop(8, 6), Rect::new(6, 4, 2, 2));
// tall pixels (D1X: 2.0) are stretched to square ones: the rows double, `developed_size` says so
let mut tall = raw.clone();
tall.pixel_aspect = 2.0;
let d = tall.develop(Method::Bilinear).unwrap();
assert_eq!((d.width, d.height), (5, 8));
assert_eq!(tall.info().developed_size(), (5, 8));
assert_eq!(raw.info().developed_size(), (5, 4));
assert!(!tall.can_bin(2));
assert_eq!(stretched_height(1324, 2.0), 2648);
assert_eq!(stretched_height(1324, 1.0), 1324);
}
/// A maker-note crop (`PEF` AOC / `ORF` ImageProcessing) with an origin of `u64::MAX` used to
/// pass the bounds check (`x + width` wraps to 0) and then index out of bounds in
/// `normalized`. It must be a `Corrupt` error at validate time instead.
#[test]
fn an_active_area_that_wraps_past_the_image_is_corrupt() {
let (w, h) = (12usize, 10usize);
let mut raw = RawImage {
format: RawFormat::Dng,
width: w,
height: h,
cpp: 1,
data: RawData::U16(vec![100; w * h]),
cfa: Cfa::bayer("RGGB"),
bits: 12,
black: BlackLevel::uniform(64.0),
white: vec![1023.0],
active_area: Rect::new(usize::MAX, 0, 1, 1),
crop: Rect::default(),
orientation: Orientation::Normal,
color: ColorData::default(),
wb_multipliers: None,
linearized: false,
opcodes: OpcodeLists::default(),
metadata: Metadata::default(),
lens_metadata: Default::default(),
pixel_aspect: 1.0,
};
assert!(raw.validate().is_err(), "`x + width` wrapped: the area is outside the image");
assert!(raw.normalized().is_err());
raw.active_area = Rect::new(0, 0, w + 1, h);
assert!(raw.validate().is_err(), "one past the last column is outside too");
}
#[test]
fn black_levels() {
let b = BlackLevel { repeat_rows: 2, repeat_cols: 2, values: vec![1.0, 2.0, 3.0, 4.0], delta_h: vec![0.5, 0.0], delta_v: vec![0.0, 10.0] };
assert_eq!(b.at(0, 0, 0, 1), 1.5);
assert_eq!(b.at(1, 1, 0, 1), 14.0);
assert_eq!(b.at(3, 2, 0, 1), 2.0);
assert_eq!(b.mean(), 2.5);
assert_eq!(BlackLevel::uniform(7.0).at(9, 9, 0, 3), 7.0);
}
// --- TIFF shells around a private raw block (thumbnail-only IFD0) ---
use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value, tags as t};
/// An uncompressed 8-bit RGB image IFD.
fn rgb_ifd(w: u32, h: u32) -> IfdBuilder {
let mut ifd = IfdBuilder::new();
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![8, 8, 8]));
ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![3]));
ifd.set(t::PHOTOMETRIC, Value::Short(vec![2]));
ifd.set(t::COMPRESSION, Value::Short(vec![1]));
ifd.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![vec![90u8; (w * h * 3) as usize]] });
ifd
}
fn exif_size(w: u32, h: u32) -> IfdBuilder {
IfdBuilder::new().with(t::PIXEL_X_DIMENSION, Value::Long(vec![w])).with(t::PIXEL_Y_DIMENSION, Value::Long(vec![h]))
}
fn write(chain: &[IfdBuilder]) -> Vec<u8> {
TiffWriter::default().write(chain).unwrap()
}
const SHELL_REASON: &str = "raw image in a private block";
/// A bigger image in a `SubIFDs` child of a small IFD0 (the layout of an 8-bit-mosaic TIFF raw).
#[test]
fn thumbnail_with_a_bigger_sub_ifd_is_an_undecodable_raw() {
let mut ifd0 = rgb_ifd(16, 12);
ifd0.add_sub_ifd(rgb_ifd(64, 48));
let bytes = write(&[ifd0]);
assert_eq!(probe(&bytes), Some(RawFormat::OtherTiff));
let Err(RawError::Unsupported(why)) = decode(&bytes) else { panic!("expected Unsupported") };
assert!(why.contains("64x48") && why.contains("16x12") && why.contains(SHELL_REASON), "{why}");
// a sub-IFD that is not much bigger (a pyramid level) does not count
let mut ifd0 = rgb_ifd(16, 12);
ifd0.add_sub_ifd(rgb_ifd(32, 24));
assert_eq!(probe(&write(&[ifd0])), None);
}
/// Exif says the picture is far bigger than IFD0 and the file holds a block that could carry it.
#[test]
fn thumbnail_with_a_private_block_behind_the_exif_size_is_an_undecodable_raw() {
let mut ifd0 = rgb_ifd(16, 12);
ifd0.set_child(t::EXIF_IFD, exif_size(400, 300));
let mut bytes = write(&[ifd0.clone()]);
// no private block: a downsized copy that kept its old Exif size is an ordinary image
assert_eq!(probe(&bytes), None);
// 400 x 300 pixels at one bit each is 15000 bytes: 14000 is not enough, 16000 is
bytes.extend(std::iter::repeat_n(7u8, 14_000));
assert_eq!(probe(&bytes), None);
bytes.extend(std::iter::repeat_n(7u8, 2_000));
assert_eq!(probe(&bytes), Some(RawFormat::OtherTiff));
let Err(RawError::Unsupported(why)) = decode(&bytes) else { panic!("expected Unsupported") };
assert!(why.contains("400x300") && why.contains("16x12") && why.contains(SHELL_REASON), "{why}");
// truncating it never panics
for n in (0..bytes.len()).step_by(37) {
let _ = probe(&bytes[..n]);
let _ = decode(&bytes[..n]);
}
}
/// Ordinary TIFFs stay images: the full picture first, equal Exif size, or later pages that are bigger.
#[test]
fn ordinary_tiffs_are_not_shells() {
let padded = |chain: &[IfdBuilder]| {
let mut b = write(chain);
b.extend(std::iter::repeat_n(0u8, 100_000));
b
};
let mut full = rgb_ifd(400, 300);
full.set_child(t::EXIF_IFD, exif_size(400, 300));
assert_eq!(probe(&padded(&[full])), None);
// the Exif size is much bigger than IFD0, with spare bytes, but IFD0 is not tiny next to it
let mut near = rgb_ifd(120, 90);
near.set_child(t::EXIF_IFD, exif_size(400, 300));
assert_eq!(probe(&padded(&[near])), None);
// a multi-page TIFF whose first page is small
assert_eq!(probe(&padded(&[rgb_ifd(16, 12), rgb_ifd(400, 300)])), None);
// no dimensions at all
assert_eq!(probe(&padded(&[IfdBuilder::new().with(t::MAKE, Value::Ascii("X".into()))])), None);
// raw evidence without a CFA photometric: Pentax's Exif `CFAPattern` (uncompressed / PackBits PEFs) and
// Samsung's private compressions (here 32773, PackBits elsewhere: NX1, NX500)
let mut pef = rgb_ifd(400, 300);
pef.set(t::MAKE, Value::Ascii("PENTAX Corporation".into()));
let mut exif = exif_size(400, 300);
exif.set(vendor::EXIF_CFA_PATTERN, Value::Undefined(vec![0, 2, 0, 2, 0, 1, 1, 2]));
pef.set_child(t::EXIF_IFD, exif);
assert_eq!(probe(&padded(&[pef])), Some(RawFormat::Pef));
let mut srw = rgb_ifd(400, 300);
srw.set(t::MAKE, Value::Ascii("SAMSUNG".into()));
srw.set(t::COMPRESSION, Value::Short(vec![32773]));
assert_eq!(probe(&padded(&[srw])), Some(RawFormat::Srw));
// camera-authored or exported TIFFs preserving camera Make tags remain ordinary images
for make in ["SONY", "NIKON CORPORATION", "PENTAX", "RICOH", "SAMSUNG"] {
let mut cam = rgb_ifd(400, 300);
cam.set(t::MAKE, Value::Ascii(make.into()));
cam.set_child(t::EXIF_IFD, exif_size(400, 300));
assert_eq!(probe(&padded(&[cam])), None, "{make} TIFF should not be probed as raw");
}
}
/// #702: two Sony layouts carry no CFA IFD, so the raw evidence #281 asks of Sony files has to
/// include them: SRF (an RGB-looking IFD0 of 14-bit samples) and the DSLR-A100 (raw data addressed
/// by `SubIFDs`). An 8-bit TIFF from the same bodies, or a SubIFDs TIFF from another Sony model,
/// stays an ordinary image.
#[test]
fn sony_raws_without_a_cfa_ifd_are_still_raws() {
let sony = |model: &str, bits: u16, compression: u16, sub_ifd: Option<IfdBuilder>| {
let mut ifd = rgb_ifd(400, 300);
ifd.set(t::MAKE, Value::Ascii("SONY".into()));
ifd.set(t::MODEL, Value::Ascii(model.into()));
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![bits; 3]));
ifd.set(t::COMPRESSION, Value::Short(vec![compression]));
if let Some(child) = sub_ifd {
ifd.set_child(t::SUB_IFDS, child);
}
probe(&write(&[ifd]))
};
let raw_block = || Some(rgb_ifd(64, 48)); // stands in for the A100's raw-data pointer
assert_eq!(sony("DSC-F828", 14, 1, None), Some(RawFormat::Arw), "SRF");
assert_eq!(sony("DSC-F828", 12, 1, None), Some(RawFormat::Arw), "12-bit samples count too");
assert_eq!(sony("DSLR-A100", 8, 6, raw_block()), Some(RawFormat::Arw), "A100: old-JPEG IFD0 and SubIFDs");
assert_eq!(sony("DSC-F828", 8, 1, None), None, "an 8-bit TIFF");
assert_eq!(sony("DSC-F828", 16, 1, None), None, "a 16-bit TIFF");
// TIFFs exported from A100 photos, with a thumbnail in SubIFDs (Codex review of #702)
assert_eq!(sony("DSLR-A100", 8, 1, Some(rgb_ifd(64, 48))), None, "an 8-bit A100 TIFF with a SubIFDs thumbnail");
assert_eq!(sony("DSLR-A100", 16, 8, Some(rgb_ifd(64, 48))), None, "a 16-bit A100 TIFF with a SubIFDs thumbnail");
assert_eq!(sony("ILCE-7M4", 8, 6, raw_block()), None, "the A100's layout under another model");
}
// --- containers that are recognised but not decoded, with a preview ---
#[test]
fn private_containers_are_recognised_by_their_magic() {
assert_eq!(probe(b"II\x1a\0\0\0HEAPCCDR\0\0"), Some(RawFormat::Crw));
assert_eq!(probe(b"\0MRM\0\x01\0\0"), Some(RawFormat::Mrw));
assert_eq!(probe(b"FOVb\x02\0\x02\0"), Some(RawFormat::X3f));
for f in [RawFormat::Crw, RawFormat::X3f] {
assert!(!f.is_supported());
}
assert!(matches!(decode(b"FOVb\x02\0\x02\0"), Err(RawError::Unsupported(w)) if w.contains("X3f")));
// too short for the CIFF signature
assert_eq!(probe(b"II\x1a\0\0\0HEAP"), None);
}
/// An IFD0 with a JPEG pointer but no `ImageWidth` / `ImageLength` (the Kodak KDC layout) is a raw
/// container with a preview; a TIFF reader would call it malformed.
#[test]
fn ifd0_without_dimensions_but_with_a_jpeg_is_a_preview_container() {
let jpeg: Vec<u8> = [&[0xffu8, 0xd8, 0xff, 0xc0, 0, 0x0b, 8, 0, 1, 0, 1, 1, 1, 0x11, 0][..], &[0x55; 40], &[0xff, 0xd9]].concat();
let off = 8 + 2 + 24 + 4;
let mut f = b"II*\0\x08\0\0\0\x02\0".to_vec();
for (tag, v) in [(513u16, off as u32), (514, jpeg.len() as u32)] {
f.extend_from_slice(&tag.to_le_bytes());
f.extend_from_slice(&[4, 0, 1, 0, 0, 0]);
f.extend_from_slice(&v.to_le_bytes());
}
f.extend_from_slice(&[0, 0, 0, 0]);
f.extend_from_slice(&jpeg);
assert_eq!(probe(&f), Some(RawFormat::OtherTiff));
assert!(matches!(decode(&f), Err(RawError::Unsupported(_))));
assert_eq!(embedded_preview(&f), Some(jpeg));
// no pointer and no sub-IFDs: just a broken TIFF
let g = write(&[IfdBuilder::new().with(t::MAKE, Value::Ascii("KODAK".into()))]);
assert_eq!(probe(&g), None);
}
/// A GoPro GPR (a DNG with VC-5 coded raw data, Compression 9) is refused by the full decode with
/// a reason that names the format, whatever its tile bytes hold; the header probe still
/// describes it (so it imports).
#[test]
fn gopro_vc5_dng_is_unsupported_with_a_clear_reason() {
let mut ifd = IfdBuilder::new();
ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0]));
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![32]));
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![16]));
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
ifd.set(t::PHOTOMETRIC, Value::Short(vec![32803]));
ifd.set(t::COMPRESSION, Value::Short(vec![9]));
ifd.set(t::DNG_VERSION, Value::Byte(vec![1, 4, 0, 0]));
ifd.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2]));
ifd.set(t::CFA_PATTERN_EP, Value::Byte(vec![0, 1, 1, 2]));
ifd.set_image(ImageData::Tiles { tile_width: 32, tile_height: 16, tiles: vec![vec![0x5au8; 400]] });
let bytes = write(&[ifd]);
assert_eq!(probe(&bytes), Some(RawFormat::Dng));
let want = "DNG compression 9 (GoPro VC-5) is not decoded yet";
let Err(RawError::Unsupported(why)) = decode(&bytes) else { panic!("expected Unsupported from decode") };
assert_eq!(why, want);
let info = probe_info(&bytes).expect("header probe describes the file");
assert_eq!((info.width, info.height), (32, 16));
}
/// A LinearRaw DNG in the layout of Lightroom's merges: a `w` × `h` main image (samples 1000) with a default
/// crop, and reduced copies (samples 2000 + their width) of the given sizes beside it.
fn linear_dng_with_copies(w: u32, h: u32, copies: &[(u32, u32)], main_tags: &[(u16, Value)]) -> Vec<u8> {
let linear = |subfile: u32, w: u32, h: u32, sample: u16| {
let mut ifd = IfdBuilder::new();
ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![subfile]));
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16, 16, 16]));
ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![3]));
ifd.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::LINEAR_RAW]));
ifd.set(t::COMPRESSION, Value::Short(vec![1]));
ifd.set(t::WHITE_LEVEL, Value::Long(vec![32768]));
let samples: Vec<u8> = std::iter::repeat_n(sample.to_le_bytes(), (w * h * 3) as usize).flatten().collect();
ifd.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![samples] });
ifd
};
let mut main = linear(0, w, h, 1000);
main.set(t::BLACK_LEVEL, Value::Short(vec![100]));
main.set(t::DEFAULT_CROP_ORIGIN, Value::Long(vec![4, 2]));
main.set(t::DEFAULT_CROP_SIZE, Value::Long(vec![w - 8, h - 4]));
for (tag, v) in main_tags {
main.set(*tag, v.clone());
}
let mut ifd0 = rgb_ifd(4, 2);
ifd0.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![1]));
ifd0.set(t::DNG_VERSION, Value::Byte(vec![1, 7, 0, 0]));
ifd0.set(t::MAKE, Value::Ascii("DJI".into()));
ifd0.set(t::COLOR_MATRIX_1, Value::SRational(vec![(1, 1), (0, 1), (0, 1), (0, 1), (1, 1), (0, 1), (0, 1), (0, 1), (1, 1)]));
ifd0.add_sub_ifd(main);
for &(cw, ch) in copies {
ifd0.add_sub_ifd(linear(1, cw, ch, 2000 + cw as u16));
}
write(&[ifd0])
}
/// A thumbnail of a Lightroom merge decodes the smallest reduced copy of its raw image that covers it, with the
/// main image's levels and its crop scaled; nothing covers it, or the main image has full-size opcodes: `None`.
#[test]
fn reduced_copies_of_a_linear_raw_dng_decode_at_their_size() {
let file = linear_dng_with_copies(64, 32, &[(32, 16), (16, 8)], &[]);
let full = decode(&file).unwrap();
assert_eq!((full.width, full.height, full.crop), (64, 32, Rect::new(4, 2, 56, 28)));
// a 12 px picture: the 16 px copy (its crop is 14 px long)
let small = decode_reduced(&file, 12).unwrap().expect("a copy covers 12 px");
assert_eq!((small.width, small.height), (16, 8));
assert_eq!(small.crop, Rect::new(1, 1, 14, 7));
let RawData::U16(v) = &small.data else { panic!("16-bit samples") };
assert!(v.iter().all(|&s| s == 2016), "the 16 px copy's samples");
assert_eq!((small.white.clone(), small.black.values.clone()), (vec![32768.0], vec![100.0]), "its own white, the main black");
assert_eq!((small.metadata.width, small.metadata.height), (Some(56), Some(28)), "the developed size is the photo's");
// 20 px: the 32 px copy (28 px); 40 px: none covers it
assert_eq!(decode_reduced(&file, 20).unwrap().map(|r| r.width), Some(32));
assert!(decode_reduced(&file, 40).unwrap().is_none());
// a different aspect ratio is not a copy of the image
assert!(decode_reduced(&linear_dng_with_copies(64, 32, &[(16, 16)], &[]), 8).unwrap().is_none());
// opcodes are in full-size pixels
let opcodes = linear_dng_with_copies(64, 32, &[(16, 8)], &[(t::OPCODE_LIST_3, Value::Undefined(vec![0, 0, 0, 0]))]);
assert!(decode_reduced(&opcodes, 8).unwrap().is_none());
// not a DNG
assert!(decode_reduced(&write(&[rgb_ifd(16, 8)]), 8).unwrap().is_none());
}
/// Compression 99 (not a registered TIFF value; Leaf MOS tiles) marks a raw even without a CFA tag.
#[test]
fn private_compression_99_is_a_raw() {
let mut ifd = rgb_ifd(16, 12);
ifd.set(t::COMPRESSION, Value::Short(vec![99]));
assert_eq!(probe(&write(&[ifd])), Some(RawFormat::OtherTiff));
}
/// Issue #1275: an uncompressed Leaf MOS (Aptus 22) is a full-size 16-bit grey IFD0 with no Make tag; only Leaf's
/// data block (`0x8606`) says it is a raw. It must not import as a greyscale image of its mosaic. The same IFD
/// without the block, or an RGB image carrying it, stays an ordinary image.
#[test]
fn uncompressed_leaf_mos_is_a_raw() {
let mosaic = |leaf_data: bool| {
let mut ifd = IfdBuilder::new();
ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0]));
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![16]));
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![12]));
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
ifd.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::BLACK_IS_ZERO]));
ifd.set(t::COMPRESSION, Value::Short(vec![1]));
ifd.set_image(ImageData::Tiles { tile_width: 16, tile_height: 12, tiles: vec![vec![0x40; 16 * 12 * 2]] });
if leaf_data {
ifd.set(LEAF_DATA, Value::Undefined(b"PKTS\0\0\0\x01".to_vec()));
}
ifd
};
let bytes = TiffWriter::new(lightcraft_tiff::ByteOrder::Big, false).write(&[mosaic(true)]).unwrap();
assert_eq!(probe(&bytes), Some(RawFormat::OtherTiff));
assert!(matches!(decode(&bytes), Err(RawError::Unsupported(_))));
assert!(matches!(probe_info(&bytes), Err(RawError::Unsupported(_))));
assert_eq!(probe(&write(&[mosaic(false)])), None, "a 16-bit grey TIFF");
let mut rgb = rgb_ifd(16, 12);
rgb.set(LEAF_DATA, Value::Undefined(b"PKTS\0\0\0\x01".to_vec()));
assert_eq!(probe(&write(&[rgb])), None, "a rendered RGB image that kept the block");
}
/// Issue #1275: a Hasselblad Lunar is a Sony NEX-7 ARW under Make HASSELBLAD. Sony's raw coding (compression 32767
/// or the tone-curve tag `0x7010`) sends it to the ARW reader; a Hasselblad file without it does not go there.
#[test]
fn hasselblad_make_with_sony_raw_coding_is_arw() {
let file = |make: &str, compression: u16, tone_curve: bool| {
let mut raw = IfdBuilder::new();
raw.set(t::IMAGE_WIDTH, Value::Long(vec![64]));
raw.set(t::IMAGE_LENGTH, Value::Long(vec![2]));
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![12]));
raw.set(t::COMPRESSION, Value::Short(vec![compression]));
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
if tone_curve {
raw.set(0x7010, Value::Short(vec![8000, 10400, 12900, 14100]));
}
raw.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![vec![0; 128]] });
let mut ifd0 = rgb_ifd(4, 3);
ifd0.set(t::MAKE, Value::Ascii(make.into()));
ifd0.set(t::MODEL, Value::Ascii("Lunar".into()));
ifd0.add_sub_ifd(raw);
write(&[ifd0])
};
assert_eq!(probe(&file("HASSELBLAD", 32767, false)), Some(RawFormat::Arw));
assert_eq!(probe(&file("Hasselblad", 1, true)), Some(RawFormat::Arw));
let lunar = decode(&file("HASSELBLAD", 32767, true)).unwrap();
assert_eq!((lunar.format, lunar.width, lunar.height, lunar.bits), (RawFormat::Arw, 64, 2, 14));
// a Hasselblad CFA image in another coding is not an ARW
assert_ne!(probe(&file("HASSELBLAD", 1, false)), Some(RawFormat::Arw));
assert_ne!(probe(&file("HASSELBLAD", 7, false)), Some(RawFormat::Arw));
}
// --- TIFF raws that describe their raw IFD the way DNG does but carry no DNG version tag ---
/// 8 x 6 samples, 16 bits, value `1000 + 100 y + x`.
fn cfa_samples() -> Vec<u16> {
(0..48u16).map(|i| 1000 + 100 * (i / 8) + i % 8).collect()
}
/// A small raw in the layout of the Hasselblad 3FR/FFF family: IFD0 is a reduced RGB image with the
/// colour tags, a SubIFD holds the full-resolution CFA image (no `CFAPattern`, DNG-style levels and
/// crop). `strip` is the CFA image's data and `compression` its coding.
fn plain_cfa_tiff(compression: u16, strip: Vec<u8>, pattern: Option<[u8; 4]>) -> Vec<u8> {
let mut raw = IfdBuilder::new();
raw.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0]));
raw.set(t::IMAGE_WIDTH, Value::Long(vec![8]));
raw.set(t::IMAGE_LENGTH, Value::Long(vec![6]));
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
raw.set(t::COMPRESSION, Value::Short(vec![compression]));
raw.set(t::BLACK_LEVEL, Value::Rational(vec![(256, 1)]));
raw.set(t::WHITE_LEVEL, Value::Long(vec![60000]));
raw.set(t::DEFAULT_CROP_ORIGIN, Value::Short(vec![2, 2]));
raw.set(t::DEFAULT_CROP_SIZE, Value::Short(vec![4, 2]));
if let Some(p) = pattern {
raw.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2]));
raw.set(t::CFA_PATTERN_EP, Value::Byte(p.to_vec()));
}
raw.set_image(ImageData::Strips { rows_per_strip: 6, strips: vec![strip] });
let mut ifd0 = rgb_ifd(4, 3);
ifd0.set(t::MAKE, Value::Ascii("Hasselblad".into()));
ifd0.set(t::COLOR_MATRIX_1, Value::SRational(vec![(5, 10), (-1, 10), (0, 10), (-5, 10), (12, 10), (3, 10), (-1, 10), (2, 10), (6, 10)]));
ifd0.set(t::AS_SHOT_NEUTRAL, Value::Rational(vec![(2, 5), (1, 1), (3, 5)]));
ifd0.add_sub_ifd(raw);
write(&[ifd0])
}
fn le_words(v: &[u16]) -> Vec<u8> {
v.iter().flat_map(|x| x.to_le_bytes()).collect()
}
/// Known answer: samples, black and white level, crop and colour come from the file's own tags; the
/// layout it does not state is RGGB.
#[test]
fn uncompressed_cfa_tiff_without_a_dng_version_decodes() {
let bytes = plain_cfa_tiff(1, le_words(&cfa_samples()), None);
assert_eq!(probe(&bytes), Some(RawFormat::CfaTiff));
let r = decode(&bytes).unwrap();
assert_eq!((r.format, r.width, r.height, r.bits), (RawFormat::CfaTiff, 8, 6, 16));
assert_eq!(r.data, RawData::U16(cfa_samples()));
assert_eq!(r.cfa.as_ref().unwrap().name(), "RGGB");
assert_eq!((r.black.mean(), r.white.clone()), (256.0, vec![60000.0]));
assert_eq!(r.crop, Rect::new(2, 2, 4, 2));
assert!(color::has_matrix(&r.color));
assert_eq!(r.color.as_shot_neutral, Some([0.4, 1.0, 0.6]));
// headers only: the same description without samples
let i = probe_info(&bytes).unwrap();
assert_eq!((i.format, i.cfa.as_ref().map(Cfa::name), i.crop), (RawFormat::CfaTiff, Some("RGGB".to_string()), Rect::new(2, 2, 4, 2)));
}
/// Without DNG's white-balance tag the file is left alone (a Kodak or Sinar TIFF with a CFA IFD, say).
#[test]
fn cfa_tiff_without_a_white_balance_tag_is_not_claimed() {
let mut raw = IfdBuilder::new();
raw.set(t::IMAGE_WIDTH, Value::Long(vec![8]));
raw.set(t::IMAGE_LENGTH, Value::Long(vec![6]));
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
raw.set(t::COMPRESSION, Value::Short(vec![1]));
raw.set_image(ImageData::Strips { rows_per_strip: 6, strips: vec![le_words(&cfa_samples())] });
let mut ifd0 = rgb_ifd(4, 3);
ifd0.add_sub_ifd(raw);
assert_eq!(probe(&write(&[ifd0])), Some(RawFormat::OtherTiff));
}
/// A stated `CFAPattern` wins over the RGGB default.
#[test]
fn cfa_tiff_keeps_a_stated_pattern() {
let bytes = plain_cfa_tiff(1, le_words(&cfa_samples()), Some([2, 1, 1, 0]));
assert_eq!(decode(&bytes).unwrap().cfa.unwrap().name(), "BGGR");
}
/// The lossless-JPEG coding of the same layout goes through the lossless decoder (one strip for the
/// whole image, as the 3FR/FFF files have it).
#[test]
fn lossless_jpeg_cfa_tiff_decodes() {
let strip = ljpeg::encode(&cfa_samples(), 8, 6, 1, 16, 1, 0);
let bytes = plain_cfa_tiff(7, strip, None);
assert_eq!(probe(&bytes), Some(RawFormat::CfaTiff));
let r = decode(&bytes).unwrap();
assert_eq!(r.data, RawData::U16(cfa_samples()));
assert_eq!(r.cfa.unwrap().name(), "RGGB");
}
/// Hasselblad's own lossless-JPEG coding (selection value 8, as in FFF and most compressed 3FR files) of the
/// same layout decodes: the file is read like the uncompressed one.
#[test]
fn selection_value_8_cfa_tiff_decodes() {
let strip = ljpeg::tests::encode_hasselblad(&cfa_samples(), 8, 6);
let bytes = plain_cfa_tiff(7, strip, None);
assert_eq!(probe(&bytes), Some(RawFormat::CfaTiff));
let r = decode(&bytes).unwrap();
assert_eq!((r.format, r.width, r.height), (RawFormat::CfaTiff, 8, 6));
assert_eq!(r.data, RawData::U16(cfa_samples()));
assert_eq!(r.cfa.unwrap().name(), "RGGB");
assert_eq!(probe_info(&bytes).unwrap().format, RawFormat::CfaTiff);
}
/// A lossless-JPEG scan whose predictor selection value is neither 0 to 7 (T.81) nor 8 (Hasselblad) is not a
/// coding the lossless decoder knows: the file stays an undecodable raw, with that as the reason, and keeps the
/// stand-in preview an unsupported container gets.
#[test]
fn cfa_tiff_with_an_unknown_predictor_stays_undecodable() {
let mut strip = ljpeg::encode(&cfa_samples(), 8, 6, 1, 16, 1, 0);
let sos = strip.windows(2).position(|w| w == [0xff, 0xda]).unwrap();
let ss = sos + 2 + 2 + 1 + 2;
assert_eq!(strip[ss], 1, "selection value byte");
strip[ss] = 9;
assert!(matches!(ljpeg::decode(&strip, 1 << 20), Err(RawError::Unsupported(w)) if w.contains("selection value 9")));
let bytes = plain_cfa_tiff(7, strip, None);
assert_eq!(probe(&bytes), Some(RawFormat::OtherTiff));
assert!(!RawFormat::OtherTiff.is_supported());
let Err(RawError::Unsupported(why)) = decode(&bytes) else { panic!("expected Unsupported") };
assert!(why.contains("lossless JPEG") && why.contains("selection value 9"), "{why}");
}
/// Other codings of a CFA IFD (here Deflate) are not claimed by the plain reader.
#[test]
fn cfa_tiff_with_another_coding_is_not_claimed() {
let bytes = plain_cfa_tiff(8, vec![0; 96], None);
assert_eq!(probe(&bytes), Some(RawFormat::OtherTiff));
}
/// A Samsung-branded body whose maker note has a Pentax layout is read by the Pentax reader; another
/// Samsung file stays with the Samsung format.
#[test]
fn samsung_make_with_a_pentax_maker_note_is_pef() {
let with_note = |note: &[u8], cfa: bool| {
let mut ifd = rgb_ifd(16, 12);
if cfa {
ifd.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
}
ifd.set(t::MAKE, Value::Ascii("SAMSUNG TECHWIN".into()));
let mut exif = IfdBuilder::new();
exif.set(t::MAKER_NOTE, Value::Undefined(note.to_vec()));
ifd.set_child(t::EXIF_IFD, exif);
write(&[ifd])
};
assert_eq!(probe(&with_note(b"AOC\0MM\0\x01\0\0\0\0\0\0", true)), Some(RawFormat::Pef));
assert_eq!(probe(&with_note(b"PENTAX \0MM\0\0\0\0\0\0\0", true)), Some(RawFormat::Pef));
assert_eq!(probe(&with_note(b"STMN100\0\0\0\0\0\0\0\0\0", true)), Some(RawFormat::Srw));
// without raw evidence (an RGB image from such a body) neither reader claims it (#281)
assert_eq!(probe(&with_note(b"AOC\0MM\0\x01\0\0\0\0\0\0", false)), None);
assert_eq!(probe(&with_note(b"STMN100\0\0\0\0\0\0\0\0\0", false)), None);
}
#[test]
fn probe_rejects_junk() {
assert_eq!(probe(b""), None);
assert_eq!(probe(b"hello world, not a raw"), None);
assert_eq!(probe(b"\0\0\0\x18ftypcrx \0\0\0\x01"), Some(RawFormat::Cr3));
assert_eq!(probe(b"FUJIFILMCCD-RAW 0201"), Some(RawFormat::Raf));
assert!(decode(b"FUJIFILMCCD-RAW 0201").is_err());
assert_eq!(decode(b"junk"), Err(RawError::NotRaw));
}