Skip to main content

otf_pixels_codec_tiff/
decoder.rs

1//! The TIFF decoder.
2//!
3//! # Random access is the point
4//!
5//! A tiled TIFF stores independently compressed rectangles, each with its own
6//! offset, so producing an arbitrary region means decompressing the tiles that
7//! region touches and nothing else. [`TiffDecoder`] reports
8//! [`DecodeCapability::Regions`] for such a file, and the scheduler then pulls
9//! regions rather than rows — which is how a 2 GB scan becomes a thumbnail
10//! without ever materializing the full-resolution image.
11//!
12//! A strip TIFF has no such property: a strip is the full image width, so
13//! "the tiles this region touches" is "every strip it crosses", and the
14//! decoder reports [`DecodeCapability::Sequential`] instead. Claiming
15//! otherwise would be a lie the scheduler would act on.
16//!
17//! # Why this decoder needs the whole file
18//!
19//! TIFF's offsets point anywhere: the IFD is commonly at the *end*, and tiles
20//! are in no particular order. Random access is therefore incompatible with a
21//! forward-only source, and the decoder reads its input into memory once.
22//!
23//! That is a real cost and it is worth being precise about what it buys: the
24//! *pixels* still stream, because only the tiles a region touches are ever
25//! decompressed. A 2 GB tiled TIFF costs 2 GB of file buffer and a handful of
26//! decompressed tiles, not 2 GB of pixels — which for a 16-bit RGB scan is a
27//! factor of six. Memory-mapping the source would remove even that, and is
28//! deferred rather than dismissed.
29
30use otf_pixels_core::{
31    Codec, DecodeCapability, Decoder, Format, ImageDescriptor, Limits, Orientation, PixelFormat,
32    PixelsError, Region, Result, Source, TileMut,
33};
34
35use crate::ifd::{ByteOrder, Directory, parse_header, probe as probe_header};
36use crate::image::{Layout, Photometric, TiffImage};
37
38/// Decodes a TIFF stream.
39#[derive(Debug)]
40pub struct TiffDecoder {
41    data: Vec<u8>,
42    image: TiffImage,
43    row: u32,
44    /// The most recently decoded chunk, keyed by index.
45    ///
46    /// One chunk is enough: sequential reads walk chunks in order, and region
47    /// reads touch each chunk's rows consecutively. A larger cache belongs to
48    /// the scheduler, which already has one.
49    cached: Option<(usize, Vec<u8>)>,
50}
51
52impl TiffDecoder {
53    /// Read and parse a TIFF.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`PixelsError::Malformed`] for a bad header or directory,
58    /// [`PixelsError::Unsupported`] for a layout this codec does not
59    /// implement, or [`PixelsError::LimitExceeded`] if the image exceeds
60    /// `limits`.
61    pub fn new<S: Source>(mut source: S, limits: Limits) -> Result<Self> {
62        // TIFF offsets point anywhere, so the whole file is read. See the
63        // module docs for what that does and does not cost.
64        let mut data = Vec::new();
65        let mut buffer = vec![0_u8; 256 * 1024];
66        loop {
67            let read = source.read(&mut buffer)?;
68            if read == 0 {
69                break;
70            }
71            let Some(chunk) = buffer.get(..read) else {
72                break;
73            };
74            data.extend_from_slice(chunk);
75        }
76
77        let (order, first_ifd) = parse_header(&data)?;
78        let directory = Directory::parse(&data, order, first_ifd)?;
79        let image = TiffImage::from_directory(&directory, order, &limits)?;
80
81        Ok(Self {
82            data,
83            image,
84            row: 0,
85            cached: None,
86        })
87    }
88
89    /// The parsed image description.
90    #[must_use]
91    pub const fn image(&self) -> &TiffImage {
92        &self.image
93    }
94
95    /// The byte order the file declared.
96    #[must_use]
97    pub const fn byte_order(&self) -> ByteOrder {
98        self.image.order
99    }
100
101    /// Ensure chunk `index` is the cached one, decompressing it if not.
102    fn ensure_chunk(&mut self, index: usize) -> Result<()> {
103        if matches!(&self.cached, Some((cached, _)) if *cached == index) {
104            return Ok(());
105        }
106        let decoded = self.image.read_chunk(&self.data, index)?;
107        self.cached = Some((index, decoded));
108        Ok(())
109    }
110
111    /// Write the part of chunk `(column, row)` that lands inside `region`.
112    fn blit_chunk(
113        &mut self,
114        column: u32,
115        chunk_row: u32,
116        region: Region,
117        out: &mut TileMut<'_>,
118    ) -> Result<()> {
119        let across = self.image.chunks_across();
120        let index = (chunk_row * across + column) as usize;
121        if index >= self.image.offsets.len() {
122            return Err(PixelsError::malformed(
123                "tiff",
124                format!(
125                    "chunk {index} is beyond the {} declared",
126                    self.image.offsets.len()
127                ),
128            ));
129        }
130
131        self.ensure_chunk(index)?;
132        // Destructured so the cached chunk and the image description are
133        // separate borrows. Cloning either instead — which is what the
134        // borrow checker first pushes you toward — would copy the whole
135        // decompressed chunk once per output row, making a strip read
136        // quadratic in image height and defeating the cache entirely.
137        let Self { image, cached, .. } = self;
138        let Some((_, data)) = cached.as_ref() else {
139            return Err(PixelsError::graph("tiff chunk vanished after decoding"));
140        };
141
142        let area = image.chunk_region(column, chunk_row);
143        let (stored_width, _) = image.chunk_stored_size();
144        let stored_row_bytes = image.chunk_row_bytes();
145
146        // The overlap of this chunk with the requested region.
147        let left = area.x.max(region.x);
148        let top = area.y.max(region.y);
149        let right = (area.x + area.width).min(region.x + region.width);
150        let bottom = (area.y + area.height).min(region.y + region.height);
151        if right <= left || bottom <= top {
152            return Ok(());
153        }
154
155        let mut expanded =
156            vec![0_u8; (right - left) as usize * image.descriptor.pixel.bytes_per_pixel()];
157        for y in top..bottom {
158            let within = (y - area.y) as usize;
159            let start = within * stored_row_bytes;
160            let Some(stored) = data.get(start..start + stored_row_bytes) else {
161                continue;
162            };
163            expand_row(
164                image,
165                stored,
166                (left - area.x) as usize,
167                (right - left) as usize,
168                stored_width as usize,
169                &mut expanded,
170            );
171            let Some(target) = out.row_mut(y) else {
172                continue;
173            };
174            let bpp = image.descriptor.pixel.bytes_per_pixel();
175            let at = (left - region.x) as usize * bpp;
176            let Some(slot) = target.get_mut(at..at + expanded.len()) else {
177                continue;
178            };
179            slot.copy_from_slice(&expanded);
180        }
181        Ok(())
182    }
183
184    /// Fill `out` with `region`, decoding only the chunks it touches.
185    fn read_region_into(&mut self, region: Region, out: &mut TileMut<'_>) -> Result<()> {
186        if region.x + region.width > self.image.descriptor.width
187            || region.y + region.height > self.image.descriptor.height
188        {
189            return Err(PixelsError::invalid_argument(
190                "region",
191                format!(
192                    "{region} is outside a {}x{} image",
193                    self.image.descriptor.width, self.image.descriptor.height
194                ),
195            ));
196        }
197
198        let (first_column, last_column, first_row, last_row) = self.chunks_covering(region);
199        for chunk_row in first_row..=last_row {
200            for column in first_column..=last_column {
201                self.blit_chunk(column, chunk_row, region, out)?;
202            }
203        }
204        Ok(())
205    }
206
207    /// The inclusive chunk grid coordinates a region touches.
208    ///
209    /// This is the whole random-access story in four numbers: everything
210    /// outside this range is never read, never decompressed, and never paid
211    /// for.
212    fn chunks_covering(&self, region: Region) -> (u32, u32, u32, u32) {
213        match self.image.layout {
214            Layout::Strips { rows_per_strip } => {
215                let first = region.y / rows_per_strip;
216                let last = (region.y + region.height.saturating_sub(1)) / rows_per_strip;
217                (0, 0, first, last.min(self.image.chunks_down() - 1))
218            }
219            Layout::Tiles { width, height } => {
220                let first_column = region.x / width;
221                let last_column = (region.x + region.width.saturating_sub(1)) / width;
222                let first_row = region.y / height;
223                let last_row = (region.y + region.height.saturating_sub(1)) / height;
224                (
225                    first_column,
226                    last_column.min(self.image.chunks_across() - 1),
227                    first_row,
228                    last_row.min(self.image.chunks_down() - 1),
229                )
230            }
231        }
232    }
233}
234
235/// Expand `count` stored pixels starting at `from` into output format.
236fn expand_row(
237    image: &TiffImage,
238    stored: &[u8],
239    from: usize,
240    count: usize,
241    stored_width: usize,
242    out: &mut [u8],
243) {
244    let bits = image.bits_per_sample as usize;
245    let channels = image.samples_per_pixel as usize;
246    let format = image.descriptor.pixel;
247    let bpp = format.bytes_per_pixel();
248    let maximum = if bits >= 16 {
249        65535_u32
250    } else {
251        (1_u32 << bits) - 1
252    };
253
254    for index in 0..count {
255        let x = from + index;
256        if x >= stored_width {
257            break;
258        }
259        let Some(target) = out.get_mut(index * bpp..(index + 1) * bpp) else {
260            break;
261        };
262
263        // Read every channel of this pixel as a sample in 0..=maximum.
264        let mut samples = [0_u32; 4];
265        for (channel, slot) in samples.iter_mut().enumerate().take(channels.min(4)) {
266            *slot = read_sample(stored, x * channels + channel, bits, image.order);
267        }
268
269        match image.photometric {
270            Photometric::Palette => {
271                // The colour map is 3 * 2^bits 16-bit values, stored as all
272                // reds, then all greens, then all blues — not interleaved,
273                // which is the thing that catches every first implementation.
274                let entries = 1_usize << bits;
275                let index = samples[0] as usize;
276                for channel in 0..3 {
277                    let value = image
278                        .color_map
279                        .get(channel * entries + index)
280                        .copied()
281                        .unwrap_or(0);
282                    if let Some(slot) = target.get_mut(channel) {
283                        // The map is 16-bit; 8-bit output is what consumers
284                        // want and a 256-entry table loses nothing by it.
285                        *slot = (value >> 8) as u8;
286                    }
287                }
288            }
289            _ => {
290                for (channel, &sample) in samples.iter().enumerate().take(channels.min(4)) {
291                    let mut value = sample;
292                    // WhiteIsZero is an inverted greyscale. Inverting only the
293                    // colour channels leaves alpha alone, which is why this is
294                    // not a blanket negation of the pixel.
295                    let is_colour = channel < 3;
296                    if image.photometric == Photometric::WhiteIsZero && is_colour {
297                        value = maximum.saturating_sub(value);
298                    }
299                    write_sample(target, channel, value, bits, maximum, format);
300                }
301            }
302        }
303    }
304}
305
306/// Read stored sample `index` at `bits` per sample.
307fn read_sample(data: &[u8], index: usize, bits: usize, order: ByteOrder) -> u32 {
308    match bits {
309        16 => u32::from(order.u16(data, index * 2)),
310        8 => u32::from(data.get(index).copied().unwrap_or(0)),
311        1 | 2 | 4 => {
312            // Sub-byte samples are packed most-significant-first within each
313            // byte, which is the opposite of what an index-first reading gives.
314            let per_byte = 8 / bits;
315            let byte = data.get(index / per_byte).copied().unwrap_or(0);
316            let shift = 8 - bits * (index % per_byte + 1);
317            u32::from((byte >> shift) & ((1_u16 << bits) - 1) as u8)
318        }
319        _ => 0,
320    }
321}
322
323/// Write one channel of an output pixel, scaling to the format's range.
324fn write_sample(
325    target: &mut [u8],
326    channel: usize,
327    value: u32,
328    bits: usize,
329    maximum: u32,
330    format: PixelFormat,
331) {
332    let widened = if bits >= 8 {
333        value
334    } else {
335        // Scale a sub-byte sample to the full range rather than shifting: a
336        // 1-bit white must become 255, not 128.
337        (value * 255 + maximum / 2) / maximum.max(1)
338    };
339    match format.sample_kind() {
340        otf_pixels_core::SampleKind::U16 => {
341            let scaled = widened as u16;
342            for (offset, byte) in scaled.to_ne_bytes().iter().enumerate() {
343                if let Some(slot) = target.get_mut(channel * 2 + offset) {
344                    *slot = *byte;
345                }
346            }
347        }
348        _ => {
349            if let Some(slot) = target.get_mut(channel) {
350                *slot = widened.min(255) as u8;
351            }
352        }
353    }
354}
355
356impl Decoder for TiffDecoder {
357    fn descriptor(&self) -> ImageDescriptor {
358        self.image.descriptor
359    }
360
361    fn orientation(&self) -> Orientation {
362        self.image.orientation
363    }
364
365    fn icc_profile(&self) -> Option<&[u8]> {
366        self.image.icc.as_deref()
367    }
368
369    fn capability(&self) -> DecodeCapability {
370        // Only a tiled file can answer an arbitrary region cheaply. Claiming
371        // otherwise for a strip file would be a lie the scheduler acts on.
372        if self.image.layout.is_random_access() {
373            DecodeCapability::Regions
374        } else {
375            DecodeCapability::Sequential
376        }
377    }
378
379    fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
380        if self.row >= self.image.descriptor.height {
381            return Err(PixelsError::invalid_argument(
382                "out",
383                format!(
384                    "all {} rows have already been read",
385                    self.image.descriptor.height
386                ),
387            ));
388        }
389        let row_bytes = self.image.descriptor.row_bytes();
390        if out.len() != row_bytes {
391            return Err(PixelsError::invalid_argument(
392                "out",
393                format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
394            ));
395        }
396
397        // Decoded straight into the caller's buffer. Allocating a scratch tile
398        // per row would be wasteful on every image and catastrophic on a
399        // corrupt one, where a bogus ImageWidth within the pixel limit still
400        // implies a very large single row.
401        let region = Region::new(0, self.row, self.image.descriptor.width, 1);
402        let pixel = self.image.descriptor.pixel;
403        let mut tile = TileMut::new(region, pixel, row_bytes, out)?;
404        self.read_region_into(region, &mut tile)?;
405        self.row += 1;
406        Ok(())
407    }
408
409    fn read_region(&mut self, region: Region, out: &mut TileMut<'_>) -> Result<()> {
410        if !self.image.layout.is_random_access() {
411            return Err(PixelsError::unsupported(
412                "this TIFF is stored in strips; region decode requires tiles",
413            ));
414        }
415        self.read_region_into(region, out)
416    }
417}
418
419/// Whether `prefix` starts with a TIFF header.
420///
421/// Detection is by magic bytes only (SPEC §Formats).
422#[must_use]
423pub fn probe(prefix: &[u8]) -> bool {
424    probe_header(prefix)
425}
426
427/// The TIFF entry in a sniffing registry.
428#[derive(Debug, Clone, Copy, Default)]
429pub struct TiffCodec;
430
431impl Codec for TiffCodec {
432    fn format(&self) -> Format {
433        Format::Tiff
434    }
435
436    fn magic_len(&self) -> usize {
437        8
438    }
439
440    fn probe(&self, prefix: &[u8]) -> bool {
441        probe(prefix)
442    }
443}