Skip to main content

document_svg/document/
tiff.rs

1//! Bounded TIFF/BigTIFF reader for single- and multi-page raster documents.
2//!
3//! The pages are embedded as PNGs so the SVG output does not depend on browser
4//! support for TIFF. Supported pages include common unsigned-integer grayscale, RGB and
5//! alpha variants, plus CMYK. Wider integer channels are reduced to 8-bit.
6//! Other color models and floating-point samples are rejected explicitly rather
7//! than emitted with guessed color semantics.
8
9use std::fs::File;
10use std::io::BufReader;
11use std::num::NonZeroUsize;
12use std::path::Path;
13
14use base64::Engine;
15use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
16use tiff::ColorType;
17use tiff::decoder::{Decoder, DecodingResult, Limits};
18use tiff::tags::Tag;
19
20use crate::convert::{ConvertOptions, PageConsumer};
21use crate::error::{Error, Result};
22use crate::ir::{IDENTITY, Node, Page, SourceMeta};
23
24const MAX_TIFF_PIXELS: u64 = 20_000_000;
25const MAX_TIFF_TOTAL_PIXELS: u64 = 100_000_000;
26const MAX_TIFF_DECODED_BYTES: usize = 96 * 1024 * 1024;
27const MAX_TIFF_DATA_URI_BYTES: usize = 128 * 1024 * 1024;
28const MAX_TIFF_TOTAL_DATA_URI_BYTES: usize = 512 * 1024 * 1024;
29const MAX_TIFF_PAGE_DIMENSION: f64 = 1_000_000.0;
30
31pub(crate) fn convert(
32    path: &Path,
33    options: &ConvertOptions,
34    sink: &mut dyn PageConsumer,
35) -> Result<Vec<String>> {
36    let file = File::open(path)?;
37    let mut limits = Limits::default();
38    limits.decoding_buffer_size = MAX_TIFF_DECODED_BYTES;
39    limits.intermediate_buffer_size = MAX_TIFF_DECODED_BYTES;
40    let mut decoder = Decoder::new(BufReader::new(file))
41        .map_err(|error| map_tiff_error(error, "TIFF header"))?
42        .with_limits(limits);
43    let mut decoded = DecodingResult::U8(Vec::new());
44    let mut page_number = 0usize;
45    let mut total_pixels = 0u64;
46    let mut total_data_uri_bytes = 0usize;
47
48    loop {
49        page_number = page_number.saturating_add(1);
50        if page_number > options.max_pages {
51            return Err(Error::LimitExceeded(format!(
52                "TIFF contains more than {} image directories",
53                options.max_pages
54            )));
55        }
56        let (width, height) = decoder
57            .dimensions()
58            .map_err(|error| map_tiff_error(error, "TIFF page dimensions"))?;
59        let pixels = u64::from(width)
60            .checked_mul(u64::from(height))
61            .ok_or_else(|| Error::LimitExceeded("TIFF pixel count overflowed".into()))?;
62        if width == 0 || height == 0 || pixels > MAX_TIFF_PIXELS {
63            return Err(Error::LimitExceeded(format!(
64                "TIFF page {page_number} is {width}x{height}; maximum is {MAX_TIFF_PIXELS} pixels"
65            )));
66        }
67        total_pixels = total_pixels
68            .checked_add(pixels)
69            .ok_or_else(|| Error::LimitExceeded("TIFF total pixel count overflowed".into()))?;
70        if total_pixels > MAX_TIFF_TOTAL_PIXELS {
71            return Err(Error::LimitExceeded(format!(
72                "TIFF pages exceed the {MAX_TIFF_TOTAL_PIXELS}-pixel total limit"
73            )));
74        }
75
76        let color_type = decoder
77            .colortype()
78            .map_err(|error| map_tiff_error(error, "TIFF color model"))?;
79        let has_icc_profile = {
80            let image_ifd = decoder.image_ifd();
81            image_ifd.directory().contains(Tag::IccProfile)
82        };
83        let orientation = decoder
84            .find_tag_unsigned::<u16>(Tag::Orientation)
85            .ok()
86            .flatten()
87            .filter(|value| (1..=8).contains(value))
88            .unwrap_or(1);
89        let (dpi_x, dpi_y) = page_resolution(&mut decoder);
90        let layout = decoder
91            .read_image_to_buffer(&mut decoded)
92            .map_err(|error| map_tiff_error(error, "TIFF image data"))?;
93        if decoded_buffer_len(&decoded) < layout.complete_len {
94            return Err(Error::LimitExceeded(format!(
95                "TIFF page {page_number} needs {} decoded bytes for all sample planes, exceeding the configured decoder limit",
96                layout.complete_len
97            )));
98        }
99
100        let png =
101            encode_page_png(width, height, color_type, &layout, &decoded).map_err(|error| {
102                match error {
103                    Error::LimitExceeded(_) | Error::Unsupported(_) => error,
104                    other => Error::InvalidInput(format!("TIFF page {page_number}: {other}")),
105                }
106            })?;
107        let data_uri = format!("data:image/png;base64,{}", BASE64_STANDARD.encode(png));
108        if data_uri.len() > MAX_TIFF_DATA_URI_BYTES {
109            return Err(Error::LimitExceeded(format!(
110                "TIFF page {page_number} PNG data URI exceeds {MAX_TIFF_DATA_URI_BYTES} bytes"
111            )));
112        }
113        total_data_uri_bytes = total_data_uri_bytes
114            .checked_add(data_uri.len())
115            .ok_or_else(|| Error::LimitExceeded("TIFF total data URI size overflowed".into()))?;
116        if total_data_uri_bytes > MAX_TIFF_TOTAL_DATA_URI_BYTES {
117            return Err(Error::LimitExceeded(format!(
118                "TIFF pages exceed the {MAX_TIFF_TOTAL_DATA_URI_BYTES}-byte total image limit"
119            )));
120        }
121
122        let transposed = orientation >= 5;
123        let (oriented_width, oriented_height) = if transposed {
124            (height, width)
125        } else {
126            (width, height)
127        };
128        let (scale_x, scale_y) = if transposed {
129            (72.0 / dpi_y, 72.0 / dpi_x)
130        } else {
131            (72.0 / dpi_x, 72.0 / dpi_y)
132        };
133        let page_width = f64::from(oriented_width) * scale_x;
134        let page_height = f64::from(oriented_height) * scale_y;
135        if !page_width.is_finite()
136            || !page_height.is_finite()
137            || page_width <= 0.0
138            || page_height <= 0.0
139            || page_width > MAX_TIFF_PAGE_DIMENSION
140            || page_height > MAX_TIFF_PAGE_DIMENSION
141        {
142            return Err(Error::LimitExceeded(format!(
143                "TIFF page {page_number} physical dimensions exceed the supported range"
144            )));
145        }
146
147        let mut page = Page::new(page_number, page_width, page_height, "tiff");
148        page.description = format!("TIFF image directory {page_number} ({width}x{height})");
149        if color_type.bit_depth() > 8 {
150            page.warn(format!(
151                "TIFF page {page_number} sample channels were reduced from {}-bit to 8-bit PNG",
152                color_type.bit_depth()
153            ));
154        }
155        if matches!(color_type, ColorType::CMYK(_) | ColorType::CMYKA(_)) {
156            page.warn(format!(
157                "TIFF page {page_number} CMYK samples were converted with a generic RGB formula; color management is not applied"
158            ));
159        }
160        if has_icc_profile {
161            page.warn(format!(
162                "TIFF page {page_number} embeds an ICC profile that was not applied"
163            ));
164        }
165        page.nodes.push(Node::Image {
166            id: format!("tiff-image-{page_number}"),
167            href: data_uri,
168            x: 0.0,
169            y: 0.0,
170            width: f64::from(width),
171            height: f64::from(height),
172            transform: orientation_transform(orientation, width, height, scale_x, scale_y),
173            opacity: 1.0,
174            clip_id: None,
175            meta: SourceMeta {
176                semantic_role: "tiff:image".into(),
177                ..Default::default()
178            },
179        });
180        sink.consume(page)?;
181
182        if !decoder.more_images() {
183            break;
184        }
185        decoder
186            .next_image()
187            .map_err(|error| map_tiff_error(error, "next TIFF image directory"))?;
188    }
189    Ok(Vec::new())
190}
191
192fn encode_page_png(
193    width: u32,
194    height: u32,
195    color_type: ColorType,
196    layout: &tiff::decoder::BufferLayoutPreference,
197    decoded: &DecodingResult,
198) -> Result<Vec<u8>> {
199    let (png_color, channels, bits) = match color_type {
200        ColorType::Gray(bits @ (1 | 2 | 4 | 8 | 16 | 32 | 64)) => {
201            (png::ColorType::Grayscale, 1, bits)
202        }
203        ColorType::GrayA(bits @ (8 | 16)) => (png::ColorType::GrayscaleAlpha, 2, bits),
204        ColorType::RGB(bits @ (8 | 16 | 32 | 64)) => (png::ColorType::Rgb, 3, bits),
205        ColorType::RGBA(bits @ (8 | 16 | 32 | 64)) => (png::ColorType::Rgba, 4, bits),
206        ColorType::CMYK(bits @ (8 | 16 | 32 | 64)) => (png::ColorType::Rgb, 4, bits),
207        ColorType::CMYKA(8) => (png::ColorType::Rgba, 5, 8),
208        unsupported => {
209            return Err(Error::Unsupported(format!(
210                "TIFF color type {unsupported:?} is not supported; expected integer grayscale, RGB, CMYK, or alpha samples"
211            )));
212        }
213    };
214    if layout.planes != 1 && layout.planes != usize::from(channels) {
215        return Err(Error::Unsupported(format!(
216            "TIFF sample layout has {} planes for {channels} output channels",
217            layout.planes
218        )));
219    }
220    let sample_bytes = usize::from(bits).div_ceil(8);
221    let row_stride = layout.row_stride.map(NonZeroUsize::get).unwrap_or_else(|| {
222        usize::try_from(width)
223            .unwrap_or(0)
224            .saturating_mul(usize::from(channels) * sample_bytes)
225    });
226    let plane_stride = layout
227        .plane_stride
228        .map(NonZeroUsize::get)
229        .unwrap_or_else(|| row_stride.saturating_mul(height as usize));
230    let minimum_row = if bits < 8 {
231        usize::try_from(width)
232            .unwrap_or(0)
233            .saturating_mul(usize::from(bits))
234            .div_ceil(8)
235    } else if layout.planes > 1 {
236        usize::try_from(width)
237            .unwrap_or(0)
238            .saturating_mul(sample_bytes)
239    } else {
240        usize::try_from(width)
241            .unwrap_or(0)
242            .saturating_mul(usize::from(channels) * sample_bytes)
243    };
244    if row_stride < minimum_row {
245        return Err(Error::InvalidInput(
246            "TIFF decoder returned a row stride shorter than its pixel data".into(),
247        ));
248    }
249    if layout.planes > 1
250        && plane_stride
251            < row_stride
252                .checked_mul(height as usize)
253                .ok_or_else(|| Error::LimitExceeded("TIFF plane stride overflowed".into()))?
254    {
255        return Err(Error::InvalidInput(
256            "TIFF decoder returned an incomplete planar image".into(),
257        ));
258    }
259    let bytes_per_pixel = png_color
260        .samples()
261        .checked_mul(usize::try_from(width).unwrap_or(0))
262        .ok_or_else(|| Error::LimitExceeded("TIFF PNG row size overflowed".into()))?;
263    let output_len = bytes_per_pixel
264        .checked_mul(height as usize)
265        .ok_or_else(|| Error::LimitExceeded("TIFF PNG size overflowed".into()))?;
266    if output_len > MAX_TIFF_DECODED_BYTES {
267        return Err(Error::LimitExceeded(format!(
268            "TIFF PNG output requires {output_len} bytes; maximum is {MAX_TIFF_DECODED_BYTES}"
269        )));
270    }
271    let mut output = Vec::with_capacity(output_len);
272    let packed = bits < 8;
273    let is_cmyk = matches!(color_type, ColorType::CMYK(_) | ColorType::CMYKA(_));
274    if bits == 8 && layout.planes == 1 && !is_cmyk {
275        let DecodingResult::U8(data) = decoded else {
276            return Err(Error::Unsupported(
277                "8-bit TIFF samples were not decoded into a byte buffer".into(),
278            ));
279        };
280        let row_bytes = usize::try_from(width)
281            .unwrap_or(0)
282            .checked_mul(usize::from(channels))
283            .ok_or_else(|| Error::LimitExceeded("TIFF row size overflowed".into()))?;
284        for y in 0..height as usize {
285            let start = y
286                .checked_mul(row_stride)
287                .ok_or_else(|| Error::LimitExceeded("TIFF row offset overflowed".into()))?;
288            let end = start
289                .checked_add(row_bytes)
290                .ok_or_else(|| Error::LimitExceeded("TIFF row offset overflowed".into()))?;
291            let row = data.get(start..end).ok_or_else(|| {
292                Error::InvalidInput("TIFF sample buffer ended before a complete row".into())
293            })?;
294            output.extend_from_slice(row);
295        }
296    } else {
297        for y in 0..height as usize {
298            for x in 0..width as usize {
299                if is_cmyk {
300                    let mut cmyk = [0u8; 5];
301                    for (channel, slot) in cmyk.iter_mut().take(usize::from(channels)).enumerate() {
302                        *slot = read_tiff_sample(
303                            decoded,
304                            layout,
305                            x,
306                            y,
307                            channel,
308                            channels,
309                            bits,
310                            sample_bytes,
311                            row_stride,
312                            plane_stride,
313                            packed,
314                        )?;
315                    }
316                    output.extend_from_slice(&cmyk_to_rgb(cmyk[0], cmyk[1], cmyk[2], cmyk[3]));
317                    if channels == 5 {
318                        output.push(cmyk[4]);
319                    }
320                    continue;
321                }
322                for channel in 0..usize::from(channels) {
323                    let value = read_tiff_sample(
324                        decoded,
325                        layout,
326                        x,
327                        y,
328                        channel,
329                        channels,
330                        bits,
331                        sample_bytes,
332                        row_stride,
333                        plane_stride,
334                        packed,
335                    )?;
336                    output.push(value);
337                }
338            }
339        }
340    }
341
342    let mut png_bytes = Vec::new();
343    {
344        let mut encoder = png::Encoder::new(&mut png_bytes, width, height);
345        encoder.set_color(png_color);
346        encoder.set_depth(png::BitDepth::Eight);
347        let mut writer = encoder.write_header().map_err(|error| {
348            Error::InvalidInput(format!("could not encode TIFF PNG header: {error}"))
349        })?;
350        writer.write_image_data(&output).map_err(|error| {
351            Error::InvalidInput(format!("could not encode TIFF PNG data: {error}"))
352        })?;
353        writer.finish().map_err(|error| {
354            Error::InvalidInput(format!("could not finish TIFF PNG image: {error}"))
355        })?;
356    }
357    if png_bytes.len().saturating_mul(4).div_ceil(3) > MAX_TIFF_DATA_URI_BYTES {
358        return Err(Error::LimitExceeded(format!(
359            "TIFF PNG data URI would exceed {MAX_TIFF_DATA_URI_BYTES} bytes"
360        )));
361    }
362    Ok(png_bytes)
363}
364
365#[allow(clippy::too_many_arguments)]
366fn read_tiff_sample(
367    decoded: &DecodingResult,
368    layout: &tiff::decoder::BufferLayoutPreference,
369    x: usize,
370    y: usize,
371    channel: usize,
372    channels: u8,
373    bits: u8,
374    sample_bytes: usize,
375    row_stride: usize,
376    plane_stride: usize,
377    packed: bool,
378) -> Result<u8> {
379    let plane = if layout.planes > 1 { channel } else { 0 };
380    let plane_start = plane
381        .checked_mul(plane_stride)
382        .ok_or_else(|| Error::LimitExceeded("TIFF plane offset overflowed".into()))?;
383    let row_start = plane_start
384        .checked_add(
385            y.checked_mul(row_stride)
386                .ok_or_else(|| Error::LimitExceeded("TIFF row offset overflowed".into()))?,
387        )
388        .ok_or_else(|| Error::LimitExceeded("TIFF row offset overflowed".into()))?;
389    if packed {
390        let bit_offset = x
391            .checked_mul(usize::from(bits))
392            .ok_or_else(|| Error::LimitExceeded("TIFF packed sample offset overflowed".into()))?;
393        let byte_offset = row_start
394            .checked_add(bit_offset / 8)
395            .ok_or_else(|| Error::LimitExceeded("TIFF packed sample offset overflowed".into()))?;
396        let shift = 8 - bits - (bit_offset % 8) as u8;
397        let maximum = (1u16 << bits) - 1;
398        let value = match decoded {
399            DecodingResult::U8(data) => *data
400                .get(byte_offset)
401                .ok_or_else(|| Error::InvalidInput("TIFF sample buffer ended early".into()))?,
402            _ => {
403                return Err(Error::Unsupported(
404                    "packed TIFF samples were not decoded as 8-bit values".into(),
405                ));
406            }
407        };
408        let sample = (u16::from(value) >> shift) & maximum;
409        return Ok(((u32::from(sample) * 255 + u32::from(maximum) / 2) / u32::from(maximum)) as u8);
410    }
411
412    let pixel_samples = if layout.planes > 1 {
413        x
414    } else {
415        x * usize::from(channels) + channel
416    };
417    let offset = row_start
418        .checked_add(
419            pixel_samples
420                .checked_mul(sample_bytes)
421                .ok_or_else(|| Error::LimitExceeded("TIFF sample offset overflowed".into()))?,
422        )
423        .ok_or_else(|| Error::LimitExceeded("TIFF sample offset overflowed".into()))?;
424    match (decoded, sample_bytes) {
425        (DecodingResult::U8(data), 1) => data
426            .get(offset)
427            .copied()
428            .ok_or_else(|| Error::InvalidInput("TIFF sample buffer ended early".into())),
429        (DecodingResult::U16(data), 2) => {
430            if offset % 2 != 0 {
431                return Err(Error::InvalidInput(
432                    "TIFF 16-bit sample is misaligned".into(),
433                ));
434            }
435            let sample = *data
436                .get(offset / 2)
437                .ok_or_else(|| Error::InvalidInput("TIFF sample buffer ended early".into()))?;
438            Ok(((u32::from(sample) * 255 + 32767) / 65535) as u8)
439        }
440        (DecodingResult::U32(data), 4) => {
441            if offset % 4 != 0 {
442                return Err(Error::InvalidInput(
443                    "TIFF 32-bit sample is misaligned".into(),
444                ));
445            }
446            let sample = *data
447                .get(offset / 4)
448                .ok_or_else(|| Error::InvalidInput("TIFF sample buffer ended early".into()))?;
449            Ok(((u64::from(sample) * 255 + u64::from(u32::MAX) / 2) / u64::from(u32::MAX)) as u8)
450        }
451        (DecodingResult::U64(data), 8) => {
452            if offset % 8 != 0 {
453                return Err(Error::InvalidInput(
454                    "TIFF 64-bit sample is misaligned".into(),
455                ));
456            }
457            let sample = *data
458                .get(offset / 8)
459                .ok_or_else(|| Error::InvalidInput("TIFF sample buffer ended early".into()))?;
460            Ok(
461                ((u128::from(sample) * 255 + u128::from(u64::MAX) / 2) / u128::from(u64::MAX))
462                    as u8,
463            )
464        }
465        _ => Err(Error::Unsupported(format!(
466            "TIFF {bits}-bit integer samples use an unsupported decoder buffer type"
467        ))),
468    }
469}
470
471fn decoded_buffer_len(decoded: &DecodingResult) -> usize {
472    match decoded {
473        DecodingResult::U8(values) => values.len(),
474        DecodingResult::U16(values) => values.len().saturating_mul(2),
475        DecodingResult::U32(values) => values.len().saturating_mul(4),
476        DecodingResult::U64(values) => values.len().saturating_mul(8),
477        DecodingResult::I8(values) => values.len(),
478        DecodingResult::I16(values) => values.len().saturating_mul(2),
479        DecodingResult::I32(values) => values.len().saturating_mul(4),
480        DecodingResult::I64(values) => values.len().saturating_mul(8),
481        DecodingResult::F16(values) => values.len().saturating_mul(2),
482        DecodingResult::F32(values) => values.len().saturating_mul(4),
483        DecodingResult::F64(values) => values.len().saturating_mul(8),
484    }
485}
486
487fn cmyk_to_rgb(cyan: u8, magenta: u8, yellow: u8, black: u8) -> [u8; 3] {
488    let convert =
489        |ink: u8| ((u16::from(u8::MAX - ink) * u16::from(u8::MAX - black) + 127) / 255) as u8;
490    [convert(cyan), convert(magenta), convert(yellow)]
491}
492
493fn page_resolution<R: std::io::Read + std::io::Seek>(decoder: &mut Decoder<R>) -> (f64, f64) {
494    let unit = decoder
495        .find_tag_unsigned::<u16>(Tag::ResolutionUnit)
496        .ok()
497        .flatten()
498        .unwrap_or(2);
499    let multiplier = match unit {
500        2 => 1.0,
501        3 => 2.54,
502        _ => return (72.0, 72.0),
503    };
504    let read_dpi = |decoder: &mut Decoder<R>, tag| {
505        decoder
506            .get_tag_f64(tag)
507            .ok()
508            .filter(|resolution| resolution.is_finite() && *resolution > 0.0)
509            .map(|resolution| resolution * multiplier)
510            .filter(|dpi| (1.0..=100_000.0).contains(dpi))
511            .unwrap_or(72.0)
512    };
513    (
514        read_dpi(decoder, Tag::XResolution),
515        read_dpi(decoder, Tag::YResolution),
516    )
517}
518
519fn orientation_transform(
520    orientation: u16,
521    width: u32,
522    height: u32,
523    scale_x: f64,
524    scale_y: f64,
525) -> [f64; 6] {
526    let w = f64::from(width);
527    let h = f64::from(height);
528    let [a, b, c, d, e, f] = match orientation {
529        2 => [-1.0, 0.0, 0.0, 1.0, w, 0.0],
530        3 => [-1.0, 0.0, 0.0, -1.0, w, h],
531        4 => [1.0, 0.0, 0.0, -1.0, 0.0, h],
532        5 => [0.0, 1.0, 1.0, 0.0, 0.0, 0.0],
533        6 => [0.0, 1.0, -1.0, 0.0, h, 0.0],
534        7 => [0.0, -1.0, -1.0, 0.0, h, w],
535        8 => [0.0, -1.0, 1.0, 0.0, 0.0, w],
536        _ => IDENTITY,
537    };
538    [
539        scale_x * a,
540        scale_y * b,
541        scale_x * c,
542        scale_y * d,
543        scale_x * e,
544        scale_y * f,
545    ]
546}
547
548fn map_tiff_error(error: tiff::TiffError, context: &str) -> Error {
549    match error {
550        tiff::TiffError::LimitsExceeded | tiff::TiffError::IntSizeError => {
551            Error::LimitExceeded(format!("{context}: decoder limits exceeded"))
552        }
553        error @ tiff::TiffError::UnsupportedError(_) => {
554            Error::Unsupported(format!("{context}: {error}"))
555        }
556        error => Error::InvalidInput(format!("{context}: {error}")),
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::orientation_transform;
563
564    #[test]
565    fn all_tiff_orientations_map_to_the_oriented_page_bounds() {
566        let width = 13.0;
567        let height = 7.0;
568        let corners = [(0.0, 0.0), (width, 0.0), (0.0, height), (width, height)];
569        for orientation in 1..=8 {
570            let [a, b, c, d, e, f] = orientation_transform(orientation, 13, 7, 1.0, 1.0);
571            let mapped = corners.map(|(x, y)| (a * x + c * y + e, b * x + d * y + f));
572            let min_x = mapped
573                .iter()
574                .map(|point| point.0)
575                .fold(f64::INFINITY, f64::min);
576            let max_x = mapped
577                .iter()
578                .map(|point| point.0)
579                .fold(f64::NEG_INFINITY, f64::max);
580            let min_y = mapped
581                .iter()
582                .map(|point| point.1)
583                .fold(f64::INFINITY, f64::min);
584            let max_y = mapped
585                .iter()
586                .map(|point| point.1)
587                .fold(f64::NEG_INFINITY, f64::max);
588            let (expected_width, expected_height) = if orientation >= 5 {
589                (height, width)
590            } else {
591                (width, height)
592            };
593            assert_eq!(
594                (min_x, max_x, min_y, max_y),
595                (0.0, expected_width, 0.0, expected_height)
596            );
597        }
598    }
599}