//! CMYK images: the ink amounts of image blobs that hold CMYK samples. //! //! Two encodings hold them: a four-component JPEG, whose samples are stored inverted with Adobe's //! marker (the convention JPEG readers and PDF writers take a CMYK JPEG to follow), and a CMYK //! TIFF (8 or 16 bits, ink amounts as they are, each pixel's inks together or one plane per ink). //! [`ImageBlob::cmyk_tiff`] writes the latter, inks together, Deflate-compressed. Every other image //! decodes to RGB(A) only. use std::io::Cursor; use std::sync::Arc; use zune_jpeg::zune_core::bytestream::ZCursor; use zune_jpeg::zune_core::colorspace::ColorSpace; use zune_jpeg::zune_core::options::DecoderOptions; use crate::ImageBlob; /// Most pixels a CMYK image is decoded with (256 MB of samples): larger ones decode as RGB. pub const MAX_CMYK_PIXELS: u64 = 1 << 26; /// The ink amounts of a CMYK image: C, M, Y and K bytes per pixel, 0 is no ink. #[derive(Clone, Debug, PartialEq)] pub struct Inks { pub width: u32, pub height: u32, pub data: Vec, } impl Inks { /// `data` as a `width` × `height` image; `None` when it doesn't hold that many pixels, the /// image is empty or it has more than [`MAX_CMYK_PIXELS`]. pub fn new(width: u32, height: u32, data: Vec) -> Option { let n = pixels(width, height)?; (data.len() as u64 == n * 4).then_some(Self { width, height, data }) } } /// `width` × `height` when it is a CMYK image's size: not empty, at most [`MAX_CMYK_PIXELS`]. fn pixels(width: u32, height: u32) -> Option { let n = width as u64 * height as u64; (n > 0 && n <= MAX_CMYK_PIXELS).then_some(n) } /// The number of colour components of JPEG `bytes` (its frame header); `None` when they aren't a /// JPEG. pub fn jpeg_components(bytes: &[u8]) -> Option { if !bytes.starts_with(&[0xFF, 0xD8]) { return None; } let mut d = zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(bytes), DecoderOptions::default()); d.decode_headers().ok()?; Some(d.info()?.components as usize) } fn is_tiff(bytes: &[u8]) -> bool { bytes.starts_with(b"II*\0") || bytes.starts_with(b"MM\0*") } /// The width and height of the first image of TIFF `bytes` when it has more strips or tiles than /// the `tiff` decoder can count: strips per plane times planes past `u32::MAX`, or tiles per plane /// times planes past `usize::MAX`. A file that stores one plane per channel (PlanarConfiguration 2) /// has a plane for each channel; other files have one. The decoder multiplies these counts without /// a check, which panics where overflow checks are on. Such an image, when it is at least a pixel /// wide, claims at least 2^32 samples. /// Where a tag repeats, the value giving the most strips or tiles counts. `None` for other images /// and for bytes that aren't a TIFF. pub fn oversized_tiff(bytes: &[u8]) -> Option<(u32, u32)> { let le = bytes.starts_with(b"II*\0"); if !le && !bytes.starts_with(b"MM\0*") { return None; } let u16_at = |at: usize| -> Option { let b: [u8; 2] = bytes.get(at..at.checked_add(2)?)?.try_into().ok()?; Some(if le { u16::from_le_bytes(b) } else { u16::from_be_bytes(b) }) }; let u32_at = |at: usize| -> Option { let b: [u8; 4] = bytes.get(at..at.checked_add(4)?)?.try_into().ok()?; Some(if le { u32::from_le_bytes(b) } else { u32::from_be_bytes(b) }) }; let ifd = usize::try_from(u32_at(4)?).ok()?; // (tag, the entry's single BYTE, SHORT, LONG or IFD value, which sits in the entry, or its // single LONG8 or IFD8 value that fits in 32 bits, which sits where the entry points) let entry = |i: usize| -> Option<(u16, Option)> { let e = ifd.checked_add(i.checked_mul(12)?.checked_add(2)?)?; let (ty, count, at) = (u16_at(e.checked_add(2)?)?, u32_at(e.checked_add(4)?)?, e.checked_add(8)?); let value = match (ty, count) { (1, 1) => bytes.get(at).map(|&b| u32::from(b)), (3, 1) => u16_at(at).map(u32::from), (4 | 13, 1) => u32_at(at), (16 | 18, 1) => { let at = usize::try_from(u32_at(at)?).ok()?; let (lo, hi) = (u32_at(at)?, u32_at(at.checked_add(4)?)?); let wide = if le { u64::from(hi) << 32 | u64::from(lo) } else { u64::from(lo) << 32 | u64::from(hi) }; u32::try_from(wide).ok() } _ => None, }; Some((u16_at(e)?, value)) }; let (mut width, mut height, mut planes, mut planar) = (0u32, 0u32, 1u32, false); let (mut rows, mut tile_w, mut tile_h) = (None::, None::, None::); let least = |was: Option, v: u32| Some(was.map_or(v, |w| w.min(v))); for i in 0..usize::from(u16_at(ifd)?) { let Some((tag, value)) = entry(i) else { break }; let Some(v) = value else { continue }; match tag { 256 => width = width.max(v), 257 => height = height.max(v), 277 => planes = planes.max(v), 278 => rows = least(rows, v), 284 => planar |= v == 2, 322 => tile_w = least(tile_w, v), 323 => tile_h = least(tile_h, v), _ => {} } } let planes = if planar { u128::from(planes) } else { 1 }; let tiles = match (tile_w, tile_h) { (Some(tw @ 1..), Some(th @ 1..)) => u128::from(width.div_ceil(tw)) * u128::from(height.div_ceil(th)) * planes > usize::MAX as u128, _ => false, }; let rows = rows.unwrap_or(height); let strips = rows > 0 && u128::from(height.saturating_sub(1) / rows + 1) * planes > u128::from(u32::MAX); (tiles || strips).then_some((width, height)) } /// The color type of TIFF `bytes` and its decoder; `None` for [`oversized_tiff`] images too. fn tiff(bytes: &[u8]) -> Option<(tiff::ColorType, tiff::decoder::Decoder>)> { if !is_tiff(bytes) || oversized_tiff(bytes).is_some() { return None; } let mut d = tiff::decoder::Decoder::new(Cursor::new(bytes)).ok()?; Some((d.colortype().ok()?, d)) } /// Does the TIFF `d` reads store one plane per channel (PlanarConfiguration 2)? fn planar(d: &mut tiff::decoder::Decoder>) -> bool { d.find_tag_unsigned::(tiff::tags::Tag::PlanarConfiguration).ok().flatten().is_some_and(|p| p != 1) } /// The samples of the `n` pixels of the TIFF `d` reads, `per` to a pixel (the number its color /// type has), pixel by pixel and 8 bits each (16-bit samples to their high byte), whether the file /// stores each pixel's samples together or one plane per channel. `None` when they don't decode to /// that many samples, which includes images whose planes together need more than the decoder's /// limit (256 MB of samples). fn read_samples(d: &mut tiff::decoder::Decoder>, n: usize, per: usize) -> Option> { let mut buf = tiff::decoder::DecodingResult::U8(Vec::new()); let layout = d.read_image_to_buffer(&mut buf).ok()?; let data = match buf { tiff::decoder::DecodingResult::U8(v) => v, tiff::decoder::DecodingResult::U16(v) => v.iter().map(|s| (s >> 8) as u8).collect(), _ => return None, }; if Some(data.len()) != n.checked_mul(per) { return None; } if layout.planes <= 1 { return Some(data); } if layout.planes != per { return None; } let planes: Vec<&[u8]> = data.chunks_exact(n).collect(); Some((0..n).flat_map(|i| planes.iter().map(move |p| p.get(i).copied().unwrap_or(0))).collect()) } impl ImageBlob { /// A CMYK TIFF of ink amounts `inks` (Deflate-compressed); `None` when it can't be written. pub fn cmyk_tiff(inks: &Inks) -> Option { Self::cmyk_tiff_at(inks, None) } /// [`ImageBlob::cmyk_tiff`], declaring the resolution `ppi` (horizontal, vertical pixels per /// inch) when it is given and both are finite and above 0. pub fn cmyk_tiff_at(inks: &Inks, ppi: Option<(f64, f64)>) -> Option { use tiff::encoder::{Compression, Rational, TiffEncoder, colortype::CMYK8, compression::DeflateLevel}; use tiff::tags::{ResolutionUnit, Tag}; let mut out = Cursor::new(Vec::new()); let mut enc = TiffEncoder::new(&mut out).ok()?.with_compression(Compression::Deflate(DeflateLevel::Balanced)); let mut img = enc.new_image::(inks.width, inks.height).ok()?; if let Some((x, y)) = ppi.filter(|(x, y)| [x, y].iter().all(|v| v.is_finite() && **v > 0.0)) { // Ten-thousandths of a pixel per inch. let r = |v: f64| Rational { n: (v * 10_000.0).round() as u32, d: 10_000 }; img.encoder().write_tag(Tag::XResolution, r(x)).ok()?; img.encoder().write_tag(Tag::YResolution, r(y)).ok()?; img.encoder().write_tag(Tag::ResolutionUnit, ResolutionUnit::Inch).ok()?; } img.write_data(&inks.data).ok()?; Some(Self::new("image/tiff", out.into_inner())) } /// Does the image hold CMYK samples (a four-component JPEG or a CMYK TIFF)? Reads only its /// header. pub fn is_cmyk(&self) -> bool { jpeg_components(&self.bytes) == Some(4) || tiff(&self.bytes).is_some_and(|(t, _)| matches!(t, tiff::ColorType::CMYK(8 | 16))) } /// The ink amounts of a CMYK image; `None` for other images, and ones that can't be decoded /// or are larger than [`MAX_CMYK_PIXELS`]. pub fn cmyk(&self) -> Option { let bytes = self.bytes.as_slice(); if bytes.starts_with(&[0xFF, 0xD8]) { jpeg_inks(bytes) } else { tiff_inks(bytes) } } /// The ink amounts around pixel (`x`, `y`) of a CMYK image, in the pixel space of an image /// object of `object` (width, height) pixels: the average of the `size` × `size` pixels centered /// on it, clipped to the image, as C, M, Y and K bytes. `None` outside the image and for images /// [`Self::cmyk`] doesn't read. The inks of the last CMYK image are kept for the next sample /// (the Eyedropper samples one image click after click), as [`Self::sample_object`] keeps its /// pixels. pub fn sample_cmyk_object(&self, x: f64, y: f64, object: (u32, u32), size: u32) -> Option<[u8; 4]> { thread_local! { static LAST: std::cell::RefCell)>> = const { std::cell::RefCell::new(None) }; } if !self.is_cmyk() { return None; } let key = self.content_key(); let inks = LAST.with_borrow_mut(|last| match last { Some((k, inks)) if *k == key => Some(inks.clone()), _ => { let inks = Arc::new(self.cmyk()?); *last = Some((key, inks.clone())); Some(inks) } })?; let k = |n: u32, of: u32| n as f64 / of.max(1) as f64; sample_inks(&inks, x * k(inks.width, object.0), y * k(inks.height, object.1), size) } } /// [`ImageBlob::sample_cmyk_object`] of `inks` at (`x`, `y`) in their own pixel space. fn sample_inks(inks: &Inks, x: f64, y: f64, size: u32) -> Option<[u8; 4]> { let (w, h) = (i64::from(inks.width), i64::from(inks.height)); let (cx, cy) = (x.floor() as i64, y.floor() as i64); if !(0..w).contains(&cx) || !(0..h).contains(&cy) { return None; } let r = i64::from(size.max(1)) / 2; let (mut sum, mut n) = ([0u64; 4], 0u64); for py in (cy - r).max(0)..=(cy + r).min(h - 1) { for px in (cx - r).max(0)..=(cx + r).min(w - 1) { let i = usize::try_from((py * w + px) * 4).ok()?; let p = inks.data.get(i..i + 4)?; sum.iter_mut().zip(p).for_each(|(s, v)| *s += u64::from(*v)); n += 1; } } let c = |v: u64| ((v + n / 2) / n) as u8; Some(sum.map(c)) } /// The ink amounts of a four-component JPEG (its samples inverted back; YCCK converted to CMYK /// first, as JPEG readers do). fn jpeg_inks(bytes: &[u8]) -> Option { let mut d = zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(bytes), DecoderOptions::default()); d.decode_headers().ok()?; let space = d.input_colorspace()?; if d.info()?.components != 4 || !matches!(space, ColorSpace::CMYK | ColorSpace::YCCK) { return None; } let (w, h) = d.dimensions()?; let (w, h) = (u32::try_from(w).ok()?, u32::try_from(h).ok()?); pixels(w, h)?; d.set_options(DecoderOptions::default().jpeg_set_out_colorspace(space)); let mut data = d.decode().ok()?; for p in data.as_chunks_mut::<4>().0 { if space == ColorSpace::YCCK { let [y, cb, cr] = [p[0], p[1], p[2]].map(f32::from); // libjpeg's YCCK → CMYK: the complement of the YCbCr colour's RGB. p[0] = (434.456 - y - 1.402 * cr).clamp(0.0, 255.0) as u8; p[1] = (119.541 - y + 0.344 * cb + 0.714 * cr).clamp(0.0, 255.0) as u8; p[2] = (481.816 - y - 1.772 * cb).clamp(0.0, 255.0) as u8; } for v in p.iter_mut() { *v = 255 - *v; } } Inks::new(w, h, data) } /// The ink amounts of a CMYK TIFF (16-bit samples to 8 bits), its inks stored together or one plane /// per ink. fn tiff_inks(bytes: &[u8]) -> Option { let (kind, mut d) = tiff(bytes)?; let (w, h) = d.dimensions().ok()?; if !matches!(kind, tiff::ColorType::CMYK(8 | 16)) { return None; } let n = usize::try_from(pixels(w, h)?).ok()?; Inks::new(w, h, read_samples(&mut d, n, 4)?) } /// CMYK TIFF `bytes` as Open and Place store them: as they are, or, when they store one plane per /// ink, which the image decoder doesn't read, their ink amounts written again by /// [`ImageBlob::cmyk_tiff_at`] with the resolution `ppi`. `None` for other images and for CMYK /// TIFFs whose inks [`ImageBlob::cmyk`] doesn't read. pub fn stored_cmyk_tiff(bytes: &[u8], ppi: Option<(f64, f64)>) -> Option> { let inks = tiff_inks(bytes)?; let (_, mut d) = tiff(bytes)?; if !planar(&mut d) { return Some(bytes.to_vec()); } ImageBlob::cmyk_tiff_at(&inks, ppi).map(|b| Arc::unwrap_or_clone(b.bytes)) } /// A TIFF whose pixels have samples after their colour, which the image decoder doesn't read at /// all, as RGBA pixels: CMYK with an alpha channel (5 samples), each colour converted by `rgb` (ink /// amounts 0..1 → sRGB 0..1; the inks can't be kept with the transparency, so they show as RGB), /// and grayscale with extra samples, of which the first is the alpha when the file says so (other /// channels are left out). Associated (premultiplied) alpha is divided back out. The samples may be /// stored together or one plane per channel. `None` for other images and ones larger than /// [`MAX_CMYK_PIXELS`]. pub fn extra_samples_tiff_rgba(bytes: &[u8], rgb: impl Fn([f32; 4]) -> [f32; 3]) -> Option { use tiff::tags::Tag; let (kind, mut d) = tiff(bytes)?; let (w, h) = d.dimensions().ok()?; // 0: white is zero, 1: black is zero (the two grayscales). let photometric = d.find_tag_unsigned::(Tag::PhotometricInterpretation).ok().flatten(); // ExtraSamples 1: associated alpha (premultiplied colour), 2: unassociated alpha, 0: not alpha. let extra = d.find_tag_unsigned_vec::(Tag::ExtraSamples).ok().flatten().and_then(|v| v.first().copied()); // (samples in a pixel, how many of them are its colour, is the next one alpha?) let (samples, colour, alpha) = match kind { tiff::ColorType::CMYKA(8 | 16) => (5, 4, true), tiff::ColorType::Multiband { bit_depth: 8 | 16, num_samples: n @ 2.. } if matches!(photometric, Some(0 | 1)) => { (usize::from(n), 1, matches!(extra, Some(1 | 2))) } _ => return None, }; let n = usize::try_from(pixels(w, h)?).ok()?; let data = read_samples(&mut d, n, samples)?; let mut out = Vec::with_capacity(n * 4); for p in data.chunks_exact(samples) { let opacity = if alpha { p.get(colour).copied().unwrap_or(u8::MAX) } else { u8::MAX }; let a = f32::from(opacity) / 255.0; let level = |v: u8| { let v = f32::from(v) / 255.0; if extra == Some(1) && a > 0.0 { (v / a).min(1.0) } else { v } }; let [r, g, b] = match *p { [c, m, y, k, ..] if colour == 4 => rgb([level(c), level(m), level(y), level(k)]), [v, ..] if photometric == Some(0) => [1.0 - level(v); 3], [v, ..] => [level(v); 3], [] => continue, }; out.extend([r, g, b].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)); out.push(opacity); } image::RgbaImage::from_raw(w, h, out) } /// Why a TIFF compressed with CCITT Group 3 wasn't read ([`group3_tiff_rgba`]). #[derive(Clone, Debug, PartialEq, thiserror::Error)] pub enum Group3Error { /// Its pixels would take more than [`crate::MAX_RASTER_ALLOC`] as RGBA. #[error("an image of {w} × {h} pixels is too large to draw")] TooLarge { w: u32, h: u32 }, /// A strip or tile ends before its last row or can't be decoded (it holds a code the decoder /// doesn't read, such as T.4's uncompressed mode, or a row of the wrong width), or a tile is /// larger than [`crate::MAX_RASTER_ALLOC`] allows as RGBA. #[error("could not decode image: {0}")] Damaged(hayro_ccitt::DecodeError), } /// The pixels of a 1-bit TIFF compressed with CCITT Group 3, which the image decoder doesn't read, /// as RGBA. Compression 2 is modified Huffman coding with each row starting at a byte and no EOL /// codes. Compression 3 is ITU-T T.4 coding with an EOL code before each row: one-dimensional rows, /// or with T4Options bit 0, rows that a tag bit marks as one-dimensional or as coded against the /// row above. Fill bits before an EOL are skipped. Rows in T.4's uncompressed mode (T4Options /// bit 1) can't be decoded. Each strip or tile is coded on its own. White runs are 0 bits and black /// runs are 1 bits, as libtiff writes them, and PhotometricInterpretation 0 shows 0 bits as white /// and 1 shows them as black. FillOrder 2 (each byte's lowest bit first) is read too. Together, the /// strips and tiles are read from at most as many bytes as the file has. `None` for other images; /// an error when the pixels would take more than [`crate::MAX_RASTER_ALLOC`] as RGBA, a tile is /// larger than that allowance, or a strip or tile can't be decoded or ends before its last row. pub fn group3_tiff_rgba(bytes: &[u8]) -> Option> { use hayro_ccitt::{DecodeError, DecodeSettings, DecoderContext, EncodingMode}; use tiff::tags::Tag; let (kind, mut d) = tiff(bytes)?; let compression = d.find_tag_unsigned::(Tag::Compression).ok().flatten()?; let (w, h) = d.dimensions().ok()?; if kind != tiff::ColorType::Gray(1) || !matches!(compression, 2 | 3) || w == 0 || h == 0 { return None; } // The level of a 0 bit (a white run): white when white is zero (0), black when black is zero (1). let zero = match d.find_tag_unsigned::(Tag::PhotometricInterpretation).ok().flatten() { Some(0) => u8::MAX, Some(1) => 0, _ => return None, }; if u64::from(w) * u64::from(h) > crate::MAX_RASTER_ALLOC / 4 { return Some(Err(Group3Error::TooLarge { w, h })); } let t4_options = d.find_tag_unsigned::(Tag::Unknown(292)).ok().flatten().unwrap_or(0); let lsb_first = d.find_tag_unsigned::(Tag::FillOrder).ok().flatten() == Some(2); let encoding = if compression == 3 && t4_options & 1 != 0 { // Each row's tag bit says how it is coded. EncodingMode::Group3_2D { k: 2 } } else { EncodingMode::Group3_1D }; // The width and height of each strip or tile, and the tags of their offsets and byte counts. let tiled = d.get_chunk_type() == tiff::decoder::ChunkType::Tile; let (cw, ch, offsets, counts) = if tiled { let tw = d.find_tag_unsigned::(Tag::TileWidth).ok().flatten().filter(|&v| v > 0)?; let th = d.find_tag_unsigned::(Tag::TileLength).ok().flatten().filter(|&v| v > 0)?; (tw, th, Tag::TileOffsets, Tag::TileByteCounts) } else { let rows = d.find_tag_unsigned::(Tag::RowsPerStrip).ok().flatten().filter(|&v| v > 0).map_or(h, |r| r.min(h)); (w, rows, Tag::StripOffsets, Tag::StripByteCounts) }; let damaged = |e: DecodeError| Some(Err(Group3Error::Damaged(e))); // A tile is held to the allowance the image is held to; a strip is no larger than the image. if u64::from(cw) * u64::from(ch) > crate::MAX_RASTER_ALLOC / 4 { return damaged(DecodeError::Overflow); } let offsets = d.find_tag_unsigned_vec::(offsets).ok().flatten().unwrap_or_default(); let counts = d.find_tag_unsigned_vec::(counts).ok().flatten().unwrap_or_default(); let across = w.div_ceil(cw); let chunks = u64::from(across) * u64::from(h.div_ceil(ch)); if chunks > offsets.len().min(counts.len()) as u64 { return damaged(DecodeError::UnexpectedEof); } let mut gray = vec![zero; usize::try_from(u64::from(w) * u64::from(h)).ok()?]; let mut budget = bytes.len(); for (i, (&offset, &count)) in (0..chunks).zip(offsets.iter().zip(&counts)) { let (x0, y0) = (u32::try_from(i % u64::from(across)).ok()? * cw, u32::try_from(i / u64::from(across)).ok()? * ch); let Some(start) = usize::try_from(offset).ok().filter(|&s| s <= bytes.len()) else { return damaged(DecodeError::UnexpectedEof); }; let len = usize::try_from(count).unwrap_or(usize::MAX).min(bytes.len() - start).min(budget); budget -= len; let data = bytes.get(start..start + len)?; let data: std::borrow::Cow<[u8]> = if lsb_first { data.iter().map(|b| b.reverse_bits()).collect() } else { data.into() }; let settings = DecodeSettings { columns: cw, // A tile is coded whole; the last strip only has the image's remaining rows. rows: if tiled { ch } else { ch.min(h - y0) }, end_of_block: compression == 3, end_of_line: compression == 3, rows_are_byte_aligned: compression == 2, encoding, invert_black: false, }; let mut rows = Rows { out: &mut gray, width: w, height: h, x0, y0, x: 0, y: 0, zero }; if let Err(e) = hayro_ccitt::decode(&data, &mut rows, &mut DecoderContext::new(settings)) { return damaged(e); } // The data ended, or six EOL codes came, before the strip's or tile's last row. if rows.y < settings.rows { return damaged(DecodeError::UnexpectedEof); } } let gray = image::GrayImage::from_raw(w, h, gray)?; Some(Ok(image::DynamicImage::ImageLuma8(gray).to_rgba8())) } /// Where a strip or tile of a Group 3 TIFF ([`group3_tiff_rgba`]) puts its decoded pixels: `out` /// holds the image's levels row by row, `width` × `height`, and the strip or tile starts at (`x0`, /// `y0`). Pixels past the image's right or bottom edge (a tile's padding) are dropped. struct Rows<'a> { out: &'a mut [u8], width: u32, height: u32, x0: u32, y0: u32, /// The next pixel in the strip or tile. x: u64, y: u32, /// The level of a white run. zero: u8, } impl Rows<'_> { /// The next `n` pixels of the row, a white run's or a black run's. fn run(&mut self, white: bool, n: u64) { let level = if white { self.zero } else { u8::MAX - self.zero }; let row = u64::from(self.y0) + u64::from(self.y); let left = u64::from(self.x0) + self.x; let right = left.saturating_add(n).min(u64::from(self.width)); if row < u64::from(self.height) && left < right { let at = row * u64::from(self.width); if let (Ok(a), Ok(b)) = (usize::try_from(at + left), usize::try_from(at + right)) && let Some(px) = self.out.get_mut(a..b) { px.fill(level); } } self.x = self.x.saturating_add(n); } } impl hayro_ccitt::Decoder for Rows<'_> { fn push_pixel(&mut self, white: bool) { self.run(white, 1); } fn push_pixel_chunk(&mut self, white: bool, chunk_count: u32) { self.run(white, u64::from(chunk_count) * 8); } fn next_line(&mut self) { self.y = self.y.saturating_add(1); self.x = 0; } } #[cfg(test)] mod tests { use super::*; #[test] fn a_cmyk_tiff_with_alpha_reads_as_rgba() { use tiff::encoder::{TiffEncoder, colortype::CMYKA8}; // 2 × 1: full cyan opaque, then 100% K at half alpha (premultiplied: K 128, alpha 128). let mut file = Cursor::new(Vec::new()); let mut enc = TiffEncoder::new(&mut file).unwrap(); let mut img = enc.new_image::(2, 1).unwrap(); img.encoder().write_tag(tiff::tags::Tag::ExtraSamples, &[1u16][..]).unwrap(); img.write_data(&[255, 0, 0, 0, 255, 0, 0, 0, 128, 128]).unwrap(); let bytes = file.into_inner(); let naive = |[c, m, y, k]: [f32; 4]| [(1.0 - c) * (1.0 - k), (1.0 - m) * (1.0 - k), (1.0 - y) * (1.0 - k)]; let px = extra_samples_tiff_rgba(&bytes, naive).expect("CMYKA reads"); assert_eq!(px.dimensions(), (2, 1)); assert_eq!(px.get_pixel(0, 0).0, [0, 255, 255, 255], "cyan, opaque"); assert_eq!(px.get_pixel(1, 0).0, [0, 0, 0, 128], "the premultiplied K divided back out: black at half alpha"); // Not CMYKA: not this reader's. assert!(extra_samples_tiff_rgba(&ImageBlob::cmyk_tiff(&inks()).unwrap().bytes, naive).is_none()); } /// Two 8-bit samples a pixel: gray and one extra sample, which the file's `ExtraSamples` names. struct GrayExtra8; impl tiff::encoder::colortype::ColorType for GrayExtra8 { type Inner = u8; const TIFF_VALUE: tiff::tags::PhotometricInterpretation = tiff::tags::PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8]; const SAMPLE_FORMAT: &'static [tiff::tags::SampleFormat] = &[tiff::tags::SampleFormat::Uint; 2]; fn horizontal_predict(row: &[u8], result: &mut Vec) { result.extend_from_slice(row); } } /// A grayscale TIFF with an alpha channel, or with a channel that isn't one (#1466): the image /// decoder reads neither. #[test] fn a_grayscale_tiff_with_an_extra_channel_reads_as_rgba() { let gray = |extra: u16| { let mut file = Cursor::new(Vec::new()); let mut enc = tiff::encoder::TiffEncoder::new(&mut file).unwrap(); let mut img = enc.new_image::(3, 1).unwrap(); img.encoder().write_tag(tiff::tags::Tag::ExtraSamples, &[extra][..]).unwrap(); // White opaque, a gray of 64 at 80% alpha (premultiplied: 51), black with no alpha. img.write_data(&[255, 255, 51, 204, 0, 0]).unwrap(); let bytes = file.into_inner(); assert!(image::load_from_memory(&bytes).is_err(), "the image decoder reads it now: this reader may go"); extra_samples_tiff_rgba(&bytes, |_| [0.0; 3]).expect("gray with an extra sample reads").into_raw() }; assert_eq!(gray(1), [255, 255, 255, 255, 64, 64, 64, 204, 0, 0, 0, 0], "associated alpha, divided back out"); assert_eq!(gray(2), [255, 255, 255, 255, 51, 51, 51, 204, 0, 0, 0, 0], "unassociated alpha"); assert_eq!(gray(0), [255, 255, 255, 255, 51, 51, 51, 255, 0, 0, 0, 255], "a channel that isn't alpha: opaque"); } /// A header claiming more tiles than can be counted: (2^32 − 1)² pixels in tiles of one pixel, /// times 2 planes. #[test] fn tiffs_with_more_tiles_than_can_be_counted_are_oversized() { let header = |planar: u32| { let mut f = b"II*\0\x08\0\0\0".to_vec(); let entries = [(256u16, u32::MAX), (257, u32::MAX), (258, 8), (262, 1), (277, 2), (284, planar), (322, 1), (323, 1), (324, 0), (325, 0)]; f.extend(u16::try_from(entries.len()).unwrap().to_le_bytes()); for (tag, v) in entries { f.extend([tag.to_le_bytes(), 4u16.to_le_bytes()].concat()); f.extend([1u32.to_le_bytes(), v.to_le_bytes()].concat()); } f.extend([0; 4]); f }; assert_eq!(oversized_tiff(&header(2)), Some((u32::MAX, u32::MAX))); assert_eq!(oversized_tiff(&header(1)), None, "one plane: (2^32 − 1)² tiles can be counted in 64 bits"); assert!(tiff(&header(2)).is_none(), "not decoded"); } /// A header claiming more strips than can be counted: 2^31 + 1 rows, a strip each, times 2 /// planes. The decoder counts strips when the file has strip offsets, tile sizes or not, and /// reads a LONG8 value from where the entry points. #[test] fn tiffs_with_more_strips_than_can_be_counted_are_oversized() { // Each entry holds one value of type 3 (SHORT), 4 (LONG) or 16 (LONG8, after the directory). let header = |entries: &[(u16, u16, u32)]| { let after = 8 + 2 + 12 * entries.len() + 4; let mut f = b"II*\0\x08\0\0\0".to_vec(); f.extend(u16::try_from(entries.len()).unwrap().to_le_bytes()); let mut wide = vec![]; for &(tag, ty, v) in entries { f.extend([tag.to_le_bytes(), ty.to_le_bytes()].concat()); f.extend(1u32.to_le_bytes()); if ty == 16 { f.extend(u32::try_from(after + wide.len()).unwrap().to_le_bytes()); wide.extend(u64::from(v).to_le_bytes()); } else { f.extend(v.to_le_bytes()); } } f.extend([0; 4]); f.extend(wide); f }; let tall = 1u32 << 31 | 1; let strips = [(256, 4, 1), (257, 4, tall), (258, 3, 8), (262, 3, 1), (273, 4, 0), (277, 3, 2), (278, 4, 1), (279, 4, 0), (284, 3, 2)]; assert_eq!(oversized_tiff(&header(&strips)), Some((1, tall))); let tile_sizes = [&strips[..], &[(322, 4, u32::MAX), (323, 4, u32::MAX)]].concat(); assert_eq!(oversized_tiff(&header(&tile_sizes)), Some((1, tall)), "tile sizes without tile offsets"); let mut long8 = strips; long8[1] = (257, 16, tall); assert_eq!(oversized_tiff(&header(&long8)), Some((1, tall)), "ImageLength as a LONG8"); for h in [header(&tile_sizes), header(&long8)] { assert!(tiff(&h).is_none(), "not decoded"); } } fn inks() -> Inks { Inks::new(3, 2, vec![0, 40, 100, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 12, 34, 56, 78]).unwrap() } #[test] fn a_cmyk_tiff_keeps_its_ink_amounts_and_shows_as_rgb() { let b = ImageBlob::cmyk_tiff(&inks()).unwrap(); assert_eq!(b.mime, "image/tiff"); assert!(b.is_cmyk()); assert_eq!(b.cmyk(), Some(inks()), "lossless"); // Every image reader sees an RGB image: cyan is (0, 255, 255). let rgb = image::load_from_memory(&b.bytes).unwrap().to_rgb8(); assert_eq!(rgb.get_pixel(1, 0).0, [0, 255, 255]); } #[test] fn a_cmyk_image_samples_its_ink_amounts() { let b = ImageBlob::cmyk_tiff(&inks()).unwrap(); assert_eq!(b.sample_cmyk_object(0.5, 0.5, (3, 2), 1), Some([0, 40, 100, 0])); // An object of 6 × 4 pixels showing these 3 × 2: (5, 3) is the last pixel. assert_eq!(b.sample_cmyk_object(5.0, 3.0, (6, 4), 1), Some([12, 34, 56, 78])); // 3 × 3 around (1, 0), clipped to the image: all six pixels averaged. assert_eq!(b.sample_cmyk_object(1.5, 0.5, (3, 2), 3), Some([45, 55, 69, 56])); assert_eq!(b.sample_cmyk_object(3.5, 0.5, (3, 2), 1), None, "outside"); let mut png = Vec::new(); image::RgbaImage::new(2, 2).write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); assert_eq!(ImageBlob::png(png).sample_cmyk_object(0.5, 0.5, (2, 2), 1), None, "an RGB image has no inks"); } #[test] fn a_cmyk_jpeg_reads_back_as_ink_amounts() { // A flat CMYK JPEG: Adobe's inverted samples with its marker. let flat = [10u8, 40, 100, 20]; let px: Vec = (0..16 * 16).flat_map(|_| flat).collect(); let mut jpg = Vec::new(); jpeg_encoder::Encoder::new(&mut jpg, 100).encode(&px, 16, 16, jpeg_encoder::ColorType::Cmyk).unwrap(); let b = ImageBlob::new("image/jpeg", jpg); assert_eq!(jpeg_components(&b.bytes), Some(4)); assert!(b.is_cmyk()); let back = b.cmyk().unwrap(); assert_eq!((back.width, back.height), (16, 16)); for p in back.data.as_chunks::<4>().0 { assert!(p.iter().zip(flat).all(|(a, b)| a.abs_diff(b) <= 2), "{p:?}"); } } #[test] fn other_images_are_not_cmyk() { let mut png = Vec::new(); image::RgbaImage::new(2, 2).write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); let mut jpg = Vec::new(); image::RgbImage::new(8, 8).write_to(&mut Cursor::new(&mut jpg), image::ImageFormat::Jpeg).unwrap(); for b in [ImageBlob::png(png), ImageBlob::new("image/jpeg", jpg), ImageBlob::new("image/tiff", b"II*\0junk".to_vec())] { assert!(!b.is_cmyk() && b.cmyk().is_none(), "{}", b.mime); } assert_eq!(Inks::new(2, 2, vec![0; 15]), None, "too few samples"); assert_eq!(Inks::new(0, 2, vec![]), None, "empty"); assert_eq!(Inks::new(1 << 14, 1 << 13, vec![]), None, "too large"); } }