From ea1c84290dadec636900b4d1bce107e59ca3f0d6 Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Sat, 14 Nov 2026 07:18:01 -0500 Subject: [PATCH] image-codecs: list InkSet and ExtraSamples on TiffPage (#1307) * image-codecs: list InkSet and ExtraSamples on TiffPage * image-codecs: TiffPage InkSet and ExtraSamples as enums * image-codecs: InkSet::InkNames, and Invalid for undefined tag values --- crates/image-codecs/README.md | 5 +- crates/image-codecs/src/codecs/tiff_ifd.rs | 59 ++++++++++++++++++++++ crates/image-codecs/src/lib.rs | 2 +- crates/image-codecs/tests/it/tiff_pages.rs | 15 ++++++ 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/crates/image-codecs/README.md b/crates/image-codecs/README.md index c2ca866b3..955542c91 100644 --- a/crates/image-codecs/README.md +++ b/crates/image-codecs/README.md @@ -250,8 +250,9 @@ embedded table (and a PNG's transparent entry) so `photocraft-io` can open them the image: when the image fits but its extra samples don't, it opens without them and carries `DecodeWarning::ExtraSamplesDropped`. * Every directory is reachable: `tiff_info` lists the main IFD chain and its SubIFDs (tag 330) - with each page's size, kind (page, reduced-resolution copy, mask) and storage, walked with a - seen-set, a depth limit and a 10 000-directory cap; `decode_tiff_page` opens any of them. + with each page's size, kind (page, reduced-resolution copy, mask), storage, InkSet and + ExtraSamples, walked with a seen-set, a depth limit and a 10 000-directory cap; + `decode_tiff_page` opens any of them. `decode` opens the first full-resolution page, like Photoshop (thumbnails are skipped). * Banded decoding: the image is allocated once at its final size and each strip or row of tiles is decoded straight into it, in parallel on native targets, with at most one diff --git a/crates/image-codecs/src/codecs/tiff_ifd.rs b/crates/image-codecs/src/codecs/tiff_ifd.rs index 8fac8c6b4..0783cbdf3 100644 --- a/crates/image-codecs/src/codecs/tiff_ifd.rs +++ b/crates/image-codecs/src/codecs/tiff_ifd.rs @@ -41,6 +41,7 @@ pub(crate) const TAG_TILE_WIDTH: u16 = 322; pub(crate) const TAG_TILE_LENGTH: u16 = 323; pub(crate) const TAG_TILE_OFFSETS: u16 = 324; pub(crate) const TAG_SUB_IFDS: u16 = 330; +pub(crate) const TAG_INK_SET: u16 = 332; pub(crate) const TAG_EXTRA_SAMPLES: u16 = 338; pub(crate) const TAG_SAMPLE_FORMAT: u16 = 339; @@ -349,6 +350,56 @@ pub struct TiffPage { pub tiled: bool, /// `PlanarConfiguration` 2: each sample in its own plane. pub planar: bool, + /// TIFF `InkSet` (tag 332), when present. Only meaningful for photometric 5 (Separated). + pub ink_set: Option, + /// TIFF `ExtraSamples` (tag 338), one per extra sample. Empty when the tag is absent or + /// unreadable. + pub extra_samples: Vec, +} + +/// The TIFF `InkSet` tag (332) of a separated image. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InkSet { + /// 1: the samples are cyan, magenta, yellow and black. + Cmyk, + /// 2: the samples are the inks listed in the `InkNames` tag. + InkNames, + /// Any other value; TIFF 6.0 defines only 1 and 2. + Invalid(u16), +} + +impl InkSet { + fn from_tag(value: u16) -> Self { + match value { + 1 => Self::Cmyk, + 2 => Self::InkNames, + v => Self::Invalid(v), + } + } +} + +/// What one extra sample of a TIFF holds, from the `ExtraSamples` tag (338). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExtraSampleKind { + /// 0: unspecified data. + Unspecified, + /// 1: associated (premultiplied) alpha. + AssociatedAlpha, + /// 2: unassociated (straight) alpha. + UnassociatedAlpha, + /// Any other value; TIFF 6.0 defines only 0, 1 and 2. + Invalid(u16), +} + +impl ExtraSampleKind { + fn from_tag(value: u16) -> Self { + match value { + 0 => Self::Unspecified, + 1 => Self::AssociatedAlpha, + 2 => Self::UnassociatedAlpha, + v => Self::Invalid(v), + } + } } /// The directory structure of a TIFF file: its byte order, BigTIFF or not, and every image @@ -405,6 +456,14 @@ fn describe(f: &File<'_>, ifd: &Ifd, parent: Option) -> TiffPage { compression: u16_of(TAG_COMPRESSION, 1), tiled: ifd.get(TAG_TILE_OFFSETS).is_some(), planar: f.tag_uint(ifd, TAG_PLANAR) == Some(2), + ink_set: f.tag_uint(ifd, TAG_INK_SET).map(|v| InkSet::from_tag(u16::try_from(v).unwrap_or(u16::MAX))), + // Capped at the sample count: a file cannot have more extra samples than samples. + extra_samples: f + .tag_uints(ifd, TAG_EXTRA_SAMPLES, u64::from(u16_of(TAG_SAMPLES, 1))) + .unwrap_or_default() + .into_iter() + .map(|v| ExtraSampleKind::from_tag(u16::try_from(v).unwrap_or(u16::MAX))) + .collect(), } } diff --git a/crates/image-codecs/src/lib.rs b/crates/image-codecs/src/lib.rs index d3ab272fa..39a035695 100644 --- a/crates/image-codecs/src/lib.rs +++ b/crates/image-codecs/src/lib.rs @@ -42,7 +42,7 @@ pub use crate::codecs::png::{ pub use crate::codecs::tiff::{ PhotoshopTags as TiffPhotoshopTags, TiffBands, photoshop_tags as tiff_photoshop_tags, writes_little_endian as tiff_writes_little_endian, }; -pub use crate::codecs::tiff_ifd::{TiffInfo, TiffPage, TiffPageKind}; +pub use crate::codecs::tiff_ifd::{ExtraSampleKind, InkSet, TiffInfo, TiffPage, TiffPageKind}; pub use crate::error::CodecError; pub use crate::exif_color::{EXIF_COLOR_SPACE_SRGB, exif_color_space}; pub use crate::fidelity::{FidelityWarning, fidelity_warnings, fidelity_warnings_with}; diff --git a/crates/image-codecs/tests/it/tiff_pages.rs b/crates/image-codecs/tests/it/tiff_pages.rs index 0e589786f..b94509598 100644 --- a/crates/image-codecs/tests/it/tiff_pages.rs +++ b/crates/image-codecs/tests/it/tiff_pages.rs @@ -382,6 +382,21 @@ fn associated_alpha_is_made_straight() { assert_eq!(img.to_u16_samples().unwrap(), [32768, 32768]); } +#[test] +fn ink_set_and_extra_samples_are_listed() { + let px = [10u8, 20, 30, 40, 17, 128]; + let d = strips(1, 1, 8, 6, 5, 1, &px).tag(332, SHORT, &[2]).tag(338, SHORT, &[0, 1]); + let info = tiff_info(&build(true, false, &[d], &[0]).bytes).unwrap(); + use ExtraSampleKind::*; + assert_eq!((info.pages[0].ink_set, &info.pages[0].extra_samples[..]), (Some(InkSet::InkNames), &[Unspecified, AssociatedAlpha][..])); + let d = strips(1, 1, 8, 6, 5, 1, &px).tag(332, SHORT, &[7]).tag(338, SHORT, &[2, 5]); + let info = tiff_info(&build(true, false, &[d], &[0]).bytes).unwrap(); + assert_eq!((info.pages[0].ink_set, &info.pages[0].extra_samples[..]), (Some(InkSet::Invalid(7)), &[UnassociatedAlpha, Invalid(5)][..])); + let d = strips(1, 1, 8, 4, 5, 1, &px[..4]); + let info = tiff_info(&build(true, false, &[d], &[0]).bytes).unwrap(); + assert_eq!((info.pages[0].ink_set, &info.pages[0].extra_samples[..]), (None, &[][..])); +} + #[test] fn white_is_zero_opens_with_the_right_tones() { // #691: 00 FF 00 in a WhiteIsZero file is white, black, white.