1use 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}