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