1use gamut_core::{Cmyk8, DecodeImage, Dimensions, Error, Gray8, ImageBuf, Result, Rgb8, Rgba8};
4
5use crate::compression::{Compression, ccitt, lzw, packbits, predictor};
6use crate::ifd::PhotometricInterpretation;
7use crate::palette::Palette8;
8use crate::tags;
9use gamut_ifd::{Ifd, read};
10
11#[derive(Debug, Clone, Default)]
17pub struct TiffDecoder {
18 _private: (),
19}
20
21const MAX_IMAGE_BYTES: usize = 64 << 20;
24
25struct DecodedImage {
27 dims: Dimensions,
28 samples_per_pixel: usize,
29 pixels: Vec<u8>,
30}
31
32enum Mode {
34 Gray { white_is_zero: bool },
36 Rgb,
38 Rgba,
40 Cmyk,
42 Palette(Box<Palette8>),
45}
46
47impl TiffDecoder {
48 #[must_use]
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn page_count(&self, data: &[u8]) -> Result<usize> {
60 Ok(read(data)?.ifds.len())
61 }
62
63 pub fn decode_page(&self, data: &[u8], page: usize) -> Result<ImageBuf<Rgb8>> {
72 let img = decode_page_samples(data, page)?;
73 ImageBuf::new(present_rgb(&img)?, img.dims)
74 }
75}
76
77impl DecodeImage<Rgb8> for TiffDecoder {
78 fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Rgb8>> {
80 self.decode_page(data, 0)
81 }
82}
83
84impl DecodeImage<Rgba8> for TiffDecoder {
85 fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Rgba8>> {
87 let img = decode_page_samples(data, 0)?;
88 ImageBuf::new(present_rgba(&img)?, img.dims)
89 }
90}
91
92impl DecodeImage<Cmyk8> for TiffDecoder {
93 fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Cmyk8>> {
95 let img = decode_page_samples(data, 0)?;
96 if img.samples_per_pixel != 4 {
97 return Err(Error::Unsupported("TIFF: image is not 4-sample CMYK"));
98 }
99 ImageBuf::new(img.pixels, img.dims)
100 }
101}
102
103impl DecodeImage<Gray8> for TiffDecoder {
104 fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Gray8>> {
106 let img = decode_page_samples(data, 0)?;
107 if img.samples_per_pixel != 1 {
108 return Err(Error::Unsupported("TIFF: image is not grayscale"));
109 }
110 ImageBuf::new(img.pixels, img.dims)
111 }
112}
113
114fn present_rgb(img: &DecodedImage) -> Result<Vec<u8>> {
116 let mut out = Vec::with_capacity(img.dims.width as usize * img.dims.height as usize * 3);
117 match img.samples_per_pixel {
118 1 => {
119 for &v in &img.pixels {
120 out.extend_from_slice(&[v, v, v]);
121 }
122 }
123 3 => out.extend_from_slice(&img.pixels),
124 4 => {
125 for px in img.pixels.chunks_exact(4) {
126 out.extend_from_slice(&px[0..3]);
127 }
128 }
129 _ => {
130 return Err(Error::Unsupported(
131 "TIFF: cannot present this sample layout as RGB",
132 ));
133 }
134 }
135 Ok(out)
136}
137
138fn present_rgba(img: &DecodedImage) -> Result<Vec<u8>> {
140 let mut out = Vec::with_capacity(img.dims.width as usize * img.dims.height as usize * 4);
141 match img.samples_per_pixel {
142 1 => {
143 for &v in &img.pixels {
144 out.extend_from_slice(&[v, v, v, 255]);
145 }
146 }
147 3 => {
148 for px in img.pixels.chunks_exact(3) {
149 out.extend_from_slice(&[px[0], px[1], px[2], 255]);
150 }
151 }
152 4 => out.extend_from_slice(&img.pixels),
153 _ => {
154 return Err(Error::Unsupported(
155 "TIFF: cannot present this sample layout as RGBA",
156 ));
157 }
158 }
159 Ok(out)
160}
161
162fn require_u32(ifd: &Ifd, tag: u16, what: &'static str) -> Result<u32> {
164 ifd.get_u32(tag).ok_or(Error::InvalidInput(what))
165}
166
167fn decode_page_samples(data: &[u8], page: usize) -> Result<DecodedImage> {
168 let file = read(data)?;
169 let ifd = file
170 .ifds
171 .get(page)
172 .ok_or(Error::InvalidInput("TIFF: page index out of range"))?;
173
174 let width = require_u32(ifd, tags::IMAGE_WIDTH, "TIFF: missing ImageWidth")? as usize;
175 let height = require_u32(ifd, tags::IMAGE_LENGTH, "TIFF: missing ImageLength")? as usize;
176 if width == 0 || height == 0 {
177 return Err(Error::InvalidInput("TIFF: zero-sized image"));
178 }
179
180 let compression = Compression::from_code(ifd.get_u32(tags::COMPRESSION).unwrap_or(1))
181 .ok_or(Error::Unsupported("TIFF: unknown compression"))?;
182 if !matches!(
183 compression,
184 Compression::None
185 | Compression::PackBits
186 | Compression::CcittRle
187 | Compression::CcittGroup4Fax
188 | Compression::Lzw
189 ) {
190 return Err(Error::Unsupported("TIFF: compression not supported yet"));
191 }
192 if ifd.get_u32(tags::PLANAR_CONFIGURATION).unwrap_or(1) != 1 {
193 return Err(Error::Unsupported(
194 "TIFF: planar configuration not supported yet",
195 ));
196 }
197
198 if ifd.get_u32(tags::FILL_ORDER).unwrap_or(1) != 1 {
199 return Err(Error::Unsupported("TIFF: FillOrder 2 not supported"));
200 }
201 let spp = ifd.get_u32(tags::SAMPLES_PER_PIXEL).unwrap_or(1) as usize;
202 let bits = ifd
203 .get_u32_vec(tags::BITS_PER_SAMPLE)
204 .unwrap_or_else(|| vec![1; spp]);
205 if bits.len() != spp || bits.iter().any(|&b| b != bits[0]) {
206 return Err(Error::Unsupported("TIFF: mixed bit depths not supported"));
207 }
208 let bps = bits[0];
209 if matches!(
210 compression,
211 Compression::CcittRle | Compression::CcittGroup4Fax
212 ) && bps != 1
213 {
214 return Err(Error::Unsupported(
215 "TIFF: CCITT coding requires a bilevel image",
216 ));
217 }
218 let use_predictor = match ifd.get_u32(tags::PREDICTOR).unwrap_or(1) {
219 1 => false,
220 2 => true,
221 _ => return Err(Error::Unsupported("TIFF: unknown predictor")),
222 };
223 if use_predictor && bps != 8 {
224 return Err(Error::Unsupported("TIFF: predictor requires 8-bit samples"));
225 }
226
227 let photometric = PhotometricInterpretation::from_code(require_u32(
228 ifd,
229 tags::PHOTOMETRIC_INTERPRETATION,
230 "TIFF: missing PhotometricInterpretation",
231 )?)
232 .ok_or(Error::Unsupported(
233 "TIFF: unknown photometric interpretation",
234 ))?;
235 let mode = match (spp, bps, photometric) {
237 (1, 1 | 8, PhotometricInterpretation::WhiteIsZero) => Mode::Gray {
238 white_is_zero: true,
239 },
240 (1, 1 | 8, PhotometricInterpretation::BlackIsZero) => Mode::Gray {
241 white_is_zero: false,
242 },
243 (3, 8, PhotometricInterpretation::Rgb) => Mode::Rgb,
244 (4, 8, PhotometricInterpretation::Rgb) => Mode::Rgba,
245 (4, 8, PhotometricInterpretation::Cmyk) => Mode::Cmyk,
246 (1, 8, PhotometricInterpretation::Palette) => {
247 let cm = ifd
248 .get_u32_vec(tags::COLOR_MAP)
249 .ok_or(Error::InvalidInput("TIFF: palette image missing ColorMap"))?;
250 Mode::Palette(Box::new(Palette8::from_tiff_colormap(&cm)?))
251 }
252 _ => {
253 return Err(Error::Unsupported(
254 "TIFF: photometric/sample combination not supported yet",
255 ));
256 }
257 };
258
259 let stored_row_bytes = match bps {
261 8 => width
262 .checked_mul(spp)
263 .ok_or(Error::InvalidInput("TIFF: image too large"))?,
264 1 => width.div_ceil(8), _ => {
266 return Err(Error::Unsupported(
267 "TIFF: only 1- and 8-bit samples supported so far",
268 ));
269 }
270 };
271 let stored_total = stored_row_bytes
272 .checked_mul(height)
273 .ok_or(Error::InvalidInput("TIFF: image too large"))?;
274 if stored_total > MAX_IMAGE_BYTES {
275 return Err(Error::Unsupported("TIFF: image exceeds the size limit"));
276 }
277
278 let layout = Layout {
280 width,
281 height,
282 spp,
283 bps,
284 stored_row_bytes,
285 compression,
286 };
287 let mut packed = if ifd.get(tags::TILE_WIDTH).is_some() {
288 decode_tiles(ifd, data, &layout)?
289 } else {
290 decode_strips(ifd, data, &layout)?
291 };
292 debug_assert_eq!(packed.len(), stored_total);
293
294 if use_predictor {
296 predictor::reverse(&mut packed, stored_row_bytes, spp);
297 }
298
299 let (out_spp, pixels) = match mode {
301 Mode::Rgb => (3, packed),
302 Mode::Rgba | Mode::Cmyk => (4, packed),
303 Mode::Gray { white_is_zero } if bps == 8 => {
304 let mut px = packed;
305 if white_is_zero {
306 for v in &mut px {
307 *v = 255 - *v;
308 }
309 }
310 (1, px)
311 }
312 Mode::Gray { white_is_zero } => {
313 let mut px = Vec::with_capacity(width * height);
315 for y in 0..height {
316 let row = &packed[y * stored_row_bytes..(y + 1) * stored_row_bytes];
317 for x in 0..width {
318 let bit = (row[x / 8] >> (7 - (x % 8))) & 1;
319 let white = if white_is_zero { bit == 0 } else { bit == 1 };
320 px.push(if white { 255 } else { 0 });
321 }
322 }
323 (1, px)
324 }
325 Mode::Palette(palette) => {
326 let mut px = Vec::with_capacity(width * height * 3);
328 for &idx in &packed {
329 px.extend_from_slice(&palette.entry(idx));
330 }
331 (3, px)
332 }
333 };
334
335 Ok(DecodedImage {
336 dims: Dimensions {
337 width: width as u32,
338 height: height as u32,
339 },
340 samples_per_pixel: out_spp,
341 pixels,
342 })
343}
344
345struct Layout {
347 width: usize,
348 height: usize,
349 spp: usize,
350 bps: u32,
351 stored_row_bytes: usize,
352 compression: Compression,
353}
354
355fn decompress_simple(raw: &[u8], want: usize, compression: Compression) -> Result<Vec<u8>> {
357 match compression {
358 Compression::None => raw
359 .get(..want)
360 .map(<[u8]>::to_vec)
361 .ok_or(Error::InvalidInput("TIFF: block shorter than expected")),
362 Compression::PackBits => packbits::decode(raw, want),
363 Compression::Lzw => lzw::decode(raw, want),
364 _ => Err(Error::Unsupported(
365 "TIFF: compression not supported for this layout",
366 )),
367 }
368}
369
370fn decode_strips(ifd: &Ifd, data: &[u8], l: &Layout) -> Result<Vec<u8>> {
372 let rows_per_strip = match ifd.get_u32(tags::ROWS_PER_STRIP) {
373 Some(0) | None => l.height,
374 Some(r) => (r as usize).min(l.height),
375 };
376 let offsets = ifd
377 .get_u32_vec(tags::STRIP_OFFSETS)
378 .ok_or(Error::InvalidInput("TIFF: missing StripOffsets"))?;
379 let counts = ifd
380 .get_u32_vec(tags::STRIP_BYTE_COUNTS)
381 .ok_or(Error::InvalidInput("TIFF: missing StripByteCounts"))?;
382 let strips = l.height.div_ceil(rows_per_strip);
383 if offsets.len() != strips || counts.len() != strips {
384 return Err(Error::InvalidInput("TIFF: strip count mismatch"));
385 }
386 let mut packed = Vec::with_capacity(l.stored_row_bytes * l.height);
387 for (i, (&off, &cnt)) in offsets.iter().zip(&counts).enumerate() {
388 let rows = rows_per_strip.min(l.height - i * rows_per_strip);
389 let want = rows * l.stored_row_bytes;
390 let raw = data
391 .get(off as usize..off as usize + cnt as usize)
392 .ok_or(Error::InvalidInput("TIFF: strip out of bounds"))?;
393 match l.compression {
394 Compression::CcittRle => {
395 packed.extend_from_slice(&ccitt::mh_decode_strip(raw, rows, l.width)?);
396 }
397 Compression::CcittGroup4Fax => {
398 packed.extend_from_slice(&ccitt::g4_decode_strip(raw, rows, l.width)?);
399 }
400 other => packed.extend_from_slice(&decompress_simple(raw, want, other)?),
401 }
402 }
403 Ok(packed)
404}
405
406fn decode_tiles(ifd: &Ifd, data: &[u8], l: &Layout) -> Result<Vec<u8>> {
408 if l.bps != 8 {
409 return Err(Error::Unsupported(
410 "TIFF: tiled images supported only for 8-bit samples so far",
411 ));
412 }
413 let tw = ifd
414 .get_u32(tags::TILE_WIDTH)
415 .ok_or(Error::InvalidInput("TIFF: missing TileWidth"))? as usize;
416 let th = ifd
417 .get_u32(tags::TILE_LENGTH)
418 .ok_or(Error::InvalidInput("TIFF: missing TileLength"))? as usize;
419 if tw == 0 || th == 0 {
420 return Err(Error::InvalidInput("TIFF: zero tile dimension"));
421 }
422 let offsets = ifd
423 .get_u32_vec(tags::TILE_OFFSETS)
424 .ok_or(Error::InvalidInput("TIFF: missing TileOffsets"))?;
425 let counts = ifd
426 .get_u32_vec(tags::TILE_BYTE_COUNTS)
427 .ok_or(Error::InvalidInput("TIFF: missing TileByteCounts"))?;
428 let across = l.width.div_ceil(tw);
429 let down = l.height.div_ceil(th);
430 if offsets.len() != across * down || counts.len() != across * down {
431 return Err(Error::InvalidInput("TIFF: tile count mismatch"));
432 }
433 let tile_row_bytes = tw
434 .checked_mul(l.spp)
435 .ok_or(Error::InvalidInput("TIFF: tile too large"))?;
436 let tile_size = th
437 .checked_mul(tile_row_bytes)
438 .ok_or(Error::InvalidInput("TIFF: tile too large"))?;
439 if tile_size > MAX_IMAGE_BYTES {
440 return Err(Error::Unsupported("TIFF: tile exceeds the size limit"));
441 }
442 let mut packed = vec![0u8; l.stored_row_bytes * l.height];
443 for ty in 0..down {
444 for tx in 0..across {
445 let idx = ty * across + tx;
446 let (off, cnt) = (offsets[idx] as usize, counts[idx] as usize);
447 let raw = data
448 .get(off..off + cnt)
449 .ok_or(Error::InvalidInput("TIFF: tile out of bounds"))?;
450 let tile = decompress_simple(raw, tile_size, l.compression)?;
451 let copy_cols = tw.min(l.width - tx * tw);
452 for r in 0..th {
453 let dst_row = ty * th + r;
454 if dst_row >= l.height {
455 break;
456 }
457 let src = r * tile_row_bytes;
458 let dst = dst_row * l.stored_row_bytes + tx * tw * l.spp;
459 packed[dst..dst + copy_cols * l.spp]
460 .copy_from_slice(&tile[src..src + copy_cols * l.spp]);
461 }
462 }
463 }
464 Ok(packed)
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::encoder::TiffEncoder;
471 use gamut_core::{EncodeImage, ImageRef};
472 use gamut_ifd::ByteOrder;
473
474 #[test]
475 fn rejects_truncated_file() {
476 let dec = TiffDecoder::new();
477 let got: Result<ImageBuf<Rgb8>> = dec.decode_image(&[]);
478 assert!(got.is_err());
479 }
480
481 #[test]
482 fn gray_roundtrips_both_orders() {
483 for order in [ByteOrder::LittleEndian, ByteOrder::BigEndian] {
484 let dims = Dimensions {
485 width: 5,
486 height: 3,
487 };
488 let pixels: Vec<u8> = (0..15).collect();
489 let mut tiff = Vec::new();
490 TiffEncoder::new()
491 .with_byte_order(order)
492 .encode_image(ImageRef::<Gray8>::new(&pixels, dims).unwrap(), &mut tiff)
493 .expect("encode");
494 let got: ImageBuf<Gray8> = TiffDecoder::new().decode_image(&tiff).expect("decode");
495 assert_eq!(got.dimensions(), dims);
496 assert_eq!(got.as_samples(), pixels.as_slice());
497 }
498 }
499}