mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-03 06:59:53 -06:00
refactor(lightcraft-meta): move focus.rs tests to crates/meta/src/focus/tests.rs
This commit is contained in:
+1
-466
@@ -281,469 +281,4 @@ fn area_used_name(v: u8) -> Option<&'static str> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lightcraft_tiff::{Entry, IfdBuilder, TiffWriter, Value};
|
||||
|
||||
fn encipher(p: u8) -> u8 {
|
||||
if p >= 249 { p } else { ((u32::from(p).pow(3)) % 249) as u8 }
|
||||
}
|
||||
|
||||
fn entry(tag: u16, value: Value) -> Entry {
|
||||
Entry { tag, value, offset: 0 }
|
||||
}
|
||||
|
||||
/// The α1 sample `_DSC6954.ARW`: AF-C, Wide, Face tracking, Animal Eye Tracking, 8640×5760 at (3226, 1980), 175×180.
|
||||
fn alpha1() -> Ifd {
|
||||
let mut block = vec![0u8; 0x40];
|
||||
block[0x16] = encipher(0x83);
|
||||
block[0x17] = encipher(20);
|
||||
Ifd {
|
||||
entries: vec![
|
||||
entry(FOCUS_MODE, Value::Byte(vec![3])),
|
||||
entry(AF_AREA_SETTING, Value::Byte(vec![0])),
|
||||
entry(AF_TRACKING, Value::Byte(vec![1])),
|
||||
entry(FOCUS_LOCATION, Value::Short(vec![8640, 5760, 3226, 1980])),
|
||||
entry(FOCUS_FRAME_SIZE, Value::Undefined(vec![0xaf, 0, 0xb4, 0, 1, 1])),
|
||||
entry(TAG_9402, Value::Undefined(block)),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn set(ifd: &mut Ifd, tag: u16, value: Value) {
|
||||
ifd.entries.retain(|e| e.tag != tag);
|
||||
ifd.entries.push(entry(tag, value));
|
||||
// IFD entries are kept sorted by tag (lookups rely on it)
|
||||
ifd.entries.sort_by_key(|e| e.tag);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cipher_matches_the_bytes_seen_in_files() {
|
||||
// stored → plain, as read from an α1 file's 0x9402 block next to ExifTool's decoded values
|
||||
assert_eq!(decipher(0x20), 20);
|
||||
assert_eq!(decipher(0x77), 0x83);
|
||||
assert_eq!(decipher(0xff), 0xff);
|
||||
for p in 0..=255u8 {
|
||||
assert_eq!(decipher(encipher(p)), p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_the_alpha1_sample() {
|
||||
let f = sony(&alpha1(), ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert!((f.rect.x0 - (3226.0 - 87.5) / 8640.0).abs() < 1e-12);
|
||||
assert!((f.rect.y1 - (1980.0 + 90.0) / 5760.0).abs() < 1e-12);
|
||||
assert_eq!(f.words(), vec!["AF-C", "Wide", "Animal Eye Tracking"]);
|
||||
let (cx, cy) = f.center();
|
||||
assert!((cx - 3226.0 / 8640.0).abs() < 1e-12 && (cy - 1980.0 / 5760.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_endian_frame_size() {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_FRAME_SIZE, Value::Undefined(vec![0, 0xaf, 0, 0xb4, 1, 1]));
|
||||
let f = sony(&ifd, ByteOrder::Big, Orientation::Normal).unwrap();
|
||||
assert!(((f.rect.x1 - f.rect.x0) * 8640.0 - 175.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follows_the_exif_orientation() {
|
||||
// a portrait shot (Rotate 270 CW, EXIF 8) still records landscape sensor coordinates
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(vec![8640, 5760, 864, 576]));
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::from_exif(8)).unwrap();
|
||||
let (cx, cy) = f.center();
|
||||
// left of the sensor (x 0.1) is the bottom of the upright photo; its top (y 0.1) the left
|
||||
assert!((cx - 0.1).abs() < 1e-9 && (cy - 0.9).abs() < 1e-9, "{cx} {cy}");
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::from_exif(6)).unwrap();
|
||||
let (cx, cy) = f.center();
|
||||
assert!((cx - 0.9).abs() < 1e-9 && (cy - 0.1).abs() < 1e-9, "{cx} {cy}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_real_point() {
|
||||
// manual focus
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_MODE, Value::Byte(vec![0]));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
// the block says Manual (its focus mode agreeing)
|
||||
let mut ifd = alpha1();
|
||||
let mut block = vec![0u8; 0x18];
|
||||
block[0x16] = encipher(3);
|
||||
block[0x17] = encipher(255);
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(block));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
// Multi at the exact centre
|
||||
let mut ifd = alpha1();
|
||||
let mut block = vec![0u8; 0x18];
|
||||
block[0x16] = encipher(3);
|
||||
block[0x17] = encipher(0);
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(block));
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(vec![8640, 5760, 4320, 2880]));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
// …and without the block: Wide, tracking off
|
||||
ifd.entries.retain(|e| e.tag != TAG_9402);
|
||||
set(&mut ifd, AF_TRACKING, Value::Byte(vec![0]));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
// missing location, zero frame, centre outside it
|
||||
let mut ifd = alpha1();
|
||||
ifd.entries.retain(|e| e.tag != FOCUS_LOCATION);
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
for loc in [vec![0, 5760, 0, 0], vec![8640, 0, 1, 0], vec![8640, 5760, 9000, 10], vec![8640, 5760, 10, 6000], vec![8640, 5760, 10]] {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(loc.clone()));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none(), "{loc:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_centred_spot_is_a_real_point() {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, AF_AREA_SETTING, Value::Byte(vec![3]));
|
||||
set(&mut ifd, AF_TRACKING, Value::Byte(vec![0]));
|
||||
let mut block = vec![0u8; 0x18];
|
||||
block[0x16] = encipher(2);
|
||||
block[0x17] = encipher(3);
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(block));
|
||||
set(&mut ifd, FOCUS_MODE, Value::Byte(vec![2]));
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(vec![8640, 5760, 4320, 2880]));
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert_eq!(f.words(), vec!["AF-S", "Flexible Spot"]);
|
||||
let (cx, cy) = f.center();
|
||||
assert!((cx - 0.5).abs() < 1e-12 && (cy - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_that_disagrees_is_ignored() {
|
||||
// another body's block layout: its "focus mode" is not the plain tag's, so its area isn't trusted
|
||||
let mut ifd = alpha1();
|
||||
let mut block = vec![0u8; 0x18];
|
||||
block[0x16] = encipher(7);
|
||||
block[0x17] = encipher(255);
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(block));
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert_eq!(f.words(), vec!["AF-C", "Wide", "Face tracking"]);
|
||||
// a block too short to hold the bytes is ignored the same way
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(vec![1, 2, 3]));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_garbage_frame_size_gets_a_small_box_and_edges_are_clipped() {
|
||||
for fs in [
|
||||
Value::Undefined(vec![0, 0, 0, 0, 0, 0]),
|
||||
Value::Undefined(vec![1]),
|
||||
Value::Short(vec![5]),
|
||||
Value::Undefined(vec![0xaf, 0, 0xb4, 0, 0, 0]),
|
||||
] {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_FRAME_SIZE, fs);
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert!(((f.rect.y1 - f.rect.y0) - DEFAULT_SIDE).abs() < 1e-9);
|
||||
}
|
||||
// a frame reaching past the corner is clipped to the photo
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(vec![8640, 5760, 0, 5760]));
|
||||
set(&mut ifd, FOCUS_FRAME_SIZE, Value::Undefined(vec![0xff, 0xff, 0xff, 0xff, 1, 1]));
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert_eq!(f.rect, Rect { x0: 0.0, y0: 0.0, x1: 1.0, y1: 1.0 });
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert!(f.rect.x0 >= 0.0 && f.rect.y1 <= 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_types_and_extreme_values_never_panic() {
|
||||
let values = [
|
||||
Value::Ascii("x".into()),
|
||||
Value::Byte(vec![]),
|
||||
Value::SShort(vec![-1, -1, -1, -1]),
|
||||
Value::Long(vec![u32::MAX; 4]),
|
||||
Value::Double(vec![f64::NAN; 4]),
|
||||
Value::Undefined(vec![0xff; 300]),
|
||||
];
|
||||
for v in &values {
|
||||
for tag in [FOCUS_MODE, AF_AREA_SETTING, AF_TRACKING, FOCUS_LOCATION, FOCUS_FRAME_SIZE, TAG_9402] {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, tag, v.clone());
|
||||
if let Some(f) = sony(&ifd, ByteOrder::Little, Orientation::Normal) {
|
||||
assert!(f.rect.x0 >= 0.0 && f.rect.x1 <= 1.0 && f.rect.x0 < f.rect.x1);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(FocusPoint { rect: Rect { x0: f64::NAN, y0: 0.0, x1: 1.0, y1: 1.0 }, mode: None, area: None, tracking: None }.sanitized().is_none());
|
||||
assert!(FocusPoint { rect: Rect { x0: 2.0, y0: 2.0, x1: 3.0, y1: 3.0 }, mode: None, area: None, tracking: None }.sanitized().is_none());
|
||||
}
|
||||
|
||||
/// A whole TIFF with a `SONY DSC` maker note holding `ifd`'s entries, written for real.
|
||||
fn sony_file(make: &str, model: &str, orientation: u16, note_entries: &[(u16, Value)]) -> Vec<u8> {
|
||||
sony_file_with(make, model, orientation, note_entries, None)
|
||||
}
|
||||
|
||||
/// [`sony_file`], with the `0x9402` block's value placed `far` bytes past the end of the TIFF
|
||||
/// (the gap filled with "sample data") when `far` is given.
|
||||
fn sony_file_with(make: &str, model: &str, orientation: u16, note_entries: &[(u16, Value)], far: Option<usize>) -> Vec<u8> {
|
||||
let order = ByteOrder::Little;
|
||||
// the note: 12-byte header, then an IFD whose out-of-line values are offset from the TIFF header,
|
||||
// which we only know after writing; write once to learn where the note lands, then rebuild
|
||||
let build = |note_at: u64, end: u64| -> (Vec<u8>, Vec<u8>) {
|
||||
let mut note = b"SONY DSC \0\0\0".to_vec();
|
||||
let ifd_len = 2 + note_entries.len() * 12 + 4;
|
||||
let mut tail: Vec<u8> = Vec::new();
|
||||
let mut outside: Vec<u8> = Vec::new();
|
||||
order.put_u16(&mut note, note_entries.len() as u16);
|
||||
for (tag, v) in note_entries {
|
||||
let raw = lightcraft_tiff::writer::encode(order, v);
|
||||
order.put_u16(&mut note, *tag);
|
||||
order.put_u16(&mut note, v.field_type() as u16);
|
||||
order.put_u32(&mut note, v.count() as u32);
|
||||
if raw.len() <= 4 {
|
||||
let mut r = raw.clone();
|
||||
r.resize(4, 0);
|
||||
note.extend(r);
|
||||
} else if let (Some(gap), TAG_9402) = (far, *tag) {
|
||||
order.put_u32(&mut note, (end + gap as u64) as u32);
|
||||
outside = raw;
|
||||
} else {
|
||||
order.put_u32(&mut note, (note_at + 12 + ifd_len as u64 + tail.len() as u64) as u32);
|
||||
tail.extend(raw);
|
||||
if tail.len() % 2 == 1 {
|
||||
tail.push(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
order.put_u32(&mut note, 0);
|
||||
note.extend(tail);
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(t::MAKER_NOTE, Value::Undefined(note));
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(t::MAKE, Value::Ascii(make.into()));
|
||||
ifd0.set(t::MODEL, Value::Ascii(model.into()));
|
||||
ifd0.set(t::ORIENTATION, Value::Short(vec![orientation]));
|
||||
ifd0.set_child(t::EXIF_IFD, exif);
|
||||
(TiffWriter { order, bigtiff: false }.write(&[ifd0]).unwrap(), outside)
|
||||
};
|
||||
let (first, _) = build(0, 0);
|
||||
let at = Tiff::parse(&first).unwrap().exif().unwrap().get(t::MAKER_NOTE).unwrap().offset;
|
||||
let (mut file, outside) = build(at, first.len() as u64);
|
||||
assert_eq!(file.len(), first.len());
|
||||
assert_eq!(Tiff::parse(&file).unwrap().exif().unwrap().get(t::MAKER_NOTE).unwrap().offset, at);
|
||||
if let Some(gap) = far {
|
||||
file.extend((0..gap).map(|i| (i * 13 % 241) as u8));
|
||||
file.extend(outside);
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
fn alpha1_entries() -> Vec<(u16, Value)> {
|
||||
alpha1().entries.into_iter().map(|e| (e.tag, e.value)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_exif_finds_it_through_the_maker_note_and_orientation() {
|
||||
let file = sony_file("SONY", "ILCE-1", 8, &alpha1_entries());
|
||||
let m = crate::read_exif(&file);
|
||||
let f = m.focus.expect("focus point");
|
||||
assert_eq!(f.words(), vec!["AF-C", "Wide", "Animal Eye Tracking"]);
|
||||
// EXIF 8: sensor x becomes 1 - upright y
|
||||
let (cx, cy) = f.center();
|
||||
assert!((cy - (1.0 - 3226.0 / 8640.0)).abs() < 1e-9 && (cx - 1980.0 / 5760.0).abs() < 1e-9, "{cx} {cy}");
|
||||
// an unconfirmed body or another maker: nothing
|
||||
assert!(crate::read_exif(&sony_file("SONY", "ILCE-7M4", 1, &alpha1_entries())).focus.is_none());
|
||||
assert!(crate::read_exif(&sony_file("Canon", "ILCE-1", 1, &alpha1_entries())).focus.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_and_garbage_maker_notes_never_panic() {
|
||||
let file = sony_file("SONY", "ILCE-1", 1, &alpha1_entries());
|
||||
let note_at = Tiff::parse(&file).unwrap().exif().unwrap().get(t::MAKER_NOTE).unwrap().offset as usize;
|
||||
for cut in (0..file.len()).step_by(7) {
|
||||
let _ = crate::read_exif(&file[..cut]);
|
||||
}
|
||||
let mut seed = 0x2545_f491_u32;
|
||||
for _ in 0..300 {
|
||||
let mut f = file.clone();
|
||||
for _ in 0..8 {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
let i = note_at + (seed as usize % (file.len() - note_at));
|
||||
f[i] = (seed >> 8) as u8;
|
||||
}
|
||||
if let Some(fp) = crate::read_exif(&f).focus {
|
||||
assert!(fp.rect.x0 >= 0.0 && fp.rect.x1 <= 1.0 && fp.rect.y0 >= 0.0 && fp.rect.y1 <= 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A whole-file read and a header-only read of the same file agree, and the header read reads a
|
||||
/// small part of it (the sample data after the metadata is never touched).
|
||||
#[test]
|
||||
fn header_read_matches_the_whole_file_and_reads_little() {
|
||||
let read = |file: &[u8]| {
|
||||
let mut asked = 0u64;
|
||||
let r = read_header(file.len() as u64, &mut |off, buf: &mut [u8]| {
|
||||
asked += buf.len() as u64;
|
||||
let s = off as usize;
|
||||
buf.copy_from_slice(&file[s..s + buf.len()]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(r.bytes_read, asked);
|
||||
r
|
||||
};
|
||||
// a raw: the metadata, then 8 MB of "samples"
|
||||
let mut file = sony_file("SONY", "ILCE-1", 8, &alpha1_entries());
|
||||
let meta_len = file.len();
|
||||
file.extend((0..8_000_000u32).map(|i| (i * 31 % 251) as u8));
|
||||
let whole = crate::extract(&file).focus;
|
||||
assert!(whole.is_some());
|
||||
let r = read(&file);
|
||||
assert_eq!(r.focus, whole);
|
||||
assert!(r.bytes_read <= HEADER_PREFIX && r.bytes_read < file.len() as u64 / 100, "{} of {}", r.bytes_read, file.len());
|
||||
assert!(meta_len < HEADER_PREFIX as usize);
|
||||
// the area-mode block placed 3 MB further on: found in a further round, still a small part
|
||||
let mut far = sony_file_with("SONY", "ILCE-1", 1, &alpha1_entries(), Some(3_000_000));
|
||||
far.extend((0..4_000_000u32).map(|i| (i * 7 % 253) as u8));
|
||||
let whole = crate::extract(&far).focus;
|
||||
assert!(whole.is_some());
|
||||
let r = read(&far);
|
||||
assert_eq!(r.focus, whole);
|
||||
assert!(r.bytes_read < 64 * 1024, "{}", r.bytes_read);
|
||||
// a JPEG: the Exif segment after another segment
|
||||
let exif = sony_file("SONY", "ILCE-1", 6, &alpha1_entries());
|
||||
let mut jpeg = vec![0xff, 0xd8, 0xff, 0xe0, 0, 16];
|
||||
jpeg.extend_from_slice(b"JFIF\0\x01\x01\0\0\x01\0\x01\0\0");
|
||||
jpeg.extend_from_slice(&[0xff, 0xe1]);
|
||||
jpeg.extend_from_slice(&((exif.len() + 8) as u16).to_be_bytes());
|
||||
jpeg.extend_from_slice(b"Exif\0\0");
|
||||
jpeg.extend_from_slice(&exif);
|
||||
jpeg.extend_from_slice(&[0xff, 0xda, 0, 2]);
|
||||
jpeg.extend(std::iter::repeat_n(0x55u8, 3_000_000));
|
||||
let whole = crate::extract(&jpeg).focus;
|
||||
assert!(whole.is_some());
|
||||
let r = read(&jpeg);
|
||||
assert_eq!(r.focus, whole);
|
||||
assert!(r.bytes_read <= HEADER_PREFIX);
|
||||
// unsupported bodies agree too (no point either way)
|
||||
let other = sony_file("SONY", "ILCE-7M4", 1, &alpha1_entries());
|
||||
assert_eq!(read(&other).focus, None);
|
||||
}
|
||||
|
||||
/// Every truncation and corruption of a file gives the header read the whole-file read's answer
|
||||
/// (or an error when the file is shorter than it said), never a panic.
|
||||
#[test]
|
||||
fn header_read_of_damaged_files_agrees_or_fails_cleanly() {
|
||||
let file = sony_file("SONY", "ILCE-1", 1, &alpha1_entries());
|
||||
for cut in (0..file.len()).step_by(5) {
|
||||
let part = &file[..cut];
|
||||
let r = read_header(part.len() as u64, &mut |off, buf: &mut [u8]| {
|
||||
let s = off as usize;
|
||||
buf.copy_from_slice(part.get(s..s + buf.len()).ok_or_else(|| std::io::Error::from(std::io::ErrorKind::UnexpectedEof))?);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(r.focus, crate::extract(part).focus, "cut at {cut}");
|
||||
}
|
||||
let mut seed = 0x9e37_79b9_u32;
|
||||
for _ in 0..400 {
|
||||
let mut f = file.clone();
|
||||
for _ in 0..6 {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
let i = seed as usize % f.len();
|
||||
f[i] = (seed >> 8) as u8;
|
||||
}
|
||||
let r = read_header(f.len() as u64, &mut |off, buf: &mut [u8]| {
|
||||
let s = off as usize;
|
||||
buf.copy_from_slice(&f[s..s + buf.len()]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(r.focus, crate::extract(&f).focus);
|
||||
}
|
||||
// a reader told the file is longer than it is (replaced since it was measured): an error
|
||||
let r = read_header(file.len() as u64 + 100_000, &mut |off, buf: &mut [u8]| {
|
||||
let s = off as usize;
|
||||
buf.copy_from_slice(file.get(s..s + buf.len()).ok_or_else(|| std::io::Error::from(std::io::ErrorKind::UnexpectedEof))?);
|
||||
Ok(())
|
||||
});
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_synthetic_arw_is_laid_out_as_described() {
|
||||
let f = synthetic_arw(3, 10);
|
||||
assert_eq!(f.len(), 260);
|
||||
let p = crate::extract(&f).focus.expect("a point");
|
||||
assert_eq!(p.words(), vec!["AF-C", "Wide", "Animal Eye Tracking"]);
|
||||
assert!(crate::extract(&synthetic_arw(0, 0)).focus.is_none(), "manual focus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cameras_that_can_have_a_point() {
|
||||
assert!(camera_supported("SONY ILCE-1") && camera_supported("Sony ILCE-7SM3 "));
|
||||
for c in ["SONY ILCE-7M4", "Canon EOS R5", "", "SONY", "ILCE-1", "SON"] {
|
||||
assert!(!camera_supported(c), "{c}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Opt-in: decode real α1 / A7S III files and compare with the values ExifTool (run as a black box)
|
||||
/// reports for them. `LIGHTCRAFT_FOCUS_SAMPLES=<dir>` is the photo folder the paths below are relative
|
||||
/// to; skipped without it. No user file is part of the repository.
|
||||
#[test]
|
||||
fn real_sony_files_match_exiftool() {
|
||||
let Ok(dir) = std::env::var("LIGHTCRAFT_FOCUS_SAMPLES") else { return };
|
||||
let dir = std::path::Path::new(&dir);
|
||||
type Want = Option<([f64; 4], [f64; 2], &'static [&'static str])>;
|
||||
// (file, EXIF orientation, ExifTool's FocusLocation and FocusFrameSize, the words we show; None = no point)
|
||||
let cases: [(&str, u16, Want); 8] = [
|
||||
// α1, AF-C, Wide, Face tracking, AFAreaMode Animal Eye Tracking
|
||||
(
|
||||
"2023/11-november/17th/_DSC6954.ARW",
|
||||
1,
|
||||
Some(([8640.0, 5760.0, 3226.0, 1980.0], [175.0, 180.0], &["AF-C", "Wide", "Animal Eye Tracking"])),
|
||||
),
|
||||
(
|
||||
"2023/11-november/17th/_DSC6894.ARW",
|
||||
1,
|
||||
Some(([8640.0, 5760.0, 4725.0, 1860.0], [135.0, 132.0], &["AF-C", "Wide", "Animal Eye Tracking"])),
|
||||
),
|
||||
// α1, manual focus (the location 4441 3060 is where the frame was parked)
|
||||
("2023/11-november/15th/_DSC6613.ARW", 1, None),
|
||||
// α1 portrait, AF-S, Wide, Multi at the exact centre 4320 2880
|
||||
("2022/01-january/9th/_DSC6079.ARW", 8, None),
|
||||
// α1 APS-C crop mode portrait, AF-C, Wide, Lock On AF, AFAreaMode Tracking
|
||||
("2022/12-december/16th/_DSC1575.ARW", 8, Some(([5616.0, 3744.0, 4071.0, 1848.0], [280.0, 280.0], &["AF-C", "Wide", "Tracking"]))),
|
||||
// A7S III portrait, AF-A, Wide, Face tracking, Animal Eye Tracking
|
||||
(
|
||||
"2020/10-october/31st/DSC00065.ARW",
|
||||
8,
|
||||
Some(([4240.0, 2832.0, 1192.0, 1675.0], [278.0, 277.0], &["AF-A", "Wide", "Animal Eye Tracking"])),
|
||||
),
|
||||
// A7S III upside down, AF-C, a centred Flexible Spot: a real point
|
||||
("2021/11-november/7th/DSC05975.ARW", 3, Some(([4240.0, 2832.0, 2120.0, 1416.0], [251.0, 224.0], &["AF-C", "Flexible Spot"]))),
|
||||
// A7S III, manual focus
|
||||
("2021/06-june/12th/DSC06806.ARW", 1, None),
|
||||
];
|
||||
for (name, exif_orientation, want) in cases {
|
||||
let bytes = std::fs::read(dir.join(name)).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let got = crate::extract(&bytes).focus;
|
||||
match (want, got) {
|
||||
(None, None) => {}
|
||||
(Some((loc, size, words)), Some(f)) => {
|
||||
let [w, h, x, y] = loc;
|
||||
let sensor =
|
||||
Rect { x0: (x - size[0] / 2.0) / w, y0: (y - size[1] / 2.0) / h, x1: (x + size[0] / 2.0) / w, y1: (y + size[1] / 2.0) / h };
|
||||
let r = Orientation::from_exif(exif_orientation).map_norm_rect(sensor);
|
||||
for (a, b) in [(f.rect.x0, r.x0), (f.rect.y0, r.y0), (f.rect.x1, r.x1), (f.rect.y1, r.y1)] {
|
||||
assert!((a - b).abs() < 1e-9, "{name}: {:?} vs {r:?}", f.rect);
|
||||
}
|
||||
assert_eq!(f.words(), words, "{name}");
|
||||
}
|
||||
(w, g) => panic!("{name}: want {w:?}, got {g:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
use super::*;
|
||||
use lightcraft_tiff::{Entry, IfdBuilder, TiffWriter, Value};
|
||||
|
||||
fn encipher(p: u8) -> u8 {
|
||||
if p >= 249 { p } else { ((u32::from(p).pow(3)) % 249) as u8 }
|
||||
}
|
||||
|
||||
fn entry(tag: u16, value: Value) -> Entry {
|
||||
Entry { tag, value, offset: 0 }
|
||||
}
|
||||
|
||||
/// The α1 sample `_DSC6954.ARW`: AF-C, Wide, Face tracking, Animal Eye Tracking, 8640×5760 at (3226, 1980), 175×180.
|
||||
fn alpha1() -> Ifd {
|
||||
let mut block = vec![0u8; 0x40];
|
||||
block[0x16] = encipher(0x83);
|
||||
block[0x17] = encipher(20);
|
||||
Ifd {
|
||||
entries: vec![
|
||||
entry(FOCUS_MODE, Value::Byte(vec![3])),
|
||||
entry(AF_AREA_SETTING, Value::Byte(vec![0])),
|
||||
entry(AF_TRACKING, Value::Byte(vec![1])),
|
||||
entry(FOCUS_LOCATION, Value::Short(vec![8640, 5760, 3226, 1980])),
|
||||
entry(FOCUS_FRAME_SIZE, Value::Undefined(vec![0xaf, 0, 0xb4, 0, 1, 1])),
|
||||
entry(TAG_9402, Value::Undefined(block)),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn set(ifd: &mut Ifd, tag: u16, value: Value) {
|
||||
ifd.entries.retain(|e| e.tag != tag);
|
||||
ifd.entries.push(entry(tag, value));
|
||||
// IFD entries are kept sorted by tag (lookups rely on it)
|
||||
ifd.entries.sort_by_key(|e| e.tag);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cipher_matches_the_bytes_seen_in_files() {
|
||||
// stored → plain, as read from an α1 file's 0x9402 block next to ExifTool's decoded values
|
||||
assert_eq!(decipher(0x20), 20);
|
||||
assert_eq!(decipher(0x77), 0x83);
|
||||
assert_eq!(decipher(0xff), 0xff);
|
||||
for p in 0..=255u8 {
|
||||
assert_eq!(decipher(encipher(p)), p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_the_alpha1_sample() {
|
||||
let f = sony(&alpha1(), ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert!((f.rect.x0 - (3226.0 - 87.5) / 8640.0).abs() < 1e-12);
|
||||
assert!((f.rect.y1 - (1980.0 + 90.0) / 5760.0).abs() < 1e-12);
|
||||
assert_eq!(f.words(), vec!["AF-C", "Wide", "Animal Eye Tracking"]);
|
||||
let (cx, cy) = f.center();
|
||||
assert!((cx - 3226.0 / 8640.0).abs() < 1e-12 && (cy - 1980.0 / 5760.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_endian_frame_size() {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_FRAME_SIZE, Value::Undefined(vec![0, 0xaf, 0, 0xb4, 1, 1]));
|
||||
let f = sony(&ifd, ByteOrder::Big, Orientation::Normal).unwrap();
|
||||
assert!(((f.rect.x1 - f.rect.x0) * 8640.0 - 175.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follows_the_exif_orientation() {
|
||||
// a portrait shot (Rotate 270 CW, EXIF 8) still records landscape sensor coordinates
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(vec![8640, 5760, 864, 576]));
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::from_exif(8)).unwrap();
|
||||
let (cx, cy) = f.center();
|
||||
// left of the sensor (x 0.1) is the bottom of the upright photo; its top (y 0.1) the left
|
||||
assert!((cx - 0.1).abs() < 1e-9 && (cy - 0.9).abs() < 1e-9, "{cx} {cy}");
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::from_exif(6)).unwrap();
|
||||
let (cx, cy) = f.center();
|
||||
assert!((cx - 0.9).abs() < 1e-9 && (cy - 0.1).abs() < 1e-9, "{cx} {cy}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_real_point() {
|
||||
// manual focus
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_MODE, Value::Byte(vec![0]));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
// the block says Manual (its focus mode agreeing)
|
||||
let mut ifd = alpha1();
|
||||
let mut block = vec![0u8; 0x18];
|
||||
block[0x16] = encipher(3);
|
||||
block[0x17] = encipher(255);
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(block));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
// Multi at the exact centre
|
||||
let mut ifd = alpha1();
|
||||
let mut block = vec![0u8; 0x18];
|
||||
block[0x16] = encipher(3);
|
||||
block[0x17] = encipher(0);
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(block));
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(vec![8640, 5760, 4320, 2880]));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
// …and without the block: Wide, tracking off
|
||||
ifd.entries.retain(|e| e.tag != TAG_9402);
|
||||
set(&mut ifd, AF_TRACKING, Value::Byte(vec![0]));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
// missing location, zero frame, centre outside it
|
||||
let mut ifd = alpha1();
|
||||
ifd.entries.retain(|e| e.tag != FOCUS_LOCATION);
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none());
|
||||
for loc in [vec![0, 5760, 0, 0], vec![8640, 0, 1, 0], vec![8640, 5760, 9000, 10], vec![8640, 5760, 10, 6000], vec![8640, 5760, 10]] {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(loc.clone()));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_none(), "{loc:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_centred_spot_is_a_real_point() {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, AF_AREA_SETTING, Value::Byte(vec![3]));
|
||||
set(&mut ifd, AF_TRACKING, Value::Byte(vec![0]));
|
||||
let mut block = vec![0u8; 0x18];
|
||||
block[0x16] = encipher(2);
|
||||
block[0x17] = encipher(3);
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(block));
|
||||
set(&mut ifd, FOCUS_MODE, Value::Byte(vec![2]));
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(vec![8640, 5760, 4320, 2880]));
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert_eq!(f.words(), vec!["AF-S", "Flexible Spot"]);
|
||||
let (cx, cy) = f.center();
|
||||
assert!((cx - 0.5).abs() < 1e-12 && (cy - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_that_disagrees_is_ignored() {
|
||||
// another body's block layout: its "focus mode" is not the plain tag's, so its area isn't trusted
|
||||
let mut ifd = alpha1();
|
||||
let mut block = vec![0u8; 0x18];
|
||||
block[0x16] = encipher(7);
|
||||
block[0x17] = encipher(255);
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(block));
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert_eq!(f.words(), vec!["AF-C", "Wide", "Face tracking"]);
|
||||
// a block too short to hold the bytes is ignored the same way
|
||||
set(&mut ifd, TAG_9402, Value::Undefined(vec![1, 2, 3]));
|
||||
assert!(sony(&ifd, ByteOrder::Little, Orientation::Normal).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_garbage_frame_size_gets_a_small_box_and_edges_are_clipped() {
|
||||
for fs in [
|
||||
Value::Undefined(vec![0, 0, 0, 0, 0, 0]),
|
||||
Value::Undefined(vec![1]),
|
||||
Value::Short(vec![5]),
|
||||
Value::Undefined(vec![0xaf, 0, 0xb4, 0, 0, 0]),
|
||||
] {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_FRAME_SIZE, fs);
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert!(((f.rect.y1 - f.rect.y0) - DEFAULT_SIDE).abs() < 1e-9);
|
||||
}
|
||||
// a frame reaching past the corner is clipped to the photo
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, FOCUS_LOCATION, Value::Short(vec![8640, 5760, 0, 5760]));
|
||||
set(&mut ifd, FOCUS_FRAME_SIZE, Value::Undefined(vec![0xff, 0xff, 0xff, 0xff, 1, 1]));
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert_eq!(f.rect, Rect { x0: 0.0, y0: 0.0, x1: 1.0, y1: 1.0 });
|
||||
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
|
||||
assert!(f.rect.x0 >= 0.0 && f.rect.y1 <= 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_types_and_extreme_values_never_panic() {
|
||||
let values = [
|
||||
Value::Ascii("x".into()),
|
||||
Value::Byte(vec![]),
|
||||
Value::SShort(vec![-1, -1, -1, -1]),
|
||||
Value::Long(vec![u32::MAX; 4]),
|
||||
Value::Double(vec![f64::NAN; 4]),
|
||||
Value::Undefined(vec![0xff; 300]),
|
||||
];
|
||||
for v in &values {
|
||||
for tag in [FOCUS_MODE, AF_AREA_SETTING, AF_TRACKING, FOCUS_LOCATION, FOCUS_FRAME_SIZE, TAG_9402] {
|
||||
let mut ifd = alpha1();
|
||||
set(&mut ifd, tag, v.clone());
|
||||
if let Some(f) = sony(&ifd, ByteOrder::Little, Orientation::Normal) {
|
||||
assert!(f.rect.x0 >= 0.0 && f.rect.x1 <= 1.0 && f.rect.x0 < f.rect.x1);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(FocusPoint { rect: Rect { x0: f64::NAN, y0: 0.0, x1: 1.0, y1: 1.0 }, mode: None, area: None, tracking: None }.sanitized().is_none());
|
||||
assert!(FocusPoint { rect: Rect { x0: 2.0, y0: 2.0, x1: 3.0, y1: 3.0 }, mode: None, area: None, tracking: None }.sanitized().is_none());
|
||||
}
|
||||
|
||||
/// A whole TIFF with a `SONY DSC` maker note holding `ifd`'s entries, written for real.
|
||||
fn sony_file(make: &str, model: &str, orientation: u16, note_entries: &[(u16, Value)]) -> Vec<u8> {
|
||||
sony_file_with(make, model, orientation, note_entries, None)
|
||||
}
|
||||
|
||||
/// [`sony_file`], with the `0x9402` block's value placed `far` bytes past the end of the TIFF
|
||||
/// (the gap filled with "sample data") when `far` is given.
|
||||
fn sony_file_with(make: &str, model: &str, orientation: u16, note_entries: &[(u16, Value)], far: Option<usize>) -> Vec<u8> {
|
||||
let order = ByteOrder::Little;
|
||||
// the note: 12-byte header, then an IFD whose out-of-line values are offset from the TIFF header,
|
||||
// which we only know after writing; write once to learn where the note lands, then rebuild
|
||||
let build = |note_at: u64, end: u64| -> (Vec<u8>, Vec<u8>) {
|
||||
let mut note = b"SONY DSC \0\0\0".to_vec();
|
||||
let ifd_len = 2 + note_entries.len() * 12 + 4;
|
||||
let mut tail: Vec<u8> = Vec::new();
|
||||
let mut outside: Vec<u8> = Vec::new();
|
||||
order.put_u16(&mut note, note_entries.len() as u16);
|
||||
for (tag, v) in note_entries {
|
||||
let raw = lightcraft_tiff::writer::encode(order, v);
|
||||
order.put_u16(&mut note, *tag);
|
||||
order.put_u16(&mut note, v.field_type() as u16);
|
||||
order.put_u32(&mut note, v.count() as u32);
|
||||
if raw.len() <= 4 {
|
||||
let mut r = raw.clone();
|
||||
r.resize(4, 0);
|
||||
note.extend(r);
|
||||
} else if let (Some(gap), TAG_9402) = (far, *tag) {
|
||||
order.put_u32(&mut note, (end + gap as u64) as u32);
|
||||
outside = raw;
|
||||
} else {
|
||||
order.put_u32(&mut note, (note_at + 12 + ifd_len as u64 + tail.len() as u64) as u32);
|
||||
tail.extend(raw);
|
||||
if tail.len() % 2 == 1 {
|
||||
tail.push(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
order.put_u32(&mut note, 0);
|
||||
note.extend(tail);
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(t::MAKER_NOTE, Value::Undefined(note));
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(t::MAKE, Value::Ascii(make.into()));
|
||||
ifd0.set(t::MODEL, Value::Ascii(model.into()));
|
||||
ifd0.set(t::ORIENTATION, Value::Short(vec![orientation]));
|
||||
ifd0.set_child(t::EXIF_IFD, exif);
|
||||
(TiffWriter { order, bigtiff: false }.write(&[ifd0]).unwrap(), outside)
|
||||
};
|
||||
let (first, _) = build(0, 0);
|
||||
let at = Tiff::parse(&first).unwrap().exif().unwrap().get(t::MAKER_NOTE).unwrap().offset;
|
||||
let (mut file, outside) = build(at, first.len() as u64);
|
||||
assert_eq!(file.len(), first.len());
|
||||
assert_eq!(Tiff::parse(&file).unwrap().exif().unwrap().get(t::MAKER_NOTE).unwrap().offset, at);
|
||||
if let Some(gap) = far {
|
||||
file.extend((0..gap).map(|i| (i * 13 % 241) as u8));
|
||||
file.extend(outside);
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
fn alpha1_entries() -> Vec<(u16, Value)> {
|
||||
alpha1().entries.into_iter().map(|e| (e.tag, e.value)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_exif_finds_it_through_the_maker_note_and_orientation() {
|
||||
let file = sony_file("SONY", "ILCE-1", 8, &alpha1_entries());
|
||||
let m = crate::read_exif(&file);
|
||||
let f = m.focus.expect("focus point");
|
||||
assert_eq!(f.words(), vec!["AF-C", "Wide", "Animal Eye Tracking"]);
|
||||
// EXIF 8: sensor x becomes 1 - upright y
|
||||
let (cx, cy) = f.center();
|
||||
assert!((cy - (1.0 - 3226.0 / 8640.0)).abs() < 1e-9 && (cx - 1980.0 / 5760.0).abs() < 1e-9, "{cx} {cy}");
|
||||
// an unconfirmed body or another maker: nothing
|
||||
assert!(crate::read_exif(&sony_file("SONY", "ILCE-7M4", 1, &alpha1_entries())).focus.is_none());
|
||||
assert!(crate::read_exif(&sony_file("Canon", "ILCE-1", 1, &alpha1_entries())).focus.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_and_garbage_maker_notes_never_panic() {
|
||||
let file = sony_file("SONY", "ILCE-1", 1, &alpha1_entries());
|
||||
let note_at = Tiff::parse(&file).unwrap().exif().unwrap().get(t::MAKER_NOTE).unwrap().offset as usize;
|
||||
for cut in (0..file.len()).step_by(7) {
|
||||
let _ = crate::read_exif(&file[..cut]);
|
||||
}
|
||||
let mut seed = 0x2545_f491_u32;
|
||||
for _ in 0..300 {
|
||||
let mut f = file.clone();
|
||||
for _ in 0..8 {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
let i = note_at + (seed as usize % (file.len() - note_at));
|
||||
f[i] = (seed >> 8) as u8;
|
||||
}
|
||||
if let Some(fp) = crate::read_exif(&f).focus {
|
||||
assert!(fp.rect.x0 >= 0.0 && fp.rect.x1 <= 1.0 && fp.rect.y0 >= 0.0 && fp.rect.y1 <= 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A whole-file read and a header-only read of the same file agree, and the header read reads a
|
||||
/// small part of it (the sample data after the metadata is never touched).
|
||||
#[test]
|
||||
fn header_read_matches_the_whole_file_and_reads_little() {
|
||||
let read = |file: &[u8]| {
|
||||
let mut asked = 0u64;
|
||||
let r = read_header(file.len() as u64, &mut |off, buf: &mut [u8]| {
|
||||
asked += buf.len() as u64;
|
||||
let s = off as usize;
|
||||
buf.copy_from_slice(&file[s..s + buf.len()]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(r.bytes_read, asked);
|
||||
r
|
||||
};
|
||||
// a raw: the metadata, then 8 MB of "samples"
|
||||
let mut file = sony_file("SONY", "ILCE-1", 8, &alpha1_entries());
|
||||
let meta_len = file.len();
|
||||
file.extend((0..8_000_000u32).map(|i| (i * 31 % 251) as u8));
|
||||
let whole = crate::extract(&file).focus;
|
||||
assert!(whole.is_some());
|
||||
let r = read(&file);
|
||||
assert_eq!(r.focus, whole);
|
||||
assert!(r.bytes_read <= HEADER_PREFIX && r.bytes_read < file.len() as u64 / 100, "{} of {}", r.bytes_read, file.len());
|
||||
assert!(meta_len < HEADER_PREFIX as usize);
|
||||
// the area-mode block placed 3 MB further on: found in a further round, still a small part
|
||||
let mut far = sony_file_with("SONY", "ILCE-1", 1, &alpha1_entries(), Some(3_000_000));
|
||||
far.extend((0..4_000_000u32).map(|i| (i * 7 % 253) as u8));
|
||||
let whole = crate::extract(&far).focus;
|
||||
assert!(whole.is_some());
|
||||
let r = read(&far);
|
||||
assert_eq!(r.focus, whole);
|
||||
assert!(r.bytes_read < 64 * 1024, "{}", r.bytes_read);
|
||||
// a JPEG: the Exif segment after another segment
|
||||
let exif = sony_file("SONY", "ILCE-1", 6, &alpha1_entries());
|
||||
let mut jpeg = vec![0xff, 0xd8, 0xff, 0xe0, 0, 16];
|
||||
jpeg.extend_from_slice(b"JFIF\0\x01\x01\0\0\x01\0\x01\0\0");
|
||||
jpeg.extend_from_slice(&[0xff, 0xe1]);
|
||||
jpeg.extend_from_slice(&((exif.len() + 8) as u16).to_be_bytes());
|
||||
jpeg.extend_from_slice(b"Exif\0\0");
|
||||
jpeg.extend_from_slice(&exif);
|
||||
jpeg.extend_from_slice(&[0xff, 0xda, 0, 2]);
|
||||
jpeg.extend(std::iter::repeat_n(0x55u8, 3_000_000));
|
||||
let whole = crate::extract(&jpeg).focus;
|
||||
assert!(whole.is_some());
|
||||
let r = read(&jpeg);
|
||||
assert_eq!(r.focus, whole);
|
||||
assert!(r.bytes_read <= HEADER_PREFIX);
|
||||
// unsupported bodies agree too (no point either way)
|
||||
let other = sony_file("SONY", "ILCE-7M4", 1, &alpha1_entries());
|
||||
assert_eq!(read(&other).focus, None);
|
||||
}
|
||||
|
||||
/// Every truncation and corruption of a file gives the header read the whole-file read's answer
|
||||
/// (or an error when the file is shorter than it said), never a panic.
|
||||
#[test]
|
||||
fn header_read_of_damaged_files_agrees_or_fails_cleanly() {
|
||||
let file = sony_file("SONY", "ILCE-1", 1, &alpha1_entries());
|
||||
for cut in (0..file.len()).step_by(5) {
|
||||
let part = &file[..cut];
|
||||
let r = read_header(part.len() as u64, &mut |off, buf: &mut [u8]| {
|
||||
let s = off as usize;
|
||||
buf.copy_from_slice(part.get(s..s + buf.len()).ok_or_else(|| std::io::Error::from(std::io::ErrorKind::UnexpectedEof))?);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(r.focus, crate::extract(part).focus, "cut at {cut}");
|
||||
}
|
||||
let mut seed = 0x9e37_79b9_u32;
|
||||
for _ in 0..400 {
|
||||
let mut f = file.clone();
|
||||
for _ in 0..6 {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
let i = seed as usize % f.len();
|
||||
f[i] = (seed >> 8) as u8;
|
||||
}
|
||||
let r = read_header(f.len() as u64, &mut |off, buf: &mut [u8]| {
|
||||
let s = off as usize;
|
||||
buf.copy_from_slice(&f[s..s + buf.len()]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(r.focus, crate::extract(&f).focus);
|
||||
}
|
||||
// a reader told the file is longer than it is (replaced since it was measured): an error
|
||||
let r = read_header(file.len() as u64 + 100_000, &mut |off, buf: &mut [u8]| {
|
||||
let s = off as usize;
|
||||
buf.copy_from_slice(file.get(s..s + buf.len()).ok_or_else(|| std::io::Error::from(std::io::ErrorKind::UnexpectedEof))?);
|
||||
Ok(())
|
||||
});
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_synthetic_arw_is_laid_out_as_described() {
|
||||
let f = synthetic_arw(3, 10);
|
||||
assert_eq!(f.len(), 260);
|
||||
let p = crate::extract(&f).focus.expect("a point");
|
||||
assert_eq!(p.words(), vec!["AF-C", "Wide", "Animal Eye Tracking"]);
|
||||
assert!(crate::extract(&synthetic_arw(0, 0)).focus.is_none(), "manual focus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cameras_that_can_have_a_point() {
|
||||
assert!(camera_supported("SONY ILCE-1") && camera_supported("Sony ILCE-7SM3 "));
|
||||
for c in ["SONY ILCE-7M4", "Canon EOS R5", "", "SONY", "ILCE-1", "SON"] {
|
||||
assert!(!camera_supported(c), "{c}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Opt-in: decode real α1 / A7S III files and compare with the values ExifTool (run as a black box)
|
||||
/// reports for them. `LIGHTCRAFT_FOCUS_SAMPLES=<dir>` is the photo folder the paths below are relative
|
||||
/// to; skipped without it. No user file is part of the repository.
|
||||
#[test]
|
||||
fn real_sony_files_match_exiftool() {
|
||||
let Ok(dir) = std::env::var("LIGHTCRAFT_FOCUS_SAMPLES") else { return };
|
||||
let dir = std::path::Path::new(&dir);
|
||||
type Want = Option<([f64; 4], [f64; 2], &'static [&'static str])>;
|
||||
// (file, EXIF orientation, ExifTool's FocusLocation and FocusFrameSize, the words we show; None = no point)
|
||||
let cases: [(&str, u16, Want); 8] = [
|
||||
// α1, AF-C, Wide, Face tracking, AFAreaMode Animal Eye Tracking
|
||||
(
|
||||
"2023/11-november/17th/_DSC6954.ARW",
|
||||
1,
|
||||
Some(([8640.0, 5760.0, 3226.0, 1980.0], [175.0, 180.0], &["AF-C", "Wide", "Animal Eye Tracking"])),
|
||||
),
|
||||
(
|
||||
"2023/11-november/17th/_DSC6894.ARW",
|
||||
1,
|
||||
Some(([8640.0, 5760.0, 4725.0, 1860.0], [135.0, 132.0], &["AF-C", "Wide", "Animal Eye Tracking"])),
|
||||
),
|
||||
// α1, manual focus (the location 4441 3060 is where the frame was parked)
|
||||
("2023/11-november/15th/_DSC6613.ARW", 1, None),
|
||||
// α1 portrait, AF-S, Wide, Multi at the exact centre 4320 2880
|
||||
("2022/01-january/9th/_DSC6079.ARW", 8, None),
|
||||
// α1 APS-C crop mode portrait, AF-C, Wide, Lock On AF, AFAreaMode Tracking
|
||||
("2022/12-december/16th/_DSC1575.ARW", 8, Some(([5616.0, 3744.0, 4071.0, 1848.0], [280.0, 280.0], &["AF-C", "Wide", "Tracking"]))),
|
||||
// A7S III portrait, AF-A, Wide, Face tracking, Animal Eye Tracking
|
||||
(
|
||||
"2020/10-october/31st/DSC00065.ARW",
|
||||
8,
|
||||
Some(([4240.0, 2832.0, 1192.0, 1675.0], [278.0, 277.0], &["AF-A", "Wide", "Animal Eye Tracking"])),
|
||||
),
|
||||
// A7S III upside down, AF-C, a centred Flexible Spot: a real point
|
||||
("2021/11-november/7th/DSC05975.ARW", 3, Some(([4240.0, 2832.0, 2120.0, 1416.0], [251.0, 224.0], &["AF-C", "Flexible Spot"]))),
|
||||
// A7S III, manual focus
|
||||
("2021/06-june/12th/DSC06806.ARW", 1, None),
|
||||
];
|
||||
for (name, exif_orientation, want) in cases {
|
||||
let bytes = std::fs::read(dir.join(name)).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let got = crate::extract(&bytes).focus;
|
||||
match (want, got) {
|
||||
(None, None) => {}
|
||||
(Some((loc, size, words)), Some(f)) => {
|
||||
let [w, h, x, y] = loc;
|
||||
let sensor =
|
||||
Rect { x0: (x - size[0] / 2.0) / w, y0: (y - size[1] / 2.0) / h, x1: (x + size[0] / 2.0) / w, y1: (y + size[1] / 2.0) / h };
|
||||
let r = Orientation::from_exif(exif_orientation).map_norm_rect(sensor);
|
||||
for (a, b) in [(f.rect.x0, r.x0), (f.rect.y0, r.y0), (f.rect.x1, r.x1), (f.rect.y1, r.y1)] {
|
||||
assert!((a - b).abs() < 1e-9, "{name}: {:?} vs {r:?}", f.rect);
|
||||
}
|
||||
assert_eq!(f.words(), words, "{name}");
|
||||
}
|
||||
(w, g) => panic!("{name}: want {w:?}, got {g:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user