mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 03:27:14 -06:00
refactor(lightcraft-raw): move preview.rs tests to crates/raw/src/preview/tests.rs
This commit is contained in:
+1
-348
@@ -435,351 +435,4 @@ fn trim_eoi(s: &[u8]) -> &[u8] {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value};
|
||||
|
||||
fn fake_jpeg(n: usize) -> Vec<u8> {
|
||||
let mut j = vec![0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 8, 0, 1, 0, 1, 1, 1, 0x11, 0];
|
||||
j.extend(std::iter::repeat_n(0x55u8, n));
|
||||
j.extend_from_slice(&[0xff, 0xd9]);
|
||||
j
|
||||
}
|
||||
|
||||
/// Preview extraction only serves single-chunk JPEG-compressed IFDs and asks the `single_chunk` first:
|
||||
/// a TIFF whose strip array declares 100 000 one-row strips and no StripByteCounts must not build their
|
||||
/// descriptor vector, and of course finds no preview in it.
|
||||
#[test]
|
||||
fn a_multi_strip_jpeg_ifd_costs_no_descriptor_vector() {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![7]));
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![16]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![100_000]));
|
||||
ifd.set(t::ROWS_PER_STRIP, Value::Short(vec![1]));
|
||||
ifd.set(t::STRIP_OFFSETS, Value::Byte((0..100_000).map(|i| (i % 255 + 1) as u8).collect()));
|
||||
let file = TiffWriter::default().write(&[ifd]).unwrap();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || tx.send(embedded_preview(&file)));
|
||||
assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None));
|
||||
}
|
||||
|
||||
/// CR3 candidates claim overlapping extents: each block's first bytes double as a 40-byte jump marker so
|
||||
/// a walk jogs past the next header and reaches the end of the file, making every one of the 9000
|
||||
/// candidates walk its whole suffix before the marker-walk budget — quadratic without it. Nothing here
|
||||
/// is a JPEG (no frame header), so the answer is `None` either way.
|
||||
#[test]
|
||||
fn cr3_preview_boxes_stop_walking_when_the_budget_is_spent() {
|
||||
// one 256-byte block per candidate, all marker-aligned so walks chain through later blocks:
|
||||
// 0..4, size (itself a jump marker of 40 bytes); 4..8, "PRVW"; 20..24, the claimed length
|
||||
// (to the file end); 24..28, an SOI plus a marker jumping to the fill at 40; 64..256, a fill of
|
||||
// 4-byte markers (`ff 01 00 02`).
|
||||
let blocks = 9000usize;
|
||||
let total = blocks * 256;
|
||||
let mut f = Vec::with_capacity(total);
|
||||
for k in 0..blocks {
|
||||
let base = k * 256;
|
||||
f.extend_from_slice(&[0xff, 0x01, 0x00, 0x26, b'P', b'R', b'V', b'W', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
f.extend_from_slice(&u32::try_from(total - base - 24).unwrap().to_be_bytes());
|
||||
f.extend_from_slice(&[0xff, 0xd8, 0x00, 0x24, 0xff, 0x01, 0x00, 0x26, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
f.resize(base + 64, 0); // 40..64 filler: walks jump over it, so its bytes are free
|
||||
for _ in 0..48 {
|
||||
f.extend_from_slice(&[0xff, 0x01, 0x00, 0x02]);
|
||||
}
|
||||
}
|
||||
assert_eq!(f.len(), total);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || tx.send(cr3_preview_boxes(&f).map(|s| s.len())));
|
||||
assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None), "no PRVW candidate is a JPEG");
|
||||
}
|
||||
|
||||
/// A minimal RAF: the fixed header (JPEG offset/length at 84/88), the JPEG, then a raw tail the
|
||||
/// caller must not have to read. `jpeg_len` is written as declared, so a test can claim more than
|
||||
/// the JPEG holds the way a padded container does.
|
||||
fn raf_fixture(jpeg: &[u8], gap: usize, preamble: usize, declared_len: Option<usize>) -> Vec<u8> {
|
||||
let head = 160 + preamble;
|
||||
let jpeg_off = head;
|
||||
let mut f = vec![0u8; head];
|
||||
f[..16].copy_from_slice(b"FUJIFILMCCD-RAW ");
|
||||
f[84..88].copy_from_slice(&(jpeg_off as u32).to_be_bytes());
|
||||
f[88..92].copy_from_slice(&(declared_len.unwrap_or(jpeg.len()) as u32).to_be_bytes());
|
||||
// records directory and raw block are absent (offset 0), which `header` reads as "none"
|
||||
f.extend_from_slice(jpeg);
|
||||
f.extend(std::iter::repeat_n(0x33u8, gap));
|
||||
f
|
||||
}
|
||||
|
||||
/// A RAF's preview pointer sits at a fixed offset, so a header-sized prefix locates it and the
|
||||
/// caller reads just that range instead of the whole file — the whole point on a network share.
|
||||
#[test]
|
||||
fn bounded_preview_locates_a_rafs_jpeg_from_the_header_alone() {
|
||||
let jpeg = fake_jpeg(5000);
|
||||
let file = raf_fixture(&jpeg, 40_000, 0, None);
|
||||
// A prefix that covers the pointer's range: header + JPEG, and nothing of the tail.
|
||||
let prefix_len = 8192.min(file.len());
|
||||
let prefix = &file[..prefix_len];
|
||||
|
||||
let range = bounded_preview(prefix).expect("a RAF's preview is found from its header");
|
||||
assert_eq!(range.offset, 160);
|
||||
assert_eq!(range.window_len, jpeg.len() as u64);
|
||||
assert!(range.offset + range.window_len <= prefix_len as u64, "the prefix covers the whole range: {range:?} of {prefix_len}");
|
||||
assert!((range.offset + range.window_len) < file.len() as u64, "the range stops short of the tail: {range:?} of {}", file.len());
|
||||
|
||||
// What the caller reads, and what it holds afterwards: identical to the whole-file answer.
|
||||
let window = prefix.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the range lies inside the prefix");
|
||||
let bounded = embedded_preview_from_range(window, range).expect("the range holds the preview");
|
||||
assert_eq!(Some(bounded.clone()), embedded_preview(&file));
|
||||
assert!(bounded.ends_with(&[0xff, 0xd9]), "the preview ends at its EOI marker");
|
||||
|
||||
// A prefix that stops inside the declared extent is clamped by the container reader. The
|
||||
// caller then reads a short window; what comes back is the whole JPEG it holds (the EOI
|
||||
// marker is inside), never bytes past the window.
|
||||
let short = &file[..2000];
|
||||
let clipped = bounded_preview(short).expect("a pointer before the cut is still found");
|
||||
assert!(clipped.window_len < jpeg.len() as u64, "clamped: {clipped:?}");
|
||||
let window = short.get(clipped.offset as usize..).expect("inside");
|
||||
let preview = embedded_preview_from_range(window, clipped).expect("the bytes are a JPEG start");
|
||||
// No end-of-image marker inside the window: the bytes come back as they are, exactly as the
|
||||
// whole-file path returns an unclosed JPEG. The decoder refuses it later; nothing is invented.
|
||||
assert_eq!(preview, window, "an unclosed JPEG is handed on unchanged, never padded");
|
||||
assert!(preview.len() <= window.len(), "never past the window");
|
||||
}
|
||||
|
||||
/// The preview pointer may sit past the prefix (a long maker-note record before it), and a
|
||||
/// container may declare padding after the JPEG: both stay correct, and the padding is dropped.
|
||||
#[test]
|
||||
fn bounded_preview_handles_a_late_pointer_and_padding() {
|
||||
let jpeg = fake_jpeg(300);
|
||||
let late = raf_fixture(&jpeg, 0, 9000, None);
|
||||
let range = bounded_preview(&late).expect("the whole file is always a valid prefix");
|
||||
assert_eq!(range.offset, (160 + 9000) as u64);
|
||||
let padded = raf_fixture(&jpeg, 500, 0, Some(jpeg.len() + 500));
|
||||
let range = bounded_preview(&padded).expect("a padded extent is still located");
|
||||
assert_eq!(range.window_len, (jpeg.len() + 500) as u64);
|
||||
let window = padded.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the window is inside the file");
|
||||
let preview = embedded_preview_from_range(window, range).expect("the padding is dropped");
|
||||
assert_eq!(preview, embedded_preview(&padded).expect("the whole-file answer agrees"));
|
||||
assert!(preview.ends_with(&[0xff, 0xd9]));
|
||||
}
|
||||
|
||||
/// Without a usable header or pointer the caller must read the file whole: the answer is `None`,
|
||||
/// never a wrong range.
|
||||
#[test]
|
||||
fn bounded_preview_declines_what_the_header_cannot_answer() {
|
||||
let jpeg = fake_jpeg(64);
|
||||
let file = raf_fixture(&jpeg, 0, 0, None);
|
||||
assert_eq!(bounded_preview(&jpeg), None, "not a raw at all");
|
||||
assert_eq!(bounded_preview(b""), None);
|
||||
if let Some(short) = file.get(..64) {
|
||||
assert_eq!(bounded_preview(short), None, "a header cut short has not shown the pointer yet");
|
||||
}
|
||||
// a pointer that claims more than the file holds: `header` clamps the slice to the buffer,
|
||||
// so the range never runs past the end (no out-of-bounds read can follow from it)
|
||||
let mut lying = raf_fixture(&jpeg, 4096, 0, None);
|
||||
lying[88..92].copy_from_slice(&(u32::MAX / 2).to_be_bytes());
|
||||
let clamped = bounded_preview(&lying).expect("a pointer inside the file is still usable");
|
||||
assert!(clamped.offset + clamped.window_len <= lying.len() as u64, "clamped to the file: {clamped:?}");
|
||||
// a range the window cannot satisfy yields nothing
|
||||
assert_eq!(embedded_preview_from_range(&jpeg, PreviewRange { offset: 0, window_len: u64::MAX }), None);
|
||||
// a TIFF-based raw: its pointer tag lives in an IFD this prefix may cut short
|
||||
let mut tiff = b"II*\0\x08\0\0\0".to_vec();
|
||||
tiff.extend_from_slice(&[0u8; 64]);
|
||||
assert_eq!(bounded_preview(&tiff), None);
|
||||
}
|
||||
|
||||
/// A range that does not hold a JPEG yields nothing, rather than a truncated image handed on.
|
||||
#[test]
|
||||
fn a_range_that_does_not_hold_a_jpeg_yields_nothing() {
|
||||
let jpeg = fake_jpeg(200);
|
||||
let range = PreviewRange { offset: 0, window_len: jpeg.len() as u64 };
|
||||
assert_eq!(embedded_preview_from_range(&jpeg, range).as_deref(), Some(&jpeg[..]));
|
||||
assert_eq!(embedded_preview_from_range(&jpeg[..10], range), None, "window shorter than the range claims");
|
||||
let not_jpeg = vec![0u8; jpeg.len()];
|
||||
assert_eq!(embedded_preview_from_range(¬_jpeg, range), None, "not a JPEG");
|
||||
assert_eq!(embedded_preview_from_range(&[], PreviewRange { offset: 0, window_len: 0 }), None, "empty window");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picks_largest_dct_jpeg() {
|
||||
let small = fake_jpeg(10);
|
||||
let big = fake_jpeg(500);
|
||||
let lossless = crate::ljpeg::encode(&[1u16; 64], 8, 8, 1, 12, 1, 0);
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(t::COMPRESSION, Value::Short(vec![6]));
|
||||
ifd0.set(t::IMAGE_WIDTH, Value::Long(vec![1]));
|
||||
ifd0.set(t::IMAGE_LENGTH, Value::Long(vec![1]));
|
||||
ifd0.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![small.clone()] });
|
||||
let mut raw = IfdBuilder::new();
|
||||
raw.set(t::COMPRESSION, Value::Short(vec![7]));
|
||||
raw.set(t::IMAGE_WIDTH, Value::Long(vec![8]));
|
||||
raw.set(t::IMAGE_LENGTH, Value::Long(vec![8]));
|
||||
raw.set_image(ImageData::Strips { rows_per_strip: 8, strips: vec![lossless] });
|
||||
ifd0.add_sub_ifd(raw);
|
||||
let mut sub = IfdBuilder::new();
|
||||
sub.set(t::COMPRESSION, Value::Short(vec![7]));
|
||||
sub.set(t::IMAGE_WIDTH, Value::Long(vec![2]));
|
||||
sub.set(t::IMAGE_LENGTH, Value::Long(vec![2]));
|
||||
sub.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![1]));
|
||||
sub.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![big.clone()] });
|
||||
ifd0.add_sub_ifd(sub);
|
||||
let bytes = TiffWriter::default().write(&[ifd0]).unwrap();
|
||||
assert_eq!(embedded_preview(&bytes).unwrap(), big);
|
||||
assert!(embedded_preview(b"nope").is_none());
|
||||
// CR3: a PRVW box after the ftyp
|
||||
let mut cr3 = b"\0\0\0\x18ftypcrx \0\0\0\x01crx isom".to_vec();
|
||||
let j = fake_jpeg(300);
|
||||
cr3.extend_from_slice(&((24 + j.len()) as u32).to_be_bytes());
|
||||
cr3.extend_from_slice(b"PRVW\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01");
|
||||
cr3.extend_from_slice(&(j.len() as u32).to_be_bytes());
|
||||
cr3.extend_from_slice(&j);
|
||||
assert_eq!(embedded_preview(&cr3).unwrap(), j);
|
||||
for n in 0..cr3.len() {
|
||||
let _ = embedded_preview(&cr3[..n]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A JPEG like [`fake_jpeg`] whose frame header says `w` x `h` with `nc` components.
|
||||
fn sized_jpeg(w: u16, h: u16, nc: u8, n: usize) -> Vec<u8> {
|
||||
let mut j = vec![0xff, 0xd8, 0xff, 0xc0, 0x00, 8 + 3 * nc, 8];
|
||||
j.extend_from_slice(&h.to_be_bytes());
|
||||
j.extend_from_slice(&w.to_be_bytes());
|
||||
j.push(nc);
|
||||
for c in 0..nc {
|
||||
j.extend_from_slice(&[c + 1, 0x11, 0]);
|
||||
}
|
||||
j.extend(std::iter::repeat_n(0x55u8, n));
|
||||
j.extend_from_slice(&[0xff, 0xd9]);
|
||||
j
|
||||
}
|
||||
|
||||
/// A TIFF raw holding `jpegs` as JPEG-compressed strips: the first in IFD0, the others in sub-IFDs.
|
||||
fn tiff_with_jpegs(jpegs: &[&[u8]]) -> Vec<u8> {
|
||||
let ifd = |j: &[u8]| {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![6]));
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![1]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![1]));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![j.to_vec()] });
|
||||
ifd
|
||||
};
|
||||
let mut ifd0 = ifd(jpegs[0]);
|
||||
for j in &jpegs[1..] {
|
||||
ifd0.add_sub_ifd(ifd(j));
|
||||
}
|
||||
TiffWriter::default().write(&[ifd0]).unwrap()
|
||||
}
|
||||
|
||||
/// A stand-in decodes the smallest JPEG that covers its box and shows the same picture as the largest: not a
|
||||
/// thumbnail of another shape (a letterboxed one) or a grey mask, and the largest when no smaller one is big enough.
|
||||
#[test]
|
||||
fn a_stand_in_takes_the_smallest_covering_jpeg_of_the_same_shape() {
|
||||
let (big, small, boxed) = (sized_jpeg(9984, 6656, 3, 500), sized_jpeg(1616, 1080, 3, 50), sized_jpeg(160, 120, 3, 10));
|
||||
// a ProRAW DNG keeps grey semantic masks of the picture's shape as JPEGs too
|
||||
let mask = sized_jpeg(768, 512, 1, 20);
|
||||
let file = tiff_with_jpegs(&[&small, &big, &boxed, &mask]);
|
||||
assert_eq!(embedded_preview(&file).unwrap(), big, "unchanged: the largest");
|
||||
assert_eq!(embedded_preview_covering(&file, 1600).unwrap(), small, "1616 px covers a 1600 px box");
|
||||
assert_eq!(embedded_preview_covering(&file, 1616).unwrap(), small);
|
||||
assert_eq!(embedded_preview_covering(&file, 1617).unwrap(), big, "too small: the largest");
|
||||
assert_eq!(embedded_preview_covering(&file, u32::MAX).unwrap(), big);
|
||||
assert_eq!(embedded_preview_covering(&file, 100).unwrap(), small, "the 4:3 thumbnail and the grey mask are not this picture");
|
||||
// a small copy of the same shape (within 1 %) is taken
|
||||
let thumb = sized_jpeg(160, 107, 3, 10);
|
||||
let file = tiff_with_jpegs(&[&small, &big, &thumb]);
|
||||
assert_eq!(embedded_preview_covering(&file, 100).unwrap(), thumb);
|
||||
assert_eq!(embedded_preview_covering(&tiff_with_jpegs(&[&big]), 100).unwrap(), big, "one preview: that one");
|
||||
for n in (0..file.len()).step_by(7) {
|
||||
let _ = embedded_preview_covering(&file[..n], 100);
|
||||
}
|
||||
}
|
||||
|
||||
/// CR3 with a full-size JPEG track: that JPEG wins over the smaller `PRVW` box.
|
||||
#[test]
|
||||
fn cr3_prefers_the_full_size_jpeg_track() {
|
||||
let bx = |kind: &[u8; 4], body: &[u8]| -> Vec<u8> { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() };
|
||||
let full = |kind: &[u8; 4], body: &[u8]| bx(kind, &[&[0u8; 4][..], body].concat());
|
||||
let (small, big) = (fake_jpeg(300), fake_jpeg(3000));
|
||||
let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom");
|
||||
let mut prvw = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec();
|
||||
prvw.extend_from_slice(&(small.len() as u32).to_be_bytes());
|
||||
prvw.extend_from_slice(&small);
|
||||
file.extend(bx(b"PRVW", &prvw));
|
||||
// CRAW sample entry: 82 bytes, then a JPEG child box; one sample at `at`
|
||||
let mut craw = vec![0u8; 82];
|
||||
craw.extend(bx(b"JPEG", &[0; 4]));
|
||||
let at = 2048u64;
|
||||
let stsd = full(b"stsd", &[&1u32.to_be_bytes()[..], &bx(b"CRAW", &craw)].concat());
|
||||
let stsz = full(b"stsz", &[0u32.to_be_bytes(), 1u32.to_be_bytes(), (big.len() as u32).to_be_bytes()].concat());
|
||||
let co64 = full(b"co64", &[&1u32.to_be_bytes()[..], &at.to_be_bytes()].concat());
|
||||
let trak = bx(b"trak", &bx(b"mdia", &bx(b"minf", &bx(b"stbl", &[stsd, stsz, co64].concat()))));
|
||||
file.extend(bx(b"moov", &trak));
|
||||
file.resize(at as usize, 0);
|
||||
file.extend_from_slice(&big);
|
||||
assert_eq!(embedded_preview(&file).unwrap(), big);
|
||||
// a track pointing past the end falls back to PRVW
|
||||
file.truncate(at as usize + 100);
|
||||
assert_eq!(embedded_preview(&file).unwrap(), small);
|
||||
}
|
||||
|
||||
/// A structurally complete DCT JPEG (no real image data): `nc` components, `n` entropy bytes.
|
||||
fn scan_jpeg(nc: u8, n: usize) -> Vec<u8> {
|
||||
let mut j = vec![0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00];
|
||||
j.extend(std::iter::repeat_n(8u8, 64));
|
||||
j.extend_from_slice(&[0xff, 0xc0, 0x00, 8 + 3 * nc, 8, 0, 16, 0, 24, nc]);
|
||||
for c in 0..nc {
|
||||
j.extend_from_slice(&[c + 1, 0x11, 0]);
|
||||
}
|
||||
j.extend_from_slice(&[0xff, 0xda, 0x00, 6 + 2 * nc, nc]);
|
||||
for c in 0..nc {
|
||||
j.extend_from_slice(&[c + 1, 0]);
|
||||
}
|
||||
j.extend_from_slice(&[0, 63, 0]);
|
||||
j.extend(std::iter::repeat_n(0x55u8, n));
|
||||
j.extend_from_slice(&[0xff, 0x00, 0x12, 0xff, 0xd0, 0x34, 0xff, 0xd9]);
|
||||
j
|
||||
}
|
||||
|
||||
/// A crafted container full of JPEG-like starts (each walk running on through the ones after it)
|
||||
/// is scanned in linear time: the walks share a budget (without it this 40 MB file takes about a minute).
|
||||
#[test]
|
||||
fn a_scan_through_many_fake_jpegs_stays_linear() {
|
||||
let mut unit = vec![0u8, 0xd8, 0xff, 0xdb, 0x00, 0x02, 0xff, 0xda, 0x00, 0x02];
|
||||
unit.extend(std::iter::repeat_n(0x55u8, 190));
|
||||
let mut f = b"\0MRM\0\x01\0\0".to_vec();
|
||||
for _ in 0..200_000 {
|
||||
f.extend_from_slice(&unit);
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
assert_eq!(scan_for_jpeg(&f), None);
|
||||
assert!(t.elapsed() < std::time::Duration::from_secs(10), "{:?}", t.elapsed());
|
||||
}
|
||||
|
||||
/// Containers whose structure we do not walk give up their largest colour JPEG, found by scanning.
|
||||
#[test]
|
||||
fn scanned_containers_give_the_largest_colour_jpeg() {
|
||||
let (small, big, mosaic) = (scan_jpeg(3, 100), scan_jpeg(3, 900), scan_jpeg(1, 5000));
|
||||
let heads: [&[u8]; 3] = [b"II\x1a\0\0\0HEAPCCDR", b"\0MRM\0\x01\0\0", b"FOVb\x02\0\x02\0"];
|
||||
for head in heads {
|
||||
let mut f = head.to_vec();
|
||||
for part in [&small, &mosaic, &big] {
|
||||
f.extend_from_slice(&[0xff, 0xd8, 0xff, 0x00, 0x00]); // a stray SOI-like run: not a JPEG
|
||||
f.extend_from_slice(part);
|
||||
f.extend_from_slice(&[0u8; 7]);
|
||||
}
|
||||
// MRW decodes; its preview is still found by the scan
|
||||
assert!(crate::probe(&f).is_some_and(|p| !p.is_supported() || p == crate::RawFormat::Mrw), "{head:?}");
|
||||
assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..]), "{head:?}");
|
||||
for n in (0..f.len()).step_by(11) {
|
||||
let _ = embedded_preview(&f[..n]);
|
||||
}
|
||||
}
|
||||
// a preview whose first byte was overwritten (Minolta and Epson files store 00, 02 or ee there) is restored
|
||||
let mut f = b"\0MRM\0\x01\0\0".to_vec();
|
||||
f.push(0x02);
|
||||
f.extend_from_slice(&big[1..]);
|
||||
assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..]));
|
||||
// only a one-component JPEG, or none: nothing
|
||||
let mut f = b"FOVb\x02\0\x02\0".to_vec();
|
||||
f.extend_from_slice(&mosaic);
|
||||
assert_eq!(embedded_preview(&f), None);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
use super::*;
|
||||
use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value};
|
||||
|
||||
fn fake_jpeg(n: usize) -> Vec<u8> {
|
||||
let mut j = vec![0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 8, 0, 1, 0, 1, 1, 1, 0x11, 0];
|
||||
j.extend(std::iter::repeat_n(0x55u8, n));
|
||||
j.extend_from_slice(&[0xff, 0xd9]);
|
||||
j
|
||||
}
|
||||
|
||||
/// Preview extraction only serves single-chunk JPEG-compressed IFDs and asks the `single_chunk` first:
|
||||
/// a TIFF whose strip array declares 100 000 one-row strips and no StripByteCounts must not build their
|
||||
/// descriptor vector, and of course finds no preview in it.
|
||||
#[test]
|
||||
fn a_multi_strip_jpeg_ifd_costs_no_descriptor_vector() {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![7]));
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![16]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![100_000]));
|
||||
ifd.set(t::ROWS_PER_STRIP, Value::Short(vec![1]));
|
||||
ifd.set(t::STRIP_OFFSETS, Value::Byte((0..100_000).map(|i| (i % 255 + 1) as u8).collect()));
|
||||
let file = TiffWriter::default().write(&[ifd]).unwrap();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || tx.send(embedded_preview(&file)));
|
||||
assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None));
|
||||
}
|
||||
|
||||
/// CR3 candidates claim overlapping extents: each block's first bytes double as a 40-byte jump marker so
|
||||
/// a walk jogs past the next header and reaches the end of the file, making every one of the 9000
|
||||
/// candidates walk its whole suffix before the marker-walk budget — quadratic without it. Nothing here
|
||||
/// is a JPEG (no frame header), so the answer is `None` either way.
|
||||
#[test]
|
||||
fn cr3_preview_boxes_stop_walking_when_the_budget_is_spent() {
|
||||
// one 256-byte block per candidate, all marker-aligned so walks chain through later blocks:
|
||||
// 0..4, size (itself a jump marker of 40 bytes); 4..8, "PRVW"; 20..24, the claimed length
|
||||
// (to the file end); 24..28, an SOI plus a marker jumping to the fill at 40; 64..256, a fill of
|
||||
// 4-byte markers (`ff 01 00 02`).
|
||||
let blocks = 9000usize;
|
||||
let total = blocks * 256;
|
||||
let mut f = Vec::with_capacity(total);
|
||||
for k in 0..blocks {
|
||||
let base = k * 256;
|
||||
f.extend_from_slice(&[0xff, 0x01, 0x00, 0x26, b'P', b'R', b'V', b'W', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
f.extend_from_slice(&u32::try_from(total - base - 24).unwrap().to_be_bytes());
|
||||
f.extend_from_slice(&[0xff, 0xd8, 0x00, 0x24, 0xff, 0x01, 0x00, 0x26, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
f.resize(base + 64, 0); // 40..64 filler: walks jump over it, so its bytes are free
|
||||
for _ in 0..48 {
|
||||
f.extend_from_slice(&[0xff, 0x01, 0x00, 0x02]);
|
||||
}
|
||||
}
|
||||
assert_eq!(f.len(), total);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || tx.send(cr3_preview_boxes(&f).map(|s| s.len())));
|
||||
assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None), "no PRVW candidate is a JPEG");
|
||||
}
|
||||
|
||||
/// A minimal RAF: the fixed header (JPEG offset/length at 84/88), the JPEG, then a raw tail the
|
||||
/// caller must not have to read. `jpeg_len` is written as declared, so a test can claim more than
|
||||
/// the JPEG holds the way a padded container does.
|
||||
fn raf_fixture(jpeg: &[u8], gap: usize, preamble: usize, declared_len: Option<usize>) -> Vec<u8> {
|
||||
let head = 160 + preamble;
|
||||
let jpeg_off = head;
|
||||
let mut f = vec![0u8; head];
|
||||
f[..16].copy_from_slice(b"FUJIFILMCCD-RAW ");
|
||||
f[84..88].copy_from_slice(&(jpeg_off as u32).to_be_bytes());
|
||||
f[88..92].copy_from_slice(&(declared_len.unwrap_or(jpeg.len()) as u32).to_be_bytes());
|
||||
// records directory and raw block are absent (offset 0), which `header` reads as "none"
|
||||
f.extend_from_slice(jpeg);
|
||||
f.extend(std::iter::repeat_n(0x33u8, gap));
|
||||
f
|
||||
}
|
||||
|
||||
/// A RAF's preview pointer sits at a fixed offset, so a header-sized prefix locates it and the
|
||||
/// caller reads just that range instead of the whole file — the whole point on a network share.
|
||||
#[test]
|
||||
fn bounded_preview_locates_a_rafs_jpeg_from_the_header_alone() {
|
||||
let jpeg = fake_jpeg(5000);
|
||||
let file = raf_fixture(&jpeg, 40_000, 0, None);
|
||||
// A prefix that covers the pointer's range: header + JPEG, and nothing of the tail.
|
||||
let prefix_len = 8192.min(file.len());
|
||||
let prefix = &file[..prefix_len];
|
||||
|
||||
let range = bounded_preview(prefix).expect("a RAF's preview is found from its header");
|
||||
assert_eq!(range.offset, 160);
|
||||
assert_eq!(range.window_len, jpeg.len() as u64);
|
||||
assert!(range.offset + range.window_len <= prefix_len as u64, "the prefix covers the whole range: {range:?} of {prefix_len}");
|
||||
assert!((range.offset + range.window_len) < file.len() as u64, "the range stops short of the tail: {range:?} of {}", file.len());
|
||||
|
||||
// What the caller reads, and what it holds afterwards: identical to the whole-file answer.
|
||||
let window = prefix.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the range lies inside the prefix");
|
||||
let bounded = embedded_preview_from_range(window, range).expect("the range holds the preview");
|
||||
assert_eq!(Some(bounded.clone()), embedded_preview(&file));
|
||||
assert!(bounded.ends_with(&[0xff, 0xd9]), "the preview ends at its EOI marker");
|
||||
|
||||
// A prefix that stops inside the declared extent is clamped by the container reader. The
|
||||
// caller then reads a short window; what comes back is the whole JPEG it holds (the EOI
|
||||
// marker is inside), never bytes past the window.
|
||||
let short = &file[..2000];
|
||||
let clipped = bounded_preview(short).expect("a pointer before the cut is still found");
|
||||
assert!(clipped.window_len < jpeg.len() as u64, "clamped: {clipped:?}");
|
||||
let window = short.get(clipped.offset as usize..).expect("inside");
|
||||
let preview = embedded_preview_from_range(window, clipped).expect("the bytes are a JPEG start");
|
||||
// No end-of-image marker inside the window: the bytes come back as they are, exactly as the
|
||||
// whole-file path returns an unclosed JPEG. The decoder refuses it later; nothing is invented.
|
||||
assert_eq!(preview, window, "an unclosed JPEG is handed on unchanged, never padded");
|
||||
assert!(preview.len() <= window.len(), "never past the window");
|
||||
}
|
||||
|
||||
/// The preview pointer may sit past the prefix (a long maker-note record before it), and a
|
||||
/// container may declare padding after the JPEG: both stay correct, and the padding is dropped.
|
||||
#[test]
|
||||
fn bounded_preview_handles_a_late_pointer_and_padding() {
|
||||
let jpeg = fake_jpeg(300);
|
||||
let late = raf_fixture(&jpeg, 0, 9000, None);
|
||||
let range = bounded_preview(&late).expect("the whole file is always a valid prefix");
|
||||
assert_eq!(range.offset, (160 + 9000) as u64);
|
||||
let padded = raf_fixture(&jpeg, 500, 0, Some(jpeg.len() + 500));
|
||||
let range = bounded_preview(&padded).expect("a padded extent is still located");
|
||||
assert_eq!(range.window_len, (jpeg.len() + 500) as u64);
|
||||
let window = padded.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the window is inside the file");
|
||||
let preview = embedded_preview_from_range(window, range).expect("the padding is dropped");
|
||||
assert_eq!(preview, embedded_preview(&padded).expect("the whole-file answer agrees"));
|
||||
assert!(preview.ends_with(&[0xff, 0xd9]));
|
||||
}
|
||||
|
||||
/// Without a usable header or pointer the caller must read the file whole: the answer is `None`,
|
||||
/// never a wrong range.
|
||||
#[test]
|
||||
fn bounded_preview_declines_what_the_header_cannot_answer() {
|
||||
let jpeg = fake_jpeg(64);
|
||||
let file = raf_fixture(&jpeg, 0, 0, None);
|
||||
assert_eq!(bounded_preview(&jpeg), None, "not a raw at all");
|
||||
assert_eq!(bounded_preview(b""), None);
|
||||
if let Some(short) = file.get(..64) {
|
||||
assert_eq!(bounded_preview(short), None, "a header cut short has not shown the pointer yet");
|
||||
}
|
||||
// a pointer that claims more than the file holds: `header` clamps the slice to the buffer,
|
||||
// so the range never runs past the end (no out-of-bounds read can follow from it)
|
||||
let mut lying = raf_fixture(&jpeg, 4096, 0, None);
|
||||
lying[88..92].copy_from_slice(&(u32::MAX / 2).to_be_bytes());
|
||||
let clamped = bounded_preview(&lying).expect("a pointer inside the file is still usable");
|
||||
assert!(clamped.offset + clamped.window_len <= lying.len() as u64, "clamped to the file: {clamped:?}");
|
||||
// a range the window cannot satisfy yields nothing
|
||||
assert_eq!(embedded_preview_from_range(&jpeg, PreviewRange { offset: 0, window_len: u64::MAX }), None);
|
||||
// a TIFF-based raw: its pointer tag lives in an IFD this prefix may cut short
|
||||
let mut tiff = b"II*\0\x08\0\0\0".to_vec();
|
||||
tiff.extend_from_slice(&[0u8; 64]);
|
||||
assert_eq!(bounded_preview(&tiff), None);
|
||||
}
|
||||
|
||||
/// A range that does not hold a JPEG yields nothing, rather than a truncated image handed on.
|
||||
#[test]
|
||||
fn a_range_that_does_not_hold_a_jpeg_yields_nothing() {
|
||||
let jpeg = fake_jpeg(200);
|
||||
let range = PreviewRange { offset: 0, window_len: jpeg.len() as u64 };
|
||||
assert_eq!(embedded_preview_from_range(&jpeg, range).as_deref(), Some(&jpeg[..]));
|
||||
assert_eq!(embedded_preview_from_range(&jpeg[..10], range), None, "window shorter than the range claims");
|
||||
let not_jpeg = vec![0u8; jpeg.len()];
|
||||
assert_eq!(embedded_preview_from_range(¬_jpeg, range), None, "not a JPEG");
|
||||
assert_eq!(embedded_preview_from_range(&[], PreviewRange { offset: 0, window_len: 0 }), None, "empty window");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picks_largest_dct_jpeg() {
|
||||
let small = fake_jpeg(10);
|
||||
let big = fake_jpeg(500);
|
||||
let lossless = crate::ljpeg::encode(&[1u16; 64], 8, 8, 1, 12, 1, 0);
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(t::COMPRESSION, Value::Short(vec![6]));
|
||||
ifd0.set(t::IMAGE_WIDTH, Value::Long(vec![1]));
|
||||
ifd0.set(t::IMAGE_LENGTH, Value::Long(vec![1]));
|
||||
ifd0.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![small.clone()] });
|
||||
let mut raw = IfdBuilder::new();
|
||||
raw.set(t::COMPRESSION, Value::Short(vec![7]));
|
||||
raw.set(t::IMAGE_WIDTH, Value::Long(vec![8]));
|
||||
raw.set(t::IMAGE_LENGTH, Value::Long(vec![8]));
|
||||
raw.set_image(ImageData::Strips { rows_per_strip: 8, strips: vec![lossless] });
|
||||
ifd0.add_sub_ifd(raw);
|
||||
let mut sub = IfdBuilder::new();
|
||||
sub.set(t::COMPRESSION, Value::Short(vec![7]));
|
||||
sub.set(t::IMAGE_WIDTH, Value::Long(vec![2]));
|
||||
sub.set(t::IMAGE_LENGTH, Value::Long(vec![2]));
|
||||
sub.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![1]));
|
||||
sub.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![big.clone()] });
|
||||
ifd0.add_sub_ifd(sub);
|
||||
let bytes = TiffWriter::default().write(&[ifd0]).unwrap();
|
||||
assert_eq!(embedded_preview(&bytes).unwrap(), big);
|
||||
assert!(embedded_preview(b"nope").is_none());
|
||||
// CR3: a PRVW box after the ftyp
|
||||
let mut cr3 = b"\0\0\0\x18ftypcrx \0\0\0\x01crx isom".to_vec();
|
||||
let j = fake_jpeg(300);
|
||||
cr3.extend_from_slice(&((24 + j.len()) as u32).to_be_bytes());
|
||||
cr3.extend_from_slice(b"PRVW\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01");
|
||||
cr3.extend_from_slice(&(j.len() as u32).to_be_bytes());
|
||||
cr3.extend_from_slice(&j);
|
||||
assert_eq!(embedded_preview(&cr3).unwrap(), j);
|
||||
for n in 0..cr3.len() {
|
||||
let _ = embedded_preview(&cr3[..n]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A JPEG like [`fake_jpeg`] whose frame header says `w` x `h` with `nc` components.
|
||||
fn sized_jpeg(w: u16, h: u16, nc: u8, n: usize) -> Vec<u8> {
|
||||
let mut j = vec![0xff, 0xd8, 0xff, 0xc0, 0x00, 8 + 3 * nc, 8];
|
||||
j.extend_from_slice(&h.to_be_bytes());
|
||||
j.extend_from_slice(&w.to_be_bytes());
|
||||
j.push(nc);
|
||||
for c in 0..nc {
|
||||
j.extend_from_slice(&[c + 1, 0x11, 0]);
|
||||
}
|
||||
j.extend(std::iter::repeat_n(0x55u8, n));
|
||||
j.extend_from_slice(&[0xff, 0xd9]);
|
||||
j
|
||||
}
|
||||
|
||||
/// A TIFF raw holding `jpegs` as JPEG-compressed strips: the first in IFD0, the others in sub-IFDs.
|
||||
fn tiff_with_jpegs(jpegs: &[&[u8]]) -> Vec<u8> {
|
||||
let ifd = |j: &[u8]| {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![6]));
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![1]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![1]));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![j.to_vec()] });
|
||||
ifd
|
||||
};
|
||||
let mut ifd0 = ifd(jpegs[0]);
|
||||
for j in &jpegs[1..] {
|
||||
ifd0.add_sub_ifd(ifd(j));
|
||||
}
|
||||
TiffWriter::default().write(&[ifd0]).unwrap()
|
||||
}
|
||||
|
||||
/// A stand-in decodes the smallest JPEG that covers its box and shows the same picture as the largest: not a
|
||||
/// thumbnail of another shape (a letterboxed one) or a grey mask, and the largest when no smaller one is big enough.
|
||||
#[test]
|
||||
fn a_stand_in_takes_the_smallest_covering_jpeg_of_the_same_shape() {
|
||||
let (big, small, boxed) = (sized_jpeg(9984, 6656, 3, 500), sized_jpeg(1616, 1080, 3, 50), sized_jpeg(160, 120, 3, 10));
|
||||
// a ProRAW DNG keeps grey semantic masks of the picture's shape as JPEGs too
|
||||
let mask = sized_jpeg(768, 512, 1, 20);
|
||||
let file = tiff_with_jpegs(&[&small, &big, &boxed, &mask]);
|
||||
assert_eq!(embedded_preview(&file).unwrap(), big, "unchanged: the largest");
|
||||
assert_eq!(embedded_preview_covering(&file, 1600).unwrap(), small, "1616 px covers a 1600 px box");
|
||||
assert_eq!(embedded_preview_covering(&file, 1616).unwrap(), small);
|
||||
assert_eq!(embedded_preview_covering(&file, 1617).unwrap(), big, "too small: the largest");
|
||||
assert_eq!(embedded_preview_covering(&file, u32::MAX).unwrap(), big);
|
||||
assert_eq!(embedded_preview_covering(&file, 100).unwrap(), small, "the 4:3 thumbnail and the grey mask are not this picture");
|
||||
// a small copy of the same shape (within 1 %) is taken
|
||||
let thumb = sized_jpeg(160, 107, 3, 10);
|
||||
let file = tiff_with_jpegs(&[&small, &big, &thumb]);
|
||||
assert_eq!(embedded_preview_covering(&file, 100).unwrap(), thumb);
|
||||
assert_eq!(embedded_preview_covering(&tiff_with_jpegs(&[&big]), 100).unwrap(), big, "one preview: that one");
|
||||
for n in (0..file.len()).step_by(7) {
|
||||
let _ = embedded_preview_covering(&file[..n], 100);
|
||||
}
|
||||
}
|
||||
|
||||
/// CR3 with a full-size JPEG track: that JPEG wins over the smaller `PRVW` box.
|
||||
#[test]
|
||||
fn cr3_prefers_the_full_size_jpeg_track() {
|
||||
let bx = |kind: &[u8; 4], body: &[u8]| -> Vec<u8> { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() };
|
||||
let full = |kind: &[u8; 4], body: &[u8]| bx(kind, &[&[0u8; 4][..], body].concat());
|
||||
let (small, big) = (fake_jpeg(300), fake_jpeg(3000));
|
||||
let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom");
|
||||
let mut prvw = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec();
|
||||
prvw.extend_from_slice(&(small.len() as u32).to_be_bytes());
|
||||
prvw.extend_from_slice(&small);
|
||||
file.extend(bx(b"PRVW", &prvw));
|
||||
// CRAW sample entry: 82 bytes, then a JPEG child box; one sample at `at`
|
||||
let mut craw = vec![0u8; 82];
|
||||
craw.extend(bx(b"JPEG", &[0; 4]));
|
||||
let at = 2048u64;
|
||||
let stsd = full(b"stsd", &[&1u32.to_be_bytes()[..], &bx(b"CRAW", &craw)].concat());
|
||||
let stsz = full(b"stsz", &[0u32.to_be_bytes(), 1u32.to_be_bytes(), (big.len() as u32).to_be_bytes()].concat());
|
||||
let co64 = full(b"co64", &[&1u32.to_be_bytes()[..], &at.to_be_bytes()].concat());
|
||||
let trak = bx(b"trak", &bx(b"mdia", &bx(b"minf", &bx(b"stbl", &[stsd, stsz, co64].concat()))));
|
||||
file.extend(bx(b"moov", &trak));
|
||||
file.resize(at as usize, 0);
|
||||
file.extend_from_slice(&big);
|
||||
assert_eq!(embedded_preview(&file).unwrap(), big);
|
||||
// a track pointing past the end falls back to PRVW
|
||||
file.truncate(at as usize + 100);
|
||||
assert_eq!(embedded_preview(&file).unwrap(), small);
|
||||
}
|
||||
|
||||
/// A structurally complete DCT JPEG (no real image data): `nc` components, `n` entropy bytes.
|
||||
fn scan_jpeg(nc: u8, n: usize) -> Vec<u8> {
|
||||
let mut j = vec![0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00];
|
||||
j.extend(std::iter::repeat_n(8u8, 64));
|
||||
j.extend_from_slice(&[0xff, 0xc0, 0x00, 8 + 3 * nc, 8, 0, 16, 0, 24, nc]);
|
||||
for c in 0..nc {
|
||||
j.extend_from_slice(&[c + 1, 0x11, 0]);
|
||||
}
|
||||
j.extend_from_slice(&[0xff, 0xda, 0x00, 6 + 2 * nc, nc]);
|
||||
for c in 0..nc {
|
||||
j.extend_from_slice(&[c + 1, 0]);
|
||||
}
|
||||
j.extend_from_slice(&[0, 63, 0]);
|
||||
j.extend(std::iter::repeat_n(0x55u8, n));
|
||||
j.extend_from_slice(&[0xff, 0x00, 0x12, 0xff, 0xd0, 0x34, 0xff, 0xd9]);
|
||||
j
|
||||
}
|
||||
|
||||
/// A crafted container full of JPEG-like starts (each walk running on through the ones after it)
|
||||
/// is scanned in linear time: the walks share a budget (without it this 40 MB file takes about a minute).
|
||||
#[test]
|
||||
fn a_scan_through_many_fake_jpegs_stays_linear() {
|
||||
let mut unit = vec![0u8, 0xd8, 0xff, 0xdb, 0x00, 0x02, 0xff, 0xda, 0x00, 0x02];
|
||||
unit.extend(std::iter::repeat_n(0x55u8, 190));
|
||||
let mut f = b"\0MRM\0\x01\0\0".to_vec();
|
||||
for _ in 0..200_000 {
|
||||
f.extend_from_slice(&unit);
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
assert_eq!(scan_for_jpeg(&f), None);
|
||||
assert!(t.elapsed() < std::time::Duration::from_secs(10), "{:?}", t.elapsed());
|
||||
}
|
||||
|
||||
/// Containers whose structure we do not walk give up their largest colour JPEG, found by scanning.
|
||||
#[test]
|
||||
fn scanned_containers_give_the_largest_colour_jpeg() {
|
||||
let (small, big, mosaic) = (scan_jpeg(3, 100), scan_jpeg(3, 900), scan_jpeg(1, 5000));
|
||||
let heads: [&[u8]; 3] = [b"II\x1a\0\0\0HEAPCCDR", b"\0MRM\0\x01\0\0", b"FOVb\x02\0\x02\0"];
|
||||
for head in heads {
|
||||
let mut f = head.to_vec();
|
||||
for part in [&small, &mosaic, &big] {
|
||||
f.extend_from_slice(&[0xff, 0xd8, 0xff, 0x00, 0x00]); // a stray SOI-like run: not a JPEG
|
||||
f.extend_from_slice(part);
|
||||
f.extend_from_slice(&[0u8; 7]);
|
||||
}
|
||||
// MRW decodes; its preview is still found by the scan
|
||||
assert!(crate::probe(&f).is_some_and(|p| !p.is_supported() || p == crate::RawFormat::Mrw), "{head:?}");
|
||||
assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..]), "{head:?}");
|
||||
for n in (0..f.len()).step_by(11) {
|
||||
let _ = embedded_preview(&f[..n]);
|
||||
}
|
||||
}
|
||||
// a preview whose first byte was overwritten (Minolta and Epson files store 00, 02 or ee there) is restored
|
||||
let mut f = b"\0MRM\0\x01\0\0".to_vec();
|
||||
f.push(0x02);
|
||||
f.extend_from_slice(&big[1..]);
|
||||
assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..]));
|
||||
// only a one-component JPEG, or none: nothing
|
||||
let mut f = b"FOVb\x02\0\x02\0".to_vec();
|
||||
f.extend_from_slice(&mosaic);
|
||||
assert_eq!(embedded_preview(&f), None);
|
||||
}
|
||||
Reference in New Issue
Block a user