wsi_streamer/format/tiff/
pyramid.rs

1//! TIFF pyramid level identification and management.
2//!
3//! WSI files contain multiple IFDs (Image File Directories), but not all are
4//! pyramid levels. This module identifies which IFDs belong to the image pyramid
5//! and provides structured access to them.
6//!
7//! # Pyramid Structure
8//!
9//! A typical WSI file contains:
10//! - **Pyramid levels**: Full resolution image and progressively smaller versions
11//! - **Label image**: Small image of the slide label (often ~500x500)
12//! - **Macro image**: Overview of the entire slide (medium resolution)
13//! - **Thumbnail**: Very small preview image
14//!
15//! # Identification Heuristics
16//!
17//! Pyramid levels are identified by:
18//! 1. Must be tiled (have TileWidth/TileLength tags)
19//! 2. Dimensions decrease by consistent ratios (typically 2x or 4x)
20//! 3. Largest tiled image is level 0
21//!
22//! Non-pyramid images are identified by:
23//! - Label: Small, often square-ish, may not be tiled
24//! - Macro: Medium-sized, different aspect ratio than pyramid
25//! - Thumbnail: Very small, may lack tile structure
26
27use bytes::Bytes;
28
29use crate::error::TiffError;
30use crate::io::RangeReader;
31
32use super::parser::{ByteOrder, Ifd, IfdEntry, TiffHeader, BIGTIFF_HEADER_SIZE};
33use super::tags::TiffTag;
34use super::values::ValueReader;
35
36// =============================================================================
37// Constants
38// =============================================================================
39
40/// Maximum number of IFDs to parse (safety limit)
41const MAX_IFDS: usize = 100;
42
43/// Minimum dimension to be considered a pyramid level (pixels)
44/// Images smaller than this are likely thumbnails
45const MIN_PYRAMID_DIMENSION: u32 = 256;
46
47/// Maximum size for a label image (pixels)
48const MAX_LABEL_DIMENSION: u32 = 2000;
49
50// =============================================================================
51// PyramidLevel
52// =============================================================================
53
54/// A single level in the image pyramid.
55///
56/// Each level represents the image at a specific resolution. Level 0 is the
57/// highest resolution (full size), with higher levels being progressively
58/// smaller (lower resolution).
59#[derive(Debug, Clone)]
60pub struct PyramidLevel {
61    /// Index of this level in the pyramid (0 = highest resolution)
62    pub level_index: usize,
63
64    /// Index of the IFD in the file's IFD chain
65    pub ifd_index: usize,
66
67    /// Image width in pixels
68    pub width: u32,
69
70    /// Image height in pixels
71    pub height: u32,
72
73    /// Tile width in pixels
74    pub tile_width: u32,
75
76    /// Tile height in pixels
77    pub tile_height: u32,
78
79    /// Number of tiles in X direction
80    pub tiles_x: u32,
81
82    /// Number of tiles in Y direction
83    pub tiles_y: u32,
84
85    /// Total number of tiles
86    pub tile_count: u32,
87
88    /// Downsample factor relative to level 0 (1.0 for level 0)
89    pub downsample: f64,
90
91    /// Compression scheme (7 = JPEG)
92    pub compression: u16,
93
94    /// The parsed IFD for this level
95    pub ifd: Ifd,
96
97    /// Offset in file where TileOffsets array is stored (if not inline)
98    pub tile_offsets_entry: Option<IfdEntry>,
99
100    /// Offset in file where TileByteCounts array is stored (if not inline)
101    pub tile_byte_counts_entry: Option<IfdEntry>,
102
103    /// JPEGTables entry for this level (if present)
104    pub jpeg_tables_entry: Option<IfdEntry>,
105}
106
107impl PyramidLevel {
108    /// Create a PyramidLevel from a parsed IFD.
109    ///
110    /// Returns None if the IFD doesn't have the required tile tags.
111    fn from_ifd(ifd: Ifd, ifd_index: usize, byte_order: ByteOrder) -> Option<Self> {
112        // Must have tile dimensions
113        let tile_width = ifd.tile_width(byte_order)?;
114        let tile_height = ifd.tile_height(byte_order)?;
115
116        // Must have image dimensions
117        let width = ifd.image_width(byte_order)?;
118        let height = ifd.image_height(byte_order)?;
119
120        // Get compression (0 indicates missing)
121        let compression = ifd.compression(byte_order).unwrap_or(0);
122
123        // Calculate tile counts
124        let tiles_x = width.div_ceil(tile_width);
125        let tiles_y = height.div_ceil(tile_height);
126        let tile_count = tiles_x * tiles_y;
127
128        // Get entries for tile offsets and byte counts
129        let tile_offsets_entry = ifd.get_entry_by_tag(TiffTag::TileOffsets).cloned();
130        let tile_byte_counts_entry = ifd.get_entry_by_tag(TiffTag::TileByteCounts).cloned();
131
132        // Get JPEGTables entry if present
133        let jpeg_tables_entry = ifd.get_entry_by_tag(TiffTag::JpegTables).cloned();
134
135        Some(PyramidLevel {
136            level_index: 0, // Will be set later when sorting
137            ifd_index,
138            width,
139            height,
140            tile_width,
141            tile_height,
142            tiles_x,
143            tiles_y,
144            tile_count,
145            downsample: 1.0, // Will be calculated later
146            compression,
147            ifd,
148            tile_offsets_entry,
149            tile_byte_counts_entry,
150            jpeg_tables_entry,
151        })
152    }
153
154    /// Check if this level has valid tile offset and byte count entries.
155    pub fn has_tile_data(&self) -> bool {
156        self.tile_offsets_entry.is_some() && self.tile_byte_counts_entry.is_some()
157    }
158
159    /// Get the tile index for a given tile coordinate.
160    ///
161    /// Returns None if the coordinates are out of bounds.
162    pub fn tile_index(&self, tile_x: u32, tile_y: u32) -> Option<u32> {
163        if tile_x >= self.tiles_x || tile_y >= self.tiles_y {
164            return None;
165        }
166        Some(tile_y * self.tiles_x + tile_x)
167    }
168
169    /// Calculate pixel dimensions of a specific tile.
170    ///
171    /// Edge tiles may be smaller than tile_width/tile_height.
172    pub fn tile_dimensions(&self, tile_x: u32, tile_y: u32) -> Option<(u32, u32)> {
173        if tile_x >= self.tiles_x || tile_y >= self.tiles_y {
174            return None;
175        }
176
177        let w = if tile_x == self.tiles_x - 1 {
178            // Last column - may be partial
179            let remainder = self.width % self.tile_width;
180            if remainder == 0 {
181                self.tile_width
182            } else {
183                remainder
184            }
185        } else {
186            self.tile_width
187        };
188
189        let h = if tile_y == self.tiles_y - 1 {
190            // Last row - may be partial
191            let remainder = self.height % self.tile_height;
192            if remainder == 0 {
193                self.tile_height
194            } else {
195                remainder
196            }
197        } else {
198            self.tile_height
199        };
200
201        Some((w, h))
202    }
203}
204
205// =============================================================================
206// TiffPyramid
207// =============================================================================
208
209/// A parsed TIFF image pyramid.
210///
211/// Contains all pyramid levels identified from the TIFF file's IFDs,
212/// sorted by resolution (level 0 = highest resolution).
213#[derive(Debug, Clone)]
214pub struct TiffPyramid {
215    /// The TIFF header
216    pub header: TiffHeader,
217
218    /// Pyramid levels, sorted by resolution (0 = highest)
219    pub levels: Vec<PyramidLevel>,
220
221    /// IFDs that were identified as non-pyramid images (label, macro, etc.)
222    pub other_ifds: Vec<(usize, Ifd)>,
223}
224
225impl TiffPyramid {
226    /// Parse a TIFF file and identify pyramid levels.
227    ///
228    /// This reads all IFDs from the file, identifies which ones belong to the
229    /// image pyramid, and sorts them by resolution.
230    pub async fn parse<R: RangeReader>(reader: &R) -> Result<Self, TiffError> {
231        // Read and parse header
232        let header_bytes = reader.read_exact_at(0, BIGTIFF_HEADER_SIZE).await?;
233        let header = TiffHeader::parse(&header_bytes, reader.size())?;
234
235        // Parse all IFDs
236        let ifds = Self::parse_all_ifds(reader, &header).await?;
237
238        // Identify pyramid levels
239        Self::build_pyramid(header, ifds)
240    }
241
242    /// Parse all IFDs in the file following the next-IFD chain.
243    async fn parse_all_ifds<R: RangeReader>(
244        reader: &R,
245        header: &TiffHeader,
246    ) -> Result<Vec<Ifd>, TiffError> {
247        let mut ifds = Vec::new();
248        let mut offset = header.first_ifd_offset;
249
250        while offset != 0 && ifds.len() < MAX_IFDS {
251            // First, read just enough to get the entry count
252            let count_size = header.ifd_count_size();
253            let count_bytes = reader.read_exact_at(offset, count_size).await?;
254
255            let entry_count = if header.is_bigtiff {
256                header.byte_order.read_u64(&count_bytes)
257            } else {
258                header.byte_order.read_u16(&count_bytes) as u64
259            };
260
261            // Now read the full IFD
262            let ifd_size = Ifd::calculate_size(entry_count, header);
263            let ifd_bytes = reader.read_exact_at(offset, ifd_size).await?;
264            let ifd = Ifd::parse(&ifd_bytes, header)?;
265
266            let next_offset = ifd.next_ifd_offset;
267            ifds.push(ifd);
268
269            offset = next_offset;
270        }
271
272        Ok(ifds)
273    }
274
275    /// Build the pyramid structure from parsed IFDs.
276    fn build_pyramid(header: TiffHeader, ifds: Vec<Ifd>) -> Result<Self, TiffError> {
277        let byte_order = header.byte_order;
278
279        let mut pyramid_candidates: Vec<PyramidLevel> = Vec::new();
280        let mut other_ifds: Vec<(usize, Ifd)> = Vec::new();
281
282        for (ifd_index, ifd) in ifds.into_iter().enumerate() {
283            // Try to create a pyramid level from this IFD
284            if let Some(level) = PyramidLevel::from_ifd(ifd.clone(), ifd_index, byte_order) {
285                // Check if this looks like a pyramid level
286                if Self::is_pyramid_candidate(&level) {
287                    pyramid_candidates.push(level);
288                } else {
289                    other_ifds.push((ifd_index, ifd));
290                }
291            } else {
292                // IFD doesn't have tile structure
293                other_ifds.push((ifd_index, ifd));
294            }
295        }
296
297        // Sort candidates by area (largest first = level 0)
298        pyramid_candidates.sort_by(|a, b| {
299            let area_a = (a.width as u64) * (a.height as u64);
300            let area_b = (b.width as u64) * (b.height as u64);
301            area_b.cmp(&area_a)
302        });
303
304        // Filter to keep only levels that form a consistent pyramid
305        let levels = Self::filter_pyramid_levels(pyramid_candidates);
306
307        Ok(TiffPyramid {
308            header,
309            levels,
310            other_ifds,
311        })
312    }
313
314    /// Check if a level looks like a pyramid candidate (vs label/macro).
315    fn is_pyramid_candidate(level: &PyramidLevel) -> bool {
316        // Must have minimum dimensions
317        if level.width < MIN_PYRAMID_DIMENSION || level.height < MIN_PYRAMID_DIMENSION {
318            return false;
319        }
320
321        // Must have tile data
322        if !level.has_tile_data() {
323            return false;
324        }
325
326        // Exclude likely label images (small and square-ish)
327        if level.width <= MAX_LABEL_DIMENSION && level.height <= MAX_LABEL_DIMENSION {
328            let aspect_ratio = level.width as f64 / level.height as f64;
329            // Labels are often square or nearly square
330            if aspect_ratio > 0.5 && aspect_ratio < 2.0 {
331                // This might be a label, but only exclude if it's small
332                if level.width <= 1000 && level.height <= 1000 {
333                    return false;
334                }
335            }
336        }
337
338        true
339    }
340
341    /// Filter candidates to keep only levels that form a consistent pyramid.
342    fn filter_pyramid_levels(candidates: Vec<PyramidLevel>) -> Vec<PyramidLevel> {
343        if candidates.is_empty() {
344            return candidates;
345        }
346
347        // The largest image is always level 0
348        let base_width = candidates[0].width as f64;
349        let base_height = candidates[0].height as f64;
350
351        let mut levels = Vec::new();
352
353        for (idx, mut level) in candidates.into_iter().enumerate() {
354            // Calculate downsample factor
355            let downsample_x = base_width / level.width as f64;
356            let downsample_y = base_height / level.height as f64;
357
358            // Use average downsample (they should be close)
359            let downsample = (downsample_x + downsample_y) / 2.0;
360
361            // Check if this level has a reasonable downsample factor
362            // Pyramid levels typically have power-of-2 or power-of-4 downsamples
363            if Self::is_valid_downsample(downsample, idx) {
364                level.level_index = levels.len();
365                level.downsample = downsample;
366                levels.push(level);
367            }
368        }
369
370        levels
371    }
372
373    /// Check if a downsample factor is valid for pyramid level.
374    fn is_valid_downsample(downsample: f64, level_idx: usize) -> bool {
375        if level_idx == 0 {
376            // First level should have downsample ~1.0
377            return (downsample - 1.0).abs() < 0.1;
378        }
379
380        // For other levels, check if it's close to a power of 2
381        // Allow some tolerance for rounding
382        let log2 = downsample.log2();
383        let rounded = log2.round();
384
385        // Must be at least 2x downsample for level 1+
386        if rounded < 1.0 {
387            return false;
388        }
389
390        // Check if close to a power of 2
391        let expected = 2.0_f64.powf(rounded);
392        let ratio = downsample / expected;
393
394        // Allow 20% tolerance
395        ratio > 0.8 && ratio < 1.2
396    }
397
398    /// Get the number of pyramid levels.
399    pub fn level_count(&self) -> usize {
400        self.levels.len()
401    }
402
403    /// Get a pyramid level by index.
404    pub fn get_level(&self, level: usize) -> Option<&PyramidLevel> {
405        self.levels.get(level)
406    }
407
408    /// Get the base (highest resolution) level.
409    pub fn base_level(&self) -> Option<&PyramidLevel> {
410        self.levels.first()
411    }
412
413    /// Get dimensions of the base level.
414    pub fn dimensions(&self) -> Option<(u32, u32)> {
415        self.base_level().map(|l| (l.width, l.height))
416    }
417
418    /// Find the best level for a given downsample factor.
419    ///
420    /// Returns the level with the smallest downsample that is >= the requested factor.
421    pub fn best_level_for_downsample(&self, downsample: f64) -> Option<&PyramidLevel> {
422        // Find the level with smallest downsample >= requested
423        self.levels
424            .iter()
425            .filter(|l| l.downsample >= downsample * 0.99) // Small tolerance
426            .min_by(|a, b| a.downsample.partial_cmp(&b.downsample).unwrap())
427            .or_else(|| self.levels.last()) // Fall back to lowest resolution
428    }
429}
430
431// =============================================================================
432// Tile Data Loading
433// =============================================================================
434
435/// Loaded tile data for a pyramid level.
436#[derive(Debug, Clone)]
437pub struct TileData {
438    /// Byte offset of each tile in the file
439    pub offsets: Vec<u64>,
440
441    /// Byte count (size) of each tile
442    pub byte_counts: Vec<u64>,
443
444    /// JPEGTables data (if present)
445    pub jpeg_tables: Option<Bytes>,
446}
447
448impl TileData {
449    /// Load tile data for a pyramid level.
450    pub async fn load<R: RangeReader>(
451        reader: &R,
452        level: &PyramidLevel,
453        header: &TiffHeader,
454    ) -> Result<Self, TiffError> {
455        let value_reader = ValueReader::new(reader, header);
456
457        // Load tile offsets
458        let offsets = if let Some(ref entry) = level.tile_offsets_entry {
459            value_reader.read_u64_array(entry).await?
460        } else {
461            return Err(TiffError::MissingTag("TileOffsets"));
462        };
463
464        // Load tile byte counts
465        let byte_counts = if let Some(ref entry) = level.tile_byte_counts_entry {
466            value_reader.read_u64_array(entry).await?
467        } else {
468            return Err(TiffError::MissingTag("TileByteCounts"));
469        };
470
471        // Load JPEGTables if present
472        let jpeg_tables = if let Some(ref entry) = level.jpeg_tables_entry {
473            Some(value_reader.read_raw_bytes(entry).await?)
474        } else {
475            None
476        };
477
478        Ok(TileData {
479            offsets,
480            byte_counts,
481            jpeg_tables,
482        })
483    }
484
485    /// Get offset and size for a specific tile.
486    pub fn get_tile_location(&self, tile_index: u32) -> Option<(u64, u64)> {
487        let idx = tile_index as usize;
488        if idx >= self.offsets.len() || idx >= self.byte_counts.len() {
489            return None;
490        }
491        Some((self.offsets[idx], self.byte_counts[idx]))
492    }
493}
494
495// =============================================================================
496// Tests
497// =============================================================================
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    fn make_tiff_header() -> TiffHeader {
504        TiffHeader {
505            byte_order: ByteOrder::LittleEndian,
506            is_bigtiff: false,
507            first_ifd_offset: 8,
508        }
509    }
510
511    // -------------------------------------------------------------------------
512    // PyramidLevel tests
513    // -------------------------------------------------------------------------
514
515    #[test]
516    fn test_tile_index() {
517        let level = PyramidLevel {
518            level_index: 0,
519            ifd_index: 0,
520            width: 1024,
521            height: 768,
522            tile_width: 256,
523            tile_height: 256,
524            tiles_x: 4,
525            tiles_y: 3,
526            tile_count: 12,
527            downsample: 1.0,
528            compression: 7,
529            ifd: create_mock_ifd(),
530            tile_offsets_entry: None,
531            tile_byte_counts_entry: None,
532            jpeg_tables_entry: None,
533        };
534
535        // Valid indices
536        assert_eq!(level.tile_index(0, 0), Some(0));
537        assert_eq!(level.tile_index(1, 0), Some(1));
538        assert_eq!(level.tile_index(0, 1), Some(4));
539        assert_eq!(level.tile_index(3, 2), Some(11));
540
541        // Out of bounds
542        assert_eq!(level.tile_index(4, 0), None);
543        assert_eq!(level.tile_index(0, 3), None);
544    }
545
546    #[test]
547    fn test_tile_dimensions() {
548        let level = PyramidLevel {
549            level_index: 0,
550            ifd_index: 0,
551            width: 1000, // Not evenly divisible by 256
552            height: 700,
553            tile_width: 256,
554            tile_height: 256,
555            tiles_x: 4, // ceil(1000/256)
556            tiles_y: 3, // ceil(700/256)
557            tile_count: 12,
558            downsample: 1.0,
559            compression: 7,
560            ifd: create_mock_ifd(),
561            tile_offsets_entry: None,
562            tile_byte_counts_entry: None,
563            jpeg_tables_entry: None,
564        };
565
566        // Full tiles
567        assert_eq!(level.tile_dimensions(0, 0), Some((256, 256)));
568        assert_eq!(level.tile_dimensions(1, 1), Some((256, 256)));
569
570        // Partial tile on right edge (1000 % 256 = 232)
571        assert_eq!(level.tile_dimensions(3, 0), Some((232, 256)));
572
573        // Partial tile on bottom edge (700 % 256 = 188)
574        assert_eq!(level.tile_dimensions(0, 2), Some((256, 188)));
575
576        // Corner partial tile
577        assert_eq!(level.tile_dimensions(3, 2), Some((232, 188)));
578
579        // Out of bounds
580        assert_eq!(level.tile_dimensions(4, 0), None);
581    }
582
583    #[test]
584    fn test_is_valid_downsample() {
585        // Level 0 should be ~1.0
586        assert!(TiffPyramid::is_valid_downsample(1.0, 0));
587        assert!(TiffPyramid::is_valid_downsample(1.05, 0));
588        assert!(!TiffPyramid::is_valid_downsample(2.0, 0));
589
590        // Level 1+ should be powers of 2
591        assert!(TiffPyramid::is_valid_downsample(2.0, 1));
592        assert!(TiffPyramid::is_valid_downsample(4.0, 2));
593        assert!(TiffPyramid::is_valid_downsample(8.0, 3));
594        assert!(TiffPyramid::is_valid_downsample(16.0, 4));
595
596        // Allow some tolerance
597        assert!(TiffPyramid::is_valid_downsample(2.1, 1));
598        assert!(TiffPyramid::is_valid_downsample(3.9, 2));
599
600        // Reject values too far off
601        assert!(!TiffPyramid::is_valid_downsample(1.5, 1)); // Not close to 2
602        assert!(!TiffPyramid::is_valid_downsample(3.0, 2)); // Not close to 4
603    }
604
605    #[test]
606    fn test_is_pyramid_candidate() {
607        // Large enough, has tile data
608        let good_level = PyramidLevel {
609            level_index: 0,
610            ifd_index: 0,
611            width: 10000,
612            height: 8000,
613            tile_width: 256,
614            tile_height: 256,
615            tiles_x: 40,
616            tiles_y: 32,
617            tile_count: 1280,
618            downsample: 1.0,
619            compression: 7,
620            ifd: create_mock_ifd(),
621            tile_offsets_entry: Some(create_mock_entry()),
622            tile_byte_counts_entry: Some(create_mock_entry()),
623            jpeg_tables_entry: None,
624        };
625        assert!(TiffPyramid::is_pyramid_candidate(&good_level));
626
627        // Too small
628        let small_level = PyramidLevel {
629            width: 100,
630            height: 100,
631            ..good_level.clone()
632        };
633        assert!(!TiffPyramid::is_pyramid_candidate(&small_level));
634
635        // No tile data
636        let no_tiles = PyramidLevel {
637            tile_offsets_entry: None,
638            ..good_level.clone()
639        };
640        assert!(!TiffPyramid::is_pyramid_candidate(&no_tiles));
641
642        // Label-like (small and square)
643        let label_like = PyramidLevel {
644            width: 500,
645            height: 500,
646            tiles_x: 2,
647            tiles_y: 2,
648            tile_count: 4,
649            ..good_level.clone()
650        };
651        assert!(!TiffPyramid::is_pyramid_candidate(&label_like));
652    }
653
654    #[test]
655    fn test_best_level_for_downsample() {
656        let header = make_tiff_header();
657        let pyramid = TiffPyramid {
658            header,
659            levels: vec![
660                create_level_with_downsample(0, 1.0, 10000, 8000),
661                create_level_with_downsample(1, 4.0, 2500, 2000),
662                create_level_with_downsample(2, 16.0, 625, 500),
663            ],
664            other_ifds: vec![],
665        };
666
667        // Exact matches
668        assert_eq!(
669            pyramid.best_level_for_downsample(1.0).unwrap().level_index,
670            0
671        );
672        assert_eq!(
673            pyramid.best_level_for_downsample(4.0).unwrap().level_index,
674            1
675        );
676        assert_eq!(
677            pyramid.best_level_for_downsample(16.0).unwrap().level_index,
678            2
679        );
680
681        // In between - should use next higher resolution
682        assert_eq!(
683            pyramid.best_level_for_downsample(2.0).unwrap().level_index,
684            1
685        );
686        assert_eq!(
687            pyramid.best_level_for_downsample(8.0).unwrap().level_index,
688            2
689        );
690
691        // Below minimum - use highest resolution
692        assert_eq!(
693            pyramid.best_level_for_downsample(0.5).unwrap().level_index,
694            0
695        );
696
697        // Above maximum - use lowest resolution
698        assert_eq!(
699            pyramid.best_level_for_downsample(32.0).unwrap().level_index,
700            2
701        );
702    }
703
704    // -------------------------------------------------------------------------
705    // Helper functions for tests
706    // -------------------------------------------------------------------------
707
708    fn create_mock_ifd() -> Ifd {
709        Ifd::empty()
710    }
711
712    fn create_mock_entry() -> IfdEntry {
713        IfdEntry {
714            tag_id: 324,
715            field_type: Some(super::super::tags::FieldType::Long),
716            field_type_raw: 4,
717            count: 1,
718            value_offset_bytes: vec![0, 0, 0, 0],
719            is_inline: true,
720        }
721    }
722
723    fn create_level_with_downsample(
724        level_index: usize,
725        downsample: f64,
726        width: u32,
727        height: u32,
728    ) -> PyramidLevel {
729        PyramidLevel {
730            level_index,
731            ifd_index: level_index,
732            width,
733            height,
734            tile_width: 256,
735            tile_height: 256,
736            tiles_x: width.div_ceil(256),
737            tiles_y: height.div_ceil(256),
738            tile_count: width.div_ceil(256) * height.div_ceil(256),
739            downsample,
740            compression: 7,
741            ifd: create_mock_ifd(),
742            tile_offsets_entry: Some(create_mock_entry()),
743            tile_byte_counts_entry: Some(create_mock_entry()),
744            jpeg_tables_entry: None,
745        }
746    }
747}