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