feat: support palette (ColorMap) image decoding
Read the ColorMap tag (320) for RGBPalette images and return raw palette indices as the decoded data with ColorType::Palette(n). Changes: - Read and validate ColorMap tag during IFD parsing (3 * 2^bps entries) - Return ColorType::Palette from colortype() for RGBPalette images - Accept Palette in expand_chunk validation (standard + sub-byte depths) - Expose color_map() method on Decoder for palette lookup - Add tests for 1-bit, 4-bit, 8-bit palette images with checksums - Add test verifying ColorMap accessibility and structure The decoder returns raw palette indices; consumers apply the lookup using the color map returned by Decoder::color_map().
This commit is contained in:
1 parent
c6f6ff4c95
commit
3b11e2f577
3 files changed
+90
-8
No files matched your search
+39
-8
@@ -85,6 +85,9 @@ pub(crate) struct Image {
|
|||||||
pub chunk_offsets: Vec<u64>,
|
pub chunk_offsets: Vec<u64>,
|
||||||
pub chunk_bytes: Vec<u64>,
|
pub chunk_bytes: Vec<u64>,
|
||||||
pub chroma_subsampling: (u16, u16),
|
pub chroma_subsampling: (u16, u16),
|
||||||
|
/// The color map (palette) for `PhotometricInterpretation::RGBPalette`.
|
||||||
|
/// Contains `3 * 2^BitsPerSample` u16 entries: R values, then G values, then B values.
|
||||||
|
pub color_map: Option<Vec<u16>>,
|
||||||
/// Will decompression yield host-endian samples?
|
/// Will decompression yield host-endian samples?
|
||||||
///
|
///
|
||||||
/// Standard compression algorithms run on pure bytes, taking the sample slice as it would be
|
/// Standard compression algorithms run on pure bytes, taking the sample slice as it would be
|
||||||
@@ -166,6 +169,7 @@ impl Image {
|
|||||||
chunk_offsets: Vec::new(),
|
chunk_offsets: Vec::new(),
|
||||||
chunk_bytes: Vec::new(),
|
chunk_bytes: Vec::new(),
|
||||||
chroma_subsampling: (2, 2),
|
chroma_subsampling: (2, 2),
|
||||||
|
color_map: None,
|
||||||
decompression_to_host_endian: false,
|
decompression_to_host_endian: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,6 +462,31 @@ impl Image {
|
|||||||
bits_per_sample = 32;
|
bits_per_sample = 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read ColorMap for palette images. The tag contains 3 * 2^BitsPerSample u16 entries.
|
||||||
|
let color_map = if photometric_interpretation == PhotometricInterpretation::RGBPalette {
|
||||||
|
match tag_reader.find_tag(Tag::ColorMap)? {
|
||||||
|
Some(v) => {
|
||||||
|
let map = v.into_u16_vec()?;
|
||||||
|
let expected_len = 3usize
|
||||||
|
.checked_mul(1usize << bits_per_sample)
|
||||||
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
|
if map.len() != expected_len {
|
||||||
|
return Err(TiffError::FormatError(
|
||||||
|
TiffFormatError::InconsistentSizesEncountered,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(map)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(TiffError::FormatError(
|
||||||
|
TiffFormatError::RequiredTagNotFound(Tag::ColorMap),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Image {
|
Ok(Image {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -477,6 +506,7 @@ impl Image {
|
|||||||
chunk_offsets,
|
chunk_offsets,
|
||||||
chunk_bytes,
|
chunk_bytes,
|
||||||
chroma_subsampling,
|
chroma_subsampling,
|
||||||
|
color_map,
|
||||||
decompression_to_host_endian: matches!(
|
decompression_to_host_endian: matches!(
|
||||||
compression_method,
|
compression_method,
|
||||||
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
|
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
|
||||||
@@ -629,14 +659,13 @@ impl Image {
|
|||||||
vec![self.bits_per_sample; self.samples as usize],
|
vec![self.bits_per_sample; self.samples as usize],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::TransparencyMask => {
|
PhotometricInterpretation::RGBPalette => Ok(ColorType::Palette(self.bits_per_sample)),
|
||||||
Err(TiffError::UnsupportedError(
|
PhotometricInterpretation::TransparencyMask => Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::InterpretationWithBits(
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
self.photometric_interpretation,
|
self.photometric_interpretation,
|
||||||
vec![self.bits_per_sample; self.samples as usize],
|
vec![self.bits_per_sample; self.samples as usize],
|
||||||
),
|
),
|
||||||
))
|
)),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -979,11 +1008,13 @@ impl Image {
|
|||||||
| ColorType::CMYKA(n)
|
| ColorType::CMYKA(n)
|
||||||
| ColorType::YCbCr(n)
|
| ColorType::YCbCr(n)
|
||||||
| ColorType::Gray(n)
|
| ColorType::Gray(n)
|
||||||
|
| ColorType::Palette(n)
|
||||||
| ColorType::Multiband {
|
| ColorType::Multiband {
|
||||||
bit_depth: n,
|
bit_depth: n,
|
||||||
num_samples: _,
|
num_samples: _,
|
||||||
} if n == 8 || n == 16 || n == 32 || n == 64 => {}
|
} if n == 8 || n == 16 || n == 32 || n == 64 => {}
|
||||||
ColorType::Gray(n)
|
ColorType::Gray(n)
|
||||||
|
| ColorType::Palette(n)
|
||||||
| ColorType::Multiband {
|
| ColorType::Multiband {
|
||||||
bit_depth: n,
|
bit_depth: n,
|
||||||
num_samples: _,
|
num_samples: _,
|
||||||
|
|||||||
@@ -941,6 +941,16 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
self.image.colortype()
|
self.image.colortype()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the color map (palette) for the current image, if it uses
|
||||||
|
/// `PhotometricInterpretation::RGBPalette`.
|
||||||
|
///
|
||||||
|
/// The returned slice contains `3 * 2^BitsPerSample` entries as u16 values,
|
||||||
|
/// laid out as: all red values, then all green values, then all blue values.
|
||||||
|
/// Each value is in the range `0..=65535` regardless of the actual bit depth.
|
||||||
|
pub fn color_map(&self) -> Option<&[u16]> {
|
||||||
|
self.image.color_map.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
/// The offset of the directory representing the current image.
|
/// The offset of the directory representing the current image.
|
||||||
pub fn ifd_pointer(&mut self) -> Option<IfdPointer> {
|
pub fn ifd_pointer(&mut self) -> Option<IfdPointer> {
|
||||||
self.current_ifd_pointer
|
self.current_ifd_pointer
|
||||||
|
|||||||
@@ -73,6 +73,47 @@ fn test_cmyk_f32() {
|
|||||||
test_image_sum_f32("cmyk-3c-32b-float.tiff", ColorType::CMYK(32), 496.0405);
|
test_image_sum_f32("cmyk-3c-32b-float.tiff", ColorType::CMYK(32), 496.0405);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_palette_u1() {
|
||||||
|
test_image_sum_u8("palette-1c-1b.tiff", ColorType::Palette(1), 379313);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_palette_u4() {
|
||||||
|
test_image_sum_u8("palette-1c-4b.tiff", ColorType::Palette(4), 1480490);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_palette_u8() {
|
||||||
|
test_image_sum_u8("palette-1c-8b.tiff", ColorType::Palette(8), 2876948);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_palette_color_map() {
|
||||||
|
let path = PathBuf::from(TEST_IMAGE_DIR).join("palette-1c-8b.tiff");
|
||||||
|
let img_file = File::open(path).expect("Cannot find test image!");
|
||||||
|
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||||
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
|
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Palette(8));
|
||||||
|
|
||||||
|
let color_map = decoder
|
||||||
|
.color_map()
|
||||||
|
.expect("Palette image should have a color map");
|
||||||
|
// 8-bit palette: 3 * 256 = 768 entries
|
||||||
|
assert_eq!(color_map.len(), 768);
|
||||||
|
|
||||||
|
// Each section (R, G, B) has 256 entries
|
||||||
|
let r = &color_map[..256];
|
||||||
|
let g = &color_map[256..512];
|
||||||
|
let b = &color_map[512..768];
|
||||||
|
|
||||||
|
// Verify all values are valid u16 (non-zero palette, at least some variation)
|
||||||
|
assert!(r.iter().any(|&v| v > 0));
|
||||||
|
assert!(g.iter().any(|&v| v > 0));
|
||||||
|
assert!(b.iter().any(|&v| v > 0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gray_u8() {
|
fn test_gray_u8() {
|
||||||
test_image_sum_u8("minisblack-1c-8b.tiff", ColorType::Gray(8), 2840893);
|
test_image_sum_u8("minisblack-1c-8b.tiff", ColorType::Gray(8), 2840893);
|
||||||
|
|||||||
Reference in new issue
Block a user