Merge remote-tracking branch 'origin/main' into palette-cielab-transparency-lzw

This commit is contained in:
aarshivv committed 2026-07-23 00:52:37 +05:30
commit d0c9198016
12 files changed
+320 -24

No files matched your search

+5 -3
View File
@@ -16,7 +16,9 @@ macro_rules! test_image_sum {
fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
let mut decoder = Decoder::open(img_file)
.expect("Cannot create decoder")
.with_lenient(true);
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), expected_type);
@@ -825,7 +827,7 @@ fn test_predictor_3_gray_f32() {
}
#[test]
#[cfg(feature = "zstd")]
#[cfg(any(feature = "zstd", feature = "zstd-safe-rust"))]
fn test_zstd_compression() {
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
@@ -975,7 +977,7 @@ fn extra_bits_gray() {
#[test]
fn extra_bits_rgb() {
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGBA(8), 64);
}
#[test]
+164
View File
@@ -233,3 +233,167 @@ fn test_rgb_u64_predict_roundtrip() {
fn test_ycbcr_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
}
// ---------------------------------------------------------------------------
// Extended bit depths (9-15 promoted to u16, 17-31 promoted to u32).
//
// A horizontal-predicted n-bit sample stores its delta in exactly n bits,
// i.e. modulo 2^n, so reconstruction must also be modulo 2^n. The decoder
// accumulates in the promoted storage width and reduces back to n bits;
// these tests pin that reduction down with exact pixel values, including
// deltas that wrap modulo 2^n. The encoder does not support extended
// depths, so the TIFFs are hand-built.
// ---------------------------------------------------------------------------
/// Hand-build a minimal little-endian, single-strip, uncompressed grayscale
/// TIFF with `Predictor::Horizontal` and the given (possibly non-standard)
/// bit depth. `pixels` are the intended sample values; they are converted to
/// horizontal differences modulo `2^bits` and bit-packed MSB-first with each
/// row padded to a byte boundary — the only representation an n-bit field
/// can hold.
fn build_gray_hpredict_tiff(
width: u32,
height: u32,
bits: u8,
photometric: u16,
pixels: &[u32],
) -> Vec<u8> {
let max = ((1u64 << bits) - 1) as u32;
let row_samples = width as usize;
assert_eq!(pixels.len(), row_samples * height as usize);
assert!(pixels.iter().all(|&v| v <= max));
// Per-row horizontal differencing modulo 2^bits.
let mut deltas = Vec::with_capacity(pixels.len());
for row in pixels.chunks(row_samples) {
for (i, &v) in row.iter().enumerate() {
deltas.push(if i == 0 {
v
} else {
v.wrapping_sub(row[i - 1]) & max
});
}
}
// Bit-pack MSB-first, each row padded to a byte boundary.
let row_bytes = (row_samples * bits as usize).div_ceil(8);
let mut data = vec![0u8; row_bytes * height as usize];
for (r, row) in deltas.chunks(row_samples).enumerate() {
for (i, &d) in row.iter().enumerate() {
let start = r * row_bytes * 8 + i * bits as usize;
for (off, b) in (start..).zip((0..bits).rev()) {
if (d >> b) & 1 == 1 {
data[off / 8] |= 0x80 >> (off % 8);
}
}
}
}
// Classic little-endian TIFF: header, strip data, then a single IFD.
let mut out = Vec::new();
out.extend_from_slice(b"II");
out.extend_from_slice(&42u16.to_le_bytes());
let data_offset = 8u32;
let ifd_offset = data_offset + data.len() as u32;
out.extend_from_slice(&ifd_offset.to_le_bytes());
out.extend_from_slice(&data);
const SHORT: u16 = 3;
const LONG: u16 = 4;
let entries: &[(u16, u16, u32)] = &[
(256, LONG, width), // ImageWidth
(257, LONG, height), // ImageLength
(258, SHORT, u32::from(bits)), // BitsPerSample
(259, SHORT, 1), // Compression: none
(262, SHORT, u32::from(photometric)), // PhotometricInterpretation
(273, LONG, data_offset), // StripOffsets
(277, SHORT, 1), // SamplesPerPixel
(278, LONG, height), // RowsPerStrip
(279, LONG, data.len() as u32), // StripByteCounts
(317, SHORT, 2), // Predictor: horizontal
(339, SHORT, 1), // SampleFormat: unsigned integer
];
out.extend_from_slice(&(entries.len() as u16).to_le_bytes());
for &(tag, ty, value) in entries {
out.extend_from_slice(&tag.to_le_bytes());
out.extend_from_slice(&ty.to_le_bytes());
out.extend_from_slice(&1u32.to_le_bytes());
// A count-1 SHORT payload occupies the low bytes of the value field.
out.extend_from_slice(&value.to_le_bytes());
}
out.extend_from_slice(&0u32.to_le_bytes()); // no next IFD
out
}
fn decode_gray_u16(tiff: Vec<u8>, bits: u8) -> Vec<u16> {
let mut decoder = Decoder::open(Cursor::new(tiff)).expect("Cannot create decoder");
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(bits));
match decoder.read_image().unwrap() {
DecodingSampleBuffer::U16(res) => res,
_ => panic!("Wrong data type"),
}
}
fn decode_gray_u32(tiff: Vec<u8>, bits: u8) -> Vec<u32> {
let mut decoder = Decoder::open(Cursor::new(tiff)).expect("Cannot create decoder");
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(bits));
match decoder.read_image().unwrap() {
DecodingSampleBuffer::U32(res) => res,
_ => panic!("Wrong data type"),
}
}
#[test]
fn test_gray12_hpredict_wraparound_exact() {
// Pixel row [4095, 0, 1] encodes as deltas [4095, 1, 1] (mod 2^12).
// u16 accumulation yields [4095, 4096, 4097]; reduction modulo 2^12
// must restore [4095, 0, 1]. Saturating instead would corrupt the two
// wrapped samples to 4095.
let tiff = build_gray_hpredict_tiff(3, 1, 12, 1, &[4095, 0, 1]);
assert_eq!(decode_gray_u16(tiff, 12), vec![4095, 0, 1]);
}
#[test]
fn test_gray12_hpredict_wraparound_white_is_zero_exact() {
// Same stored samples with PhotometricInterpretation = WhiteIsZero:
// after the mod-2^12 reduction the decoder inverts as (2^12 - 1) - v.
let tiff = build_gray_hpredict_tiff(3, 1, 12, 0, &[4095, 0, 1]);
assert_eq!(decode_gray_u16(tiff, 12), vec![0, 4095, 4094]);
}
#[test]
fn test_gray20_hpredict_wraparound_exact() {
// The same wrap-around shape on the u32 storage path (17-31 bits).
let tiff = build_gray_hpredict_tiff(3, 1, 20, 1, &[0xF_FFFF, 0, 1]);
assert_eq!(decode_gray_u32(tiff, 20), vec![0xF_FFFF, 0, 1]);
}
#[test]
fn test_extended_depth_hpredict_random_roundtrip() {
// Deterministic pseudo-random pixels across a spread of extended depths;
// encode-then-decode must be the identity for every sample, including
// every mod-2^n wrap-around the differencing produces. Three rows verify
// that prediction restarts per row.
let mut state = 0x243F_6A88_85A3_08D3u64;
let mut next = move || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 33) as u32
};
for &bits in &[9u8, 11, 12, 15] {
let max = (1u32 << bits) - 1;
let pixels: Vec<u32> = (0..16 * 3).map(|_| next() & max).collect();
let tiff = build_gray_hpredict_tiff(16, 3, bits, 1, &pixels);
let expected: Vec<u16> = pixels.iter().map(|&v| v as u16).collect();
assert_eq!(decode_gray_u16(tiff, bits), expected, "bits={bits}");
}
for &bits in &[17u8, 20, 25, 31] {
let max = ((1u64 << bits) - 1) as u32;
let pixels: Vec<u32> = (0..16 * 3).map(|_| next() & max).collect();
let tiff = build_gray_hpredict_tiff(16, 3, bits, 1, &pixels);
assert_eq!(decode_gray_u32(tiff, bits), pixels, "bits={bits}");
}
}