From ce4f11c3b1e8656aa883d87030d490ae8232073c Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Mon, 18 Dec 2023 23:13:16 +0100 Subject: [PATCH] tiff: Support CMYK images --- src/codecs/tiff.rs | 51 +++++++++++++++++++++++++++------ src/color.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++- src/traits.rs | 12 +++++++- 3 files changed, 124 insertions(+), 10 deletions(-) diff --git a/src/codecs/tiff.rs b/src/codecs/tiff.rs index 7ee05e1047..c6e1591a31 100644 --- a/src/codecs/tiff.rs +++ b/src/codecs/tiff.rs @@ -13,13 +13,13 @@ use std::io::{self, Cursor, Read, Seek, Write}; use std::marker::PhantomData; use std::mem; -use crate::color::{ColorType, ExtendedColorType}; +use crate::color::{Cmyk, ColorType, ExtendedColorType, IntoColor}; use crate::error::{ DecodingError, EncodingError, ImageError, ImageResult, LimitError, LimitErrorKind, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; use crate::image::{ImageDecoder, ImageEncoder, ImageFormat}; -use crate::utils; +use crate::{utils, Rgb}; /// Decoder for TIFF images. pub struct TiffDecoder @@ -28,6 +28,7 @@ where { dimensions: (u32, u32), color_type: ColorType, + original_color_type: ExtendedColorType, // We only use an Option here so we can call with_limits on the decoder without moving. inner: Option>, @@ -42,7 +43,7 @@ where let mut inner = tiff::decoder::Decoder::new(r).map_err(ImageError::from_tiff_decode)?; let dimensions = inner.dimensions().map_err(ImageError::from_tiff_decode)?; - let color_type = inner.colortype().map_err(ImageError::from_tiff_decode)?; + let tiff_color_type = inner.colortype().map_err(ImageError::from_tiff_decode)?; match inner.find_tag_unsigned_vec::(tiff::tags::Tag::SampleFormat) { Ok(Some(sample_formats)) => { for format in sample_formats { @@ -53,7 +54,7 @@ where Err(other) => return Err(ImageError::from_tiff_decode(other)), }; - let color_type = match color_type { + let color_type = match tiff_color_type { tiff::ColorType::Gray(8) => ColorType::L8, tiff::ColorType::Gray(16) => ColorType::L16, tiff::ColorType::GrayA(8) => ColorType::La8, @@ -62,6 +63,7 @@ where tiff::ColorType::RGB(16) => ColorType::Rgb16, tiff::ColorType::RGBA(8) => ColorType::Rgba8, tiff::ColorType::RGBA(16) => ColorType::Rgba16, + tiff::ColorType::CMYK(8) => ColorType::Rgb8, tiff::ColorType::Palette(n) | tiff::ColorType::Gray(n) => { return Err(err_unknown_color_type(n)) @@ -74,12 +76,29 @@ where } }; + let original_color_type = match tiff_color_type { + tiff::ColorType::CMYK(8) => ExtendedColorType::Cmyk8, + _ => color_type.into(), + }; + Ok(TiffDecoder { dimensions, color_type, + original_color_type, inner: Some(inner), }) } + + fn total_buffer_bytes(&self) -> u64 { + let dimensions = self.dimensions(); + let total_pixels = u64::from(dimensions.0) * u64::from(dimensions.1); + let bytes_per_pixel = if self.original_color_type == ExtendedColorType::Cmyk8 { + 16 + } else { + u64::from(self.color_type().bytes_per_pixel()) + }; + total_pixels.saturating_mul(bytes_per_pixel) + } } fn check_sample_format(sample_format: u16) -> Result<(), ImageError> { @@ -176,6 +195,10 @@ impl<'a, R: 'a + Read + Seek> ImageDecoder<'a> for TiffDecoder { self.color_type } + fn original_color_type(&self) -> ExtendedColorType { + self.original_color_type + } + fn icc_profile(&mut self) -> Option> { if let Some(decoder) = &mut self.inner { decoder.get_tag_u8_vec(tiff::tags::Tag::Unknown(34675)).ok() @@ -191,7 +214,7 @@ impl<'a, R: 'a + Read + Seek> ImageDecoder<'a> for TiffDecoder { limits.check_dimensions(width, height)?; let max_alloc = limits.max_alloc.unwrap_or(u64::MAX); - let max_intermediate_alloc = max_alloc.saturating_sub(self.total_bytes()); + let max_intermediate_alloc = max_alloc.saturating_sub(self.total_buffer_bytes()); let mut tiff_limits: tiff::decoder::Limits = Default::default(); tiff_limits.decoding_buffer_size = @@ -228,12 +251,24 @@ impl<'a, R: 'a + Read + Seek> ImageDecoder<'a> for TiffDecoder { fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); - match self + let r = self .inner .unwrap() .read_image() - .map_err(ImageError::from_tiff_decode)? - { + .map_err(ImageError::from_tiff_decode)?; + + match r { + tiff::decoder::DecodingResult::U8(v) + if self.original_color_type == ExtendedColorType::Cmyk8 => + { + let mut in_cur = Cursor::new(v); + let mut out_cur = Cursor::new(buf); + let mut cmyk = [0_u8; 4]; + while in_cur.read_exact(&mut cmyk).is_ok() { + let rgb: Rgb = Cmyk(cmyk).into_color(); + out_cur.write_all(&rgb.0)?; + } + } tiff::decoder::DecodingResult::U8(v) => { buf.copy_from_slice(&v); } diff --git a/src/color.rs b/src/color.rs index 5ec6a4677b..e774ef7687 100644 --- a/src/color.rs +++ b/src/color.rs @@ -142,6 +142,9 @@ pub enum ExtendedColorType { /// Pixel is 32-bit float RGBA Rgba32F, + /// Pixel is 8-bit CMYK + Cmyk8, + /// Pixel is of unknown color type with the specified bits per pixel. This can apply to pixels /// which are associated with an external palette. In that case, the pixel value is an index /// into the palette. @@ -180,7 +183,8 @@ impl ExtendedColorType { | ExtendedColorType::Rgba8 | ExtendedColorType::Rgba16 | ExtendedColorType::Rgba32F - | ExtendedColorType::Bgra8 => 4, + | ExtendedColorType::Bgra8 + | ExtendedColorType::Cmyk8 => 4, } } } @@ -367,6 +371,8 @@ define_colors! { pub struct Rgba([T; 4, 1]) = "RGBA"; /// Grayscale colors + alpha channel pub struct LumaA([T; 2, 1]) = "YA"; + /// CMYK + pub struct Cmyk([T; 4, 1]) = "CMYK"; } /// Convert from one pixel component type to another. For example, convert from `u8` to `f32` pixel values. @@ -682,6 +688,57 @@ where } } +impl FromColor> for Rgb +where + T: FromPrimitive, +{ + fn from_color(&mut self, other: &Cmyk) { + let [r, g, b] = &mut self.0; + + (*r, *g, *b) = cmyk_to_rgb(other.0); + } +} + +fn cmyk_to_rgb([c, m, y, k]: [S; 4]) -> (T, T, T) { + let max = S::max_value().into_f32(); + let c = c.into_f32(); + let m = m.into_f32(); + let y = y.into_f32(); + let kf = 1. - k.into_f32() / max; + ( + T::from((max - c) * kf).unwrap(), + T::from((max - m) * kf).unwrap(), + T::from((max - y) * kf).unwrap(), + ) +} + +impl FromColor> for Rgba +where + T: FromPrimitive, +{ + fn from_color(&mut self, other: &Cmyk) { + todo!() + } +} + +impl FromColor> for Luma +where + T: FromPrimitive, +{ + fn from_color(&mut self, other: &Cmyk) { + todo!() + } +} + +impl FromColor> for LumaA +where + T: FromPrimitive, +{ + fn from_color(&mut self, other: &Cmyk) { + todo!() + } +} + /// Blends a color inter another one pub(crate) trait Blend { /// Blends a color in-place. @@ -797,6 +854,12 @@ impl Blend for Rgb { } } +impl Blend for Cmyk { + fn blend(&mut self, other: &Cmyk) { + *self = *other + } +} + /// Invert a color pub(crate) trait Invert { /// Inverts a color in-place. @@ -847,6 +910,12 @@ impl Invert for Rgb { } } +impl Invert for Cmyk { + fn invert(&mut self) { + todo!() + } +} + #[cfg(test)] mod tests { use super::{Luma, LumaA, Pixel, Rgb, Rgba}; diff --git a/src/traits.rs b/src/traits.rs index 56daaa0ddf..d3f9a80a75 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -34,7 +34,7 @@ impl EncodableLayout for [f32] { /// The type of each channel in a pixel. For example, this can be `u8`, `u16`, `f32`. // TODO rename to `PixelComponent`? Split up into separate traits? Seal? -pub trait Primitive: Copy + NumCast + Num + PartialOrd + Clone + Bounded { +pub trait Primitive: Copy + NumCast + Num + PartialOrd + Clone + Bounded + IntoF32 { /// The maximum value for this type of primitive within the context of color. /// For floats, the maximum is `1.0`, whereas the integer types inherit their usual maximum values. const DEFAULT_MAX_VALUE: Self; @@ -44,12 +44,22 @@ pub trait Primitive: Copy + NumCast + Num + PartialOrd + Clone + Bounded { const DEFAULT_MIN_VALUE: Self; } +pub trait IntoF32 { + fn into_f32(self) -> f32; +} + macro_rules! declare_primitive { ($base:ty: ($from:expr)..$to:expr) => { impl Primitive for $base { const DEFAULT_MAX_VALUE: Self = $to; const DEFAULT_MIN_VALUE: Self = $from; } + + impl IntoF32 for $base { + fn into_f32(self) -> f32 { + self as f32 + } + } }; }