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
This commit is contained in:
aiongg authored and GitHub committed 2026-11-14 07:18:01 -05:00
1 parent 1118899c28
commit ea1c84290d
4 files changed
+78 -3

No files matched your search

+3 -2
View File
@@ -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
@@ -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<InkSet>,
/// TIFF `ExtraSamples` (tag 338), one per extra sample. Empty when the tag is absent or
/// unreadable.
pub extra_samples: Vec<ExtraSampleKind>,
}
/// 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<usize>) -> 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(),
}
}
+1 -1
View File
@@ -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};
@@ -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.