Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tiff: Support CMYK images #2075

Merged
merged 1 commit into from
Dec 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions src/codecs/tiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<tiff::decoder::Decoder<R>>,
Expand All @@ -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::<u16>(tiff::tags::Tag::SampleFormat) {
Ok(Some(sample_formats)) => {
for format in sample_formats {
Expand All @@ -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,
Expand All @@ -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))
Expand All @@ -74,12 +76,30 @@ 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),
})
}

// The buffer can be larger for CMYK than the RGB output
fn total_bytes_buffer(&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 {
sophie-h marked this conversation as resolved.
Show resolved Hide resolved
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> {
Expand Down Expand Up @@ -176,6 +196,10 @@ impl<'a, R: 'a + Read + Seek> ImageDecoder<'a> for TiffDecoder<R> {
self.color_type
}

fn original_color_type(&self) -> ExtendedColorType {
self.original_color_type
}

fn icc_profile(&mut self) -> Option<Vec<u8>> {
if let Some(decoder) = &mut self.inner {
decoder.get_tag_u8_vec(tiff::tags::Tag::Unknown(34675)).ok()
Expand All @@ -191,7 +215,7 @@ impl<'a, R: 'a + Read + Seek> ImageDecoder<'a> for TiffDecoder<R> {
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_bytes_buffer());

let mut tiff_limits: tiff::decoder::Limits = Default::default();
tiff_limits.decoding_buffer_size =
Expand Down Expand Up @@ -234,6 +258,14 @@ impl<'a, R: 'a + Read + Seek> ImageDecoder<'a> for TiffDecoder<R> {
.read_image()
.map_err(ImageError::from_tiff_decode)?
{
tiff::decoder::DecodingResult::U8(v)
if self.original_color_type == ExtendedColorType::Cmyk8 =>
{
let mut out_cur = Cursor::new(buf);
for cmyk in v.chunks_exact(4) {
out_cur.write_all(&cmyk_to_rgb(cmyk))?;
}
}
tiff::decoder::DecodingResult::U8(v) => {
buf.copy_from_slice(&v);
}
Expand Down Expand Up @@ -274,6 +306,18 @@ pub struct TiffEncoder<W> {
w: W,
}

fn cmyk_to_rgb(cmyk: &[u8]) -> [u8; 3] {
let c = cmyk[0] as f32;
let m = cmyk[1] as f32;
let y = cmyk[2] as f32;
let kf = 1. - cmyk[3] as f32 / 255.;
[
((255. - c) * kf) as u8,
((255. - m) * kf) as u8,
((255. - y) * kf) as u8,
]
}

// Utility to simplify and deduplicate error handling during 16-bit encoding.
fn u8_slice_as_u16(buf: &[u8]) -> ImageResult<&[u16]> {
bytemuck::try_cast_slice(buf).map_err(|err| {
Expand Down
6 changes: 5 additions & 1 deletion src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -180,7 +183,8 @@ impl ExtendedColorType {
| ExtendedColorType::Rgba8
| ExtendedColorType::Rgba16
| ExtendedColorType::Rgba32F
| ExtendedColorType::Bgra8 => 4,
| ExtendedColorType::Bgra8
| ExtendedColorType::Cmyk8 => 4,
}
}
}
Expand Down
Binary file added tests/images/tiff/testsuite/hpredict_cmyk.tiff
Binary file not shown.
Loading