Skip to main content

smriti/services/
raw_preview.rs

1//! Extract the largest embedded JPEG preview from a TIFF-based RAW.
2//!
3//! TIFF-based RAW formats (NEF, CR2, CR3, ARW, DNG, ORF, RW2, PEF,
4//! RWL, SRW, …) all pack one or more full-decoded JPEGs at known
5//! offsets inside the file. Cameras embed these for the rear-screen
6//! preview — they're exactly what every consumer photo-viewer shows
7//! when it claims to "open a RAW". The image is the manufacturer's
8//! intended rendering: correct white balance, correct colours,
9//! correct sharpening. Better than anything an open-source debayer
10//! pipeline produces without the camera's proprietary DCP profiles.
11//!
12//! ## Why a hand-rolled walker
13//!
14//! `rawloader` and `rawler` give you Bayer sensor data, not embedded
15//! JPEGs. `libraw` would work but is a native C dep we don't want to
16//! ship per-platform. `kamadak-exif` (already a dep) walks IFD0,
17//! IFD1, plus the EXIF / GPS / Interop SubIFDs — but the preview
18//! JPEGs in NEF / CR2 / ARW / DNG / ORF live in a TIFF "SubIFD"
19//! chain referenced by tag 0x014A, which kamadak doesn't traverse.
20//!
21//! So we walk the TIFF directly. ~150 lines of well-defined byte
22//! pushing. No deps. Covers every TIFF-based RAW in one shot.
23//!
24//! ## Algorithm
25//!
26//! 1. Read the 8-byte TIFF header. Confirm `II*\0` (little-endian)
27//!    or `MM\0*` (big-endian). Note the offset of IFD0.
28//! 2. Recursively walk every IFD reachable from IFD0:
29//!    - IFD chain via the NextIFD offset (last 4 bytes of each IFD)
30//!    - SubIFD chain via tag 0x014A
31//!    - EXIF / GPS / Interop SubIFDs via tags 0x8769 / 0x8825 / 0xA005
32//!      Each visit short-circuits if we've already seen that offset,
33//!      so a malformed file with a cycle can't hang us.
34//! 3. For each IFD, look for two embedded-JPEG idioms:
35//!    a. `JPEGInterchangeFormat` (0x0201) + length (0x0202)
36//!    b. `StripOffsets` (0x0111) + byte counts (0x0117) where
37//!    `Compression` (0x0103) is 6 (old-style JPEG) or 7 (new).
38//! 4. Sanity-bound each candidate against the file size, then return
39//!    the bytes of the largest one (or None if no candidate found).
40//!
41//! ## What this doesn't do
42//!
43//! - RAF (Fujifilm) — custom container, not TIFF. Falls through to
44//!   the `image::open` path which fails; the user gets a clear error.
45//!   ~100 LOC of standalone parser would add it; do that when a Fuji
46//!   user actually asks.
47//! - Full Bayer demosaic — out of scope; that's a separate path that
48//!   would need DCP colour profiles and per-camera tuning.
49
50use std::collections::HashSet;
51use std::fs::File;
52use std::io::{BufReader, Read, Seek, SeekFrom};
53use std::path::Path;
54
55/// Bytes of the largest embedded JPEG in this TIFF-based RAW, or
56/// `None` if the file doesn't contain a recognisable embedded preview.
57///
58/// Returns `Err` only for I/O failures and malformed TIFF headers —
59/// "no preview found" is the `Ok(None)` case and is normal for some
60/// older / minimal RAWs. Callers should surface that as "couldn't
61/// decode this photo" rather than a hard error.
62pub fn extract_largest_preview(path: &Path) -> Result<Option<Vec<u8>>, String> {
63    let file = File::open(path).map_err(|e| format!("open: {e}"))?;
64    let file_len = file.metadata().map_err(|e| format!("stat: {e}"))?.len();
65    let mut reader = TiffReader::new(BufReader::new(file), file_len)?;
66    let candidates = reader.find_candidates()?;
67    let Some(best) = candidates.into_iter().max_by_key(|c| c.length) else {
68        return Ok(None);
69    };
70    let bytes = reader
71        .read_at(best.offset, best.length)
72        .map_err(|e| format!("read preview bytes: {e}"))?;
73    Ok(Some(bytes))
74}
75
76// ----- internals -----------------------------------------------------
77
78#[derive(Debug, Clone, Copy)]
79struct PreviewCandidate {
80    offset: u64,
81    length: u64,
82}
83
84struct TiffReader {
85    file: BufReader<File>,
86    file_len: u64,
87    little_endian: bool,
88}
89
90/// Tags we care about. TIFF 6.0 + EXIF SubIFD chain entries.
91const TAG_COMPRESSION: u16 = 0x0103;
92const TAG_STRIP_OFFSETS: u16 = 0x0111;
93const TAG_STRIP_BYTE_COUNTS: u16 = 0x0117;
94const TAG_JPEG_INTERCHANGE_FORMAT: u16 = 0x0201;
95const TAG_JPEG_INTERCHANGE_FORMAT_LENGTH: u16 = 0x0202;
96const TAG_SUB_IFDS: u16 = 0x014A;
97const TAG_EXIF_IFD: u16 = 0x8769;
98const TAG_GPS_IFD: u16 = 0x8825;
99const TAG_INTEROP_IFD: u16 = 0xA005;
100
101/// Compression codes that indicate the strip data IS a JPEG bitstream.
102///   6  = old-style JPEG (TIFF 6.0)
103///   7  = new-style JPEG (Technical Note 2)
104fn is_jpeg_compression(code: u32) -> bool {
105    code == 6 || code == 7
106}
107
108/// Hard cap on IFDs walked per file. Protects against pathological
109/// inputs (cycles already prevented by the visited set, but a chain
110/// of valid distinct IFDs could still spiral).
111const MAX_IFDS: usize = 64;
112
113/// Embedded JPEG previews are usually a few MB. Very high-resolution
114/// camera previews can be larger, but anything above this is more
115/// likely a corrupt length tag than a useful preview.
116const MAX_PREVIEW_BYTES: u64 = 128 * 1024 * 1024;
117
118impl TiffReader {
119    fn new(mut file: BufReader<File>, file_len: u64) -> Result<Self, String> {
120        let mut header = [0u8; 8];
121        file.seek(SeekFrom::Start(0))
122            .map_err(|e| format!("seek header: {e}"))?;
123        file.read_exact(&mut header)
124            .map_err(|e| format!("read header: {e}"))?;
125        let little_endian = match &header[0..2] {
126            b"II" => true,
127            b"MM" => false,
128            _ => return Err("not a TIFF (bad byte-order mark)".into()),
129        };
130        // Magic 42 (0x002A). Some makers use 0x0055 (Olympus ORF) but
131        // their IFD layout is the same — accept anything non-zero so
132        // we don't lock out valid containers we haven't seen yet.
133        let magic = read_u16(&header[2..4], little_endian);
134        if magic == 0 {
135            return Err("not a TIFF (zero magic)".into());
136        }
137        Ok(Self {
138            file,
139            file_len,
140            little_endian,
141        })
142    }
143
144    fn find_candidates(&mut self) -> Result<Vec<PreviewCandidate>, String> {
145        // IFD0 offset is bytes 4..8 of the header.
146        let mut header = [0u8; 8];
147        self.file
148            .seek(SeekFrom::Start(0))
149            .map_err(|e| format!("seek: {e}"))?;
150        self.file
151            .read_exact(&mut header)
152            .map_err(|e| format!("read: {e}"))?;
153        let ifd0 = read_u32(&header[4..8], self.little_endian) as u64;
154
155        let mut out: Vec<PreviewCandidate> = Vec::new();
156        let mut visited: HashSet<u64> = HashSet::new();
157        let mut count = 0usize;
158        self.walk(ifd0, &mut visited, &mut out, &mut count)?;
159        Ok(out)
160    }
161
162    fn walk(
163        &mut self,
164        ifd_offset: u64,
165        visited: &mut HashSet<u64>,
166        out: &mut Vec<PreviewCandidate>,
167        count: &mut usize,
168    ) -> Result<(), String> {
169        if ifd_offset == 0 || ifd_offset >= self.file_len {
170            return Ok(());
171        }
172        if !visited.insert(ifd_offset) {
173            return Ok(());
174        }
175        *count += 1;
176        if *count > MAX_IFDS {
177            return Ok(());
178        }
179
180        // IFD layout: u16 entry-count, N × 12-byte entries, u32 next-IFD.
181        let entry_count = self.read_u16(ifd_offset)? as u64;
182        // Defensive: don't trust insane entry counts.
183        if entry_count > 4096 {
184            return Ok(());
185        }
186        let entries_start = ifd_offset + 2;
187        let next_ifd_pos = entries_start + entry_count * 12;
188        if next_ifd_pos + 4 > self.file_len {
189            return Ok(());
190        }
191
192        // First pass: read every entry into memory. We need random
193        // access to multiple tags within one IFD, and re-seeking per
194        // tag is slow + bug-prone.
195        struct Entry {
196            tag: u16,
197            type_id: u16,
198            count: u32,
199            value: u32,
200        }
201        let mut entries: Vec<Entry> = Vec::with_capacity(entry_count as usize);
202        let mut buf = [0u8; 12];
203        for i in 0..entry_count {
204            self.file
205                .seek(SeekFrom::Start(entries_start + i * 12))
206                .map_err(|e| format!("seek entry: {e}"))?;
207            self.file
208                .read_exact(&mut buf)
209                .map_err(|e| format!("read entry: {e}"))?;
210            entries.push(Entry {
211                tag: read_u16(&buf[0..2], self.little_endian),
212                type_id: read_u16(&buf[2..4], self.little_endian),
213                count: read_u32(&buf[4..8], self.little_endian),
214                value: read_u32(&buf[8..12], self.little_endian),
215            });
216        }
217
218        // Pull the values we'll need for this IFD's preview-candidate
219        // decision. All these are LONG or SHORT — we can read either
220        // inline (count = 1, fits in the 4-byte slot) or from the
221        // pointed-to offset.
222        let mut compression: Option<u32> = None;
223        let mut strip_offset: Option<u64> = None;
224        let mut strip_byte_count: Option<u64> = None;
225        let mut jpeg_offset: Option<u64> = None;
226        let mut jpeg_length: Option<u64> = None;
227        let mut sub_ifd_offsets: Vec<u64> = Vec::new();
228        let mut exif_sub_ifd: Option<u64> = None;
229
230        for e in &entries {
231            match e.tag {
232                TAG_COMPRESSION => {
233                    compression = self.read_long_value(e.type_id, e.count, e.value).ok();
234                }
235                // Multiple strips are normal for tiled TIFFs, but for
236                // an embedded JPEG it's always a single strip — count =
237                // 1. The `if e.count == 1` guard on each arm skips
238                // multi-strip variants; they're not JPEG bitstreams.
239                TAG_STRIP_OFFSETS if e.count == 1 => {
240                    strip_offset = self
241                        .read_long_value(e.type_id, e.count, e.value)
242                        .ok()
243                        .map(|v| v as u64);
244                }
245                TAG_STRIP_BYTE_COUNTS if e.count == 1 => {
246                    strip_byte_count = self
247                        .read_long_value(e.type_id, e.count, e.value)
248                        .ok()
249                        .map(|v| v as u64);
250                }
251                TAG_JPEG_INTERCHANGE_FORMAT => {
252                    jpeg_offset = self
253                        .read_long_value(e.type_id, e.count, e.value)
254                        .ok()
255                        .map(|v| v as u64);
256                }
257                TAG_JPEG_INTERCHANGE_FORMAT_LENGTH => {
258                    jpeg_length = self
259                        .read_long_value(e.type_id, e.count, e.value)
260                        .ok()
261                        .map(|v| v as u64);
262                }
263                TAG_SUB_IFDS => {
264                    // Value is an array of u32 offsets. If count == 1
265                    // it sits inline; otherwise the value is an offset
266                    // to count×4 bytes of u32s.
267                    sub_ifd_offsets = self.read_long_array(e.count, e.value)?;
268                }
269                TAG_EXIF_IFD => {
270                    exif_sub_ifd = Some(e.value as u64);
271                }
272                TAG_GPS_IFD | TAG_INTEROP_IFD => {
273                    // We walk these for completeness — they don't
274                    // normally hold preview JPEGs but it's cheap to
275                    // visit and protects against future formats that
276                    // tuck data there.
277                    self.walk(e.value as u64, visited, out, count)?;
278                }
279                _ => {}
280            }
281        }
282
283        // Idiom (a): explicit JPEGInterchangeFormat tag pair.
284        if let (Some(off), Some(len)) = (jpeg_offset, jpeg_length) {
285            if let Some(c) = self.bounded(off, len) {
286                out.push(c);
287            }
288        }
289        // Idiom (b): single-strip + JPEG compression.
290        if let (Some(off), Some(len), Some(comp)) = (strip_offset, strip_byte_count, compression) {
291            if is_jpeg_compression(comp) {
292                if let Some(c) = self.bounded(off, len) {
293                    out.push(c);
294                }
295            }
296        }
297
298        // Recurse into SubIFDs (where Nikon/Sony/DNG put their
299        // full-res previews) and the EXIF SubIFD (sometimes carries
300        // a smaller PreviewIFD on phones).
301        for sub in sub_ifd_offsets {
302            self.walk(sub, visited, out, count)?;
303        }
304        if let Some(off) = exif_sub_ifd {
305            self.walk(off, visited, out, count)?;
306        }
307
308        // Walk the next-IFD chain too — IFD1 (the standard thumbnail
309        // IFD) is reached this way, and on some bodies it carries a
310        // JPEG that's bigger than the SubIFD preview.
311        let next_ifd = self.read_u32(next_ifd_pos)? as u64;
312        self.walk(next_ifd, visited, out, count)?;
313
314        Ok(())
315    }
316
317    /// Confirm `offset + length` is inside the file. Returns `None`
318    /// for any out-of-range or zero-length candidate.
319    fn bounded(&self, offset: u64, length: u64) -> Option<PreviewCandidate> {
320        if length == 0 || offset == 0 {
321            return None;
322        }
323        if length > MAX_PREVIEW_BYTES {
324            return None;
325        }
326        let end = offset.checked_add(length)?;
327        if end > self.file_len {
328            return None;
329        }
330        Some(PreviewCandidate { offset, length })
331    }
332
333    fn read_u16(&mut self, offset: u64) -> Result<u16, String> {
334        let mut buf = [0u8; 2];
335        self.file
336            .seek(SeekFrom::Start(offset))
337            .map_err(|e| format!("seek u16: {e}"))?;
338        self.file
339            .read_exact(&mut buf)
340            .map_err(|e| format!("read u16: {e}"))?;
341        Ok(read_u16(&buf, self.little_endian))
342    }
343
344    fn read_u32(&mut self, offset: u64) -> Result<u32, String> {
345        let mut buf = [0u8; 4];
346        self.file
347            .seek(SeekFrom::Start(offset))
348            .map_err(|e| format!("seek u32: {e}"))?;
349        self.file
350            .read_exact(&mut buf)
351            .map_err(|e| format!("read u32: {e}"))?;
352        Ok(read_u32(&buf, self.little_endian))
353    }
354
355    /// Read a tag value that's a single LONG or SHORT. Handles the
356    /// inline-vs-pointed-to distinction: if the on-disk byte size of
357    /// the value fits in the 4-byte slot (≤4 bytes), it's stored
358    /// inline; otherwise the slot holds a u32 offset.
359    fn read_long_value(&mut self, type_id: u16, count: u32, value: u32) -> Result<u32, String> {
360        // Types: 3 = SHORT (u16), 4 = LONG (u32). Count must be 1.
361        if count != 1 {
362            return Err(format!("expected count=1, got {count}"));
363        }
364        match type_id {
365            3 => Ok(value & 0xFFFF), // inline SHORT
366            4 => Ok(value),          // inline LONG
367            _ => Err(format!("unsupported type {type_id}")),
368        }
369    }
370
371    /// Read a tag value that's an array of LONGs (SubIFDs[]).
372    fn read_long_array(&mut self, count: u32, value: u32) -> Result<Vec<u64>, String> {
373        if count == 0 {
374            return Ok(Vec::new());
375        }
376        if count == 1 {
377            return Ok(vec![value as u64]);
378        }
379        // count >= 2 → value is an offset to count×4 bytes
380        let bytes_needed = (count as u64) * 4;
381        if (value as u64) + bytes_needed > self.file_len {
382            return Ok(Vec::new()); // truncated / corrupt; skip
383        }
384        let mut buf = vec![0u8; bytes_needed as usize];
385        self.file
386            .seek(SeekFrom::Start(value as u64))
387            .map_err(|e| format!("seek long-array: {e}"))?;
388        self.file
389            .read_exact(&mut buf)
390            .map_err(|e| format!("read long-array: {e}"))?;
391        let mut out = Vec::with_capacity(count as usize);
392        for i in 0..count as usize {
393            let off = read_u32(&buf[i * 4..i * 4 + 4], self.little_endian);
394            out.push(off as u64);
395        }
396        Ok(out)
397    }
398
399    fn read_at(&mut self, offset: u64, length: u64) -> std::io::Result<Vec<u8>> {
400        self.file.seek(SeekFrom::Start(offset))?;
401        let mut buf = vec![0u8; length as usize];
402        self.file.read_exact(&mut buf)?;
403        Ok(buf)
404    }
405}
406
407fn read_u16(b: &[u8], little_endian: bool) -> u16 {
408    if little_endian {
409        u16::from_le_bytes([b[0], b[1]])
410    } else {
411        u16::from_be_bytes([b[0], b[1]])
412    }
413}
414
415fn read_u32(b: &[u8], little_endian: bool) -> u32 {
416    if little_endian {
417        u32::from_le_bytes([b[0], b[1], b[2], b[3]])
418    } else {
419        u32::from_be_bytes([b[0], b[1], b[2], b[3]])
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use std::io::Write;
427
428    /// Build a minimal little-endian TIFF in memory with N embedded
429    /// "JPEGs" (just byte runs — they don't need to be valid JPEGs,
430    /// since extract_largest_preview only reads the strip bytes; the
431    /// extractor's job is to find them, not to validate them).
432    /// Each embedded blob lives at a known offset after the IFD0,
433    /// referenced via JPEGInterchangeFormat (0x0201) + length (0x0202).
434    fn make_tiff(blob_lengths: &[u32]) -> Vec<u8> {
435        // We'll build IFD0 with one entry per (offset, length) pair,
436        // followed by IFD1 chained via NextIFD, each carrying ONE
437        // embedded blob. That way each IFD has exactly the
438        // JPEGInterchangeFormat + Length pair the walker recognises.
439        //
440        // Layout:
441        //   [0..8]      header (II*\0, ifd0_offset = 8)
442        //   [8..]       IFD0 (count, entries, next_ifd_offset)
443        //   ...         IFD1, IFD2, ... if multiple blobs
444        //   [end..]     blob bytes
445        //
446        // We compute offsets up front, then emit.
447        let n = blob_lengths.len() as u64;
448        let mut bytes: Vec<u8> = Vec::new();
449
450        // Reserve header.
451        bytes.extend_from_slice(b"II");
452        bytes.extend_from_slice(&42u16.to_le_bytes());
453        bytes.extend_from_slice(&8u32.to_le_bytes()); // IFD0 at offset 8
454
455        // Each IFD is: 2 bytes (count) + 2×12 bytes (entries) + 4 bytes (next).
456        // = 30 bytes per IFD.
457        let ifd_size: u64 = 30;
458        let ifds_total: u64 = ifd_size * n;
459        let blobs_start: u64 = 8 + ifds_total;
460
461        // Compute per-blob offsets.
462        let mut blob_offsets: Vec<u64> = Vec::with_capacity(n as usize);
463        let mut cursor = blobs_start;
464        for &len in blob_lengths {
465            blob_offsets.push(cursor);
466            cursor += len as u64;
467        }
468
469        // Emit IFDs.
470        for (i, (&len, &off)) in blob_lengths.iter().zip(&blob_offsets).enumerate() {
471            // entry count = 2 (JPEGInterchangeFormat + Length).
472            bytes.extend_from_slice(&2u16.to_le_bytes());
473            // Entry: tag 0x0201, type 4 (LONG), count 1, value = offset.
474            bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT.to_le_bytes());
475            bytes.extend_from_slice(&4u16.to_le_bytes());
476            bytes.extend_from_slice(&1u32.to_le_bytes());
477            bytes.extend_from_slice(&(off as u32).to_le_bytes());
478            // Entry: tag 0x0202, type 4 (LONG), count 1, value = length.
479            bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT_LENGTH.to_le_bytes());
480            bytes.extend_from_slice(&4u16.to_le_bytes());
481            bytes.extend_from_slice(&1u32.to_le_bytes());
482            bytes.extend_from_slice(&len.to_le_bytes());
483            // NextIFD pointer. Last IFD has 0.
484            let next: u32 = if i + 1 < blob_lengths.len() {
485                (8 + ifd_size * (i as u64 + 1)) as u32
486            } else {
487                0
488            };
489            bytes.extend_from_slice(&next.to_le_bytes());
490        }
491
492        // Emit the blob bytes themselves.
493        for (i, &len) in blob_lengths.iter().enumerate() {
494            // Fill with distinct bytes per blob so tests can verify
495            // we pulled the right one.
496            let filler = (i as u8).wrapping_add(0xA0);
497            for _ in 0..len {
498                bytes.push(filler);
499            }
500        }
501        bytes
502    }
503
504    fn write_tiff(bytes: &[u8]) -> tempfile::NamedTempFile {
505        let mut f = tempfile::NamedTempFile::new().unwrap();
506        f.as_file_mut().write_all(bytes).unwrap();
507        f.as_file_mut().sync_all().unwrap();
508        f
509    }
510
511    #[test]
512    fn single_preview_is_returned() {
513        let blob_len: u32 = 64;
514        let tiff = make_tiff(&[blob_len]);
515        let f = write_tiff(&tiff);
516        let bytes = extract_largest_preview(f.path()).unwrap().unwrap();
517        assert_eq!(bytes.len(), blob_len as usize);
518        // Every byte should be 0xA0 (filler for blob index 0).
519        assert!(bytes.iter().all(|&b| b == 0xA0));
520    }
521
522    #[test]
523    fn largest_of_multiple_wins() {
524        // Three embedded JPEGs: 64 bytes (idx 0), 256 (idx 1), 32 (idx 2).
525        let tiff = make_tiff(&[64, 256, 32]);
526        let f = write_tiff(&tiff);
527        let bytes = extract_largest_preview(f.path()).unwrap().unwrap();
528        assert_eq!(bytes.len(), 256);
529        // Filler for blob index 1 is 0xA1.
530        assert!(bytes.iter().all(|&b| b == 0xA1));
531    }
532
533    #[test]
534    fn no_preview_returns_none() {
535        // A TIFF whose IFD0 has no JPEG-pointer tags at all.
536        let mut bytes: Vec<u8> = Vec::new();
537        bytes.extend_from_slice(b"II");
538        bytes.extend_from_slice(&42u16.to_le_bytes());
539        bytes.extend_from_slice(&8u32.to_le_bytes());
540        // IFD0: 0 entries, next = 0.
541        bytes.extend_from_slice(&0u16.to_le_bytes());
542        bytes.extend_from_slice(&0u32.to_le_bytes());
543        let f = write_tiff(&bytes);
544        assert!(extract_largest_preview(f.path()).unwrap().is_none());
545    }
546
547    #[test]
548    fn bad_header_errors() {
549        let mut f = tempfile::NamedTempFile::new().unwrap();
550        f.as_file_mut().write_all(b"NOTATIFF").unwrap();
551        f.as_file_mut().sync_all().unwrap();
552        assert!(extract_largest_preview(f.path()).is_err());
553    }
554
555    #[test]
556    fn out_of_bounds_offset_is_skipped() {
557        // Build a TIFF that claims an embedded JPEG ten megabytes
558        // past EOF. The walker should silently drop the candidate.
559        let mut bytes: Vec<u8> = Vec::new();
560        bytes.extend_from_slice(b"II");
561        bytes.extend_from_slice(&42u16.to_le_bytes());
562        bytes.extend_from_slice(&8u32.to_le_bytes());
563        // IFD0: 2 entries, pointing 10 MB past the file end.
564        bytes.extend_from_slice(&2u16.to_le_bytes());
565        bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT.to_le_bytes());
566        bytes.extend_from_slice(&4u16.to_le_bytes());
567        bytes.extend_from_slice(&1u32.to_le_bytes());
568        bytes.extend_from_slice(&10_000_000u32.to_le_bytes());
569        bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT_LENGTH.to_le_bytes());
570        bytes.extend_from_slice(&4u16.to_le_bytes());
571        bytes.extend_from_slice(&1u32.to_le_bytes());
572        bytes.extend_from_slice(&1024u32.to_le_bytes());
573        bytes.extend_from_slice(&0u32.to_le_bytes()); // next IFD
574        let f = write_tiff(&bytes);
575        assert!(extract_largest_preview(f.path()).unwrap().is_none());
576    }
577
578    #[test]
579    fn oversized_preview_length_is_skipped_without_allocation() {
580        let mut bytes: Vec<u8> = Vec::new();
581        bytes.extend_from_slice(b"II");
582        bytes.extend_from_slice(&42u16.to_le_bytes());
583        bytes.extend_from_slice(&8u32.to_le_bytes());
584        bytes.extend_from_slice(&2u16.to_le_bytes());
585        bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT.to_le_bytes());
586        bytes.extend_from_slice(&4u16.to_le_bytes());
587        bytes.extend_from_slice(&1u32.to_le_bytes());
588        bytes.extend_from_slice(&64u32.to_le_bytes());
589        bytes.extend_from_slice(&TAG_JPEG_INTERCHANGE_FORMAT_LENGTH.to_le_bytes());
590        bytes.extend_from_slice(&4u16.to_le_bytes());
591        bytes.extend_from_slice(&1u32.to_le_bytes());
592        bytes.extend_from_slice(&((MAX_PREVIEW_BYTES + 1) as u32).to_le_bytes());
593        bytes.extend_from_slice(&0u32.to_le_bytes());
594
595        let mut f = write_tiff(&bytes);
596        f.as_file_mut().set_len(64 + MAX_PREVIEW_BYTES + 1).unwrap();
597
598        assert!(extract_largest_preview(f.path()).unwrap().is_none());
599    }
600}