mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
fix(codecs): correct TIFF polarity and BigTIFF orientation (#746)
Use the TIFF decoder's normalized WhiteIsZero samples without a second inversion, and parse BigTIFF IFD0 Orientation so decode applies the recorded rotation. Fixes #691 Fixes #693 Co-authored-by: Copilot <[email protected]> Co-authored-by: Tanish Manku <[email protected]> Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
This commit is contained in:
3 files changed
+142
-38
No files matched your search
@@ -160,8 +160,6 @@ pub(crate) fn decode(bytes: &[u8], limits: &Limits) -> Result<Image, CodecError>
|
||||
};
|
||||
let bytes_per_sample = u64::from(bits.max(8) / 8);
|
||||
limits.check_bytes(w, h, layout.channels() as u64 * bytes_per_sample.max(1))?;
|
||||
let white_is_zero = dec.find_tag_unsigned::<u16>(Tag::PhotometricInterpretation).ok().flatten() == Some(PhotometricInterpretation::WhiteIsZero.to_u16());
|
||||
|
||||
let result = dec.read_image().map_err(map_err)?;
|
||||
let n = w as usize * h as usize * layout.channels();
|
||||
let mut img = match result {
|
||||
@@ -185,19 +183,6 @@ pub(crate) fn decode(bytes: &[u8], limits: &Limits) -> Result<Image, CodecError>
|
||||
if img.sample_count() != n {
|
||||
return Err(err("sample count mismatch"));
|
||||
}
|
||||
if white_is_zero && layout.is_gray() {
|
||||
let v: Vec<f32> = img
|
||||
.to_normalized()
|
||||
.chunks(layout.channels())
|
||||
.flat_map(|p| {
|
||||
let mut p = p.to_vec();
|
||||
p[0] = 1.0 - p[0];
|
||||
p
|
||||
})
|
||||
.collect();
|
||||
img = Image::from_normalized(w, h, layout, img.sample_type(), &v)?;
|
||||
}
|
||||
|
||||
img.icc = dec.find_tag(Tag::IccProfile).ok().flatten().and_then(value_bytes);
|
||||
let mut meta = Metadata {
|
||||
xmp: dec
|
||||
|
||||
@@ -30,6 +30,9 @@ const TYPE_LONG: u16 = 4;
|
||||
const ENTRY: usize = 12;
|
||||
/// The TIFF header is 8 bytes, so no IFD can start before it.
|
||||
const HEADER: usize = 8;
|
||||
/// BigTIFF uses a 16-byte header, 20-byte entries, and 8-byte offsets.
|
||||
const BIG_ENTRY: usize = 20;
|
||||
const BIG_HEADER: usize = 16;
|
||||
|
||||
/// Byte order of a TIFF structure.
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -54,6 +57,14 @@ impl Order {
|
||||
Order::Big => u32::from_be_bytes(s),
|
||||
})
|
||||
}
|
||||
|
||||
fn u64(self, b: &[u8], at: usize) -> Option<u64> {
|
||||
let s: [u8; 8] = b.get(at..at.checked_add(8)?)?.try_into().ok()?;
|
||||
Some(match self {
|
||||
Order::Little => u64::from_le_bytes(s),
|
||||
Order::Big => u64::from_be_bytes(s),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The TIFF payload of an EXIF block (a JPEG-style `Exif\0\0` prefix is skipped).
|
||||
@@ -61,29 +72,37 @@ fn tiff_body(b: &[u8]) -> &[u8] {
|
||||
b.strip_prefix(b"Exif\0\0").unwrap_or(b)
|
||||
}
|
||||
|
||||
/// IFD0 of a TIFF structure: its byte order and the offsets of its entries.
|
||||
/// IFD0 of a TIFF structure: byte order, entry layout, and entry offsets.
|
||||
///
|
||||
/// The whole table must be present, up to and including the 4-byte next-IFD
|
||||
/// pointer, and it must start after the 8-byte header; anything else is
|
||||
/// treated as unparseable.
|
||||
fn ifd0_entries(b: &[u8]) -> Option<(Order, impl Iterator<Item = usize>)> {
|
||||
let order = match b.get(0..4)? {
|
||||
[b'I', b'I', 42, 0] => Order::Little,
|
||||
[b'M', b'M', 0, 42] => Order::Big,
|
||||
/// The full table and next-IFD pointer must be present, and IFD0 must start
|
||||
/// after the corresponding TIFF or BigTIFF header.
|
||||
fn ifd0_entries(b: &[u8]) -> Option<(Order, usize, usize, usize, bool)> {
|
||||
let (order, big_tiff) = match b.get(0..4)? {
|
||||
[b'I', b'I', 42, 0] => (Order::Little, false),
|
||||
[b'M', b'M', 0, 42] => (Order::Big, false),
|
||||
[b'I', b'I', 43, 0] => (Order::Little, true),
|
||||
[b'M', b'M', 0, 43] => (Order::Big, true),
|
||||
_ => return None,
|
||||
};
|
||||
let ifd = usize::try_from(order.u32(b, 4)?).ok()?;
|
||||
if ifd < HEADER {
|
||||
let (ifd, count_bytes, entry_bytes, next_ifd_bytes, header) = if big_tiff {
|
||||
if order.u16(b, 4)? != 8 || order.u16(b, 6)? != 0 {
|
||||
return None;
|
||||
}
|
||||
(usize::try_from(order.u64(b, 8)?).ok()?, 8usize, BIG_ENTRY, 8usize, BIG_HEADER)
|
||||
} else {
|
||||
(usize::try_from(order.u32(b, 4)?).ok()?, 2usize, ENTRY, 4usize, HEADER)
|
||||
};
|
||||
if ifd < header {
|
||||
return None;
|
||||
}
|
||||
let count = usize::from(order.u16(b, ifd)?);
|
||||
let first = ifd.checked_add(2)?;
|
||||
let next_ifd = first.checked_add(count.checked_mul(ENTRY)?)?;
|
||||
if next_ifd.checked_add(4)? > b.len() {
|
||||
let count = if big_tiff { usize::try_from(order.u64(b, ifd)?).ok()? } else { usize::from(order.u16(b, ifd)?) };
|
||||
let first = ifd.checked_add(count_bytes)?;
|
||||
let next_ifd = first.checked_add(count.checked_mul(entry_bytes)?)?;
|
||||
if next_ifd.checked_add(next_ifd_bytes)? > b.len() {
|
||||
return None;
|
||||
}
|
||||
// In bounds: every entry ends at or before `next_ifd`.
|
||||
Some((order, (0..count).map(move |i| first + i * ENTRY)))
|
||||
Some((order, first, count, entry_bytes, big_tiff))
|
||||
}
|
||||
|
||||
/// A well-formed Orientation entry's value: where it sits and its field type.
|
||||
@@ -115,12 +134,14 @@ impl Value {
|
||||
|
||||
/// The value of the entry at `e` if it is a well-formed Orientation entry:
|
||||
/// exactly one SHORT or LONG.
|
||||
fn orientation_value(order: Order, b: &[u8], e: usize) -> Option<Value> {
|
||||
if order.u16(b, e)? != TAG_ORIENTATION || order.u32(b, e.checked_add(4)?)? != 1 {
|
||||
fn orientation_value(order: Order, b: &[u8], e: usize, big_tiff: bool) -> Option<Value> {
|
||||
let count = if big_tiff { order.u64(b, e.checked_add(4)?)? } else { u64::from(order.u32(b, e.checked_add(4)?)?) };
|
||||
if order.u16(b, e)? != TAG_ORIENTATION || count != 1 {
|
||||
return None;
|
||||
}
|
||||
let ty = order.u16(b, e.checked_add(2)?)?;
|
||||
matches!(ty, TYPE_SHORT | TYPE_LONG).then_some(Value { at: e.checked_add(8)?, ty })
|
||||
let value_offset = if big_tiff { 12 } else { 8 };
|
||||
matches!(ty, TYPE_SHORT | TYPE_LONG).then_some(Value { at: e.checked_add(value_offset)?, ty })
|
||||
}
|
||||
|
||||
/// Locates the Orientation value in IFD0.
|
||||
@@ -129,9 +150,9 @@ fn orientation_value(order: Order, b: &[u8], e: usize) -> Option<Value> {
|
||||
/// one is malformed (count ≠ 1, or neither SHORT nor LONG) the tag is ignored,
|
||||
/// even when a later duplicate is well formed.
|
||||
fn find_entry(b: &[u8]) -> Option<(Order, Value)> {
|
||||
let (order, mut entries) = ifd0_entries(b)?;
|
||||
let e = entries.find(|&e| order.u16(b, e) == Some(TAG_ORIENTATION))?;
|
||||
Some((order, orientation_value(order, b, e)?))
|
||||
let (order, first, count, entry_bytes, big_tiff) = ifd0_entries(b)?;
|
||||
let e = (0..count).filter_map(|i| first.checked_add(i.checked_mul(entry_bytes)?)).find(|&e| order.u16(b, e) == Some(TAG_ORIENTATION))?;
|
||||
Some((order, orientation_value(order, b, e, big_tiff)?))
|
||||
}
|
||||
|
||||
/// The orientation (1–8) recorded in a TIFF-structured block: an EXIF payload
|
||||
@@ -153,10 +174,11 @@ pub fn exif_orientation(exif: &[u8]) -> u16 {
|
||||
pub fn upright_exif(exif: &[u8]) -> Cow<'_, [u8]> {
|
||||
let body = tiff_body(exif);
|
||||
let prefix = exif.len() - body.len();
|
||||
let Some((order, entries)) = ifd0_entries(body) else {
|
||||
let Some((order, first, count, entry_bytes, big_tiff)) = ifd0_entries(body) else {
|
||||
return Cow::Borrowed(exif);
|
||||
};
|
||||
let values: Vec<Value> = entries.filter_map(|e| orientation_value(order, body, e)).collect();
|
||||
let values: Vec<Value> =
|
||||
(0..count).filter_map(|i| first.checked_add(i.checked_mul(entry_bytes)?)).filter_map(|e| orientation_value(order, body, e, big_tiff)).collect();
|
||||
if values.iter().all(|v| v.read(order, body) == Some(1)) {
|
||||
return Cow::Borrowed(exif);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use photocraft_codecs::{Image, decode, exif_orientation, upright_exif};
|
||||
|
||||
fn classic_white_is_zero() -> Vec<u8> {
|
||||
let width = 4u16;
|
||||
let height = 1u16;
|
||||
let pixels = [0u8, 64, 192, 255];
|
||||
let entries: [(u16, u16, u32); 9] = [
|
||||
(256, 3, u32::from(width)),
|
||||
(257, 3, u32::from(height)),
|
||||
(258, 3, 8),
|
||||
(259, 3, 1),
|
||||
(262, 3, 0), // WhiteIsZero
|
||||
(273, 4, 122), // StripOffsets: immediately after IFD0.
|
||||
(277, 3, 1),
|
||||
(278, 3, u32::from(height)),
|
||||
(279, 4, pixels.len() as u32),
|
||||
];
|
||||
let mut bytes = b"II*\0\x08\0\0\0".to_vec();
|
||||
bytes.extend_from_slice(&(entries.len() as u16).to_le_bytes());
|
||||
for (tag, field_type, value) in entries {
|
||||
bytes.extend_from_slice(&tag.to_le_bytes());
|
||||
bytes.extend_from_slice(&field_type.to_le_bytes());
|
||||
bytes.extend_from_slice(&1u32.to_le_bytes());
|
||||
if field_type == 3 {
|
||||
bytes.extend_from_slice(&(value as u16).to_le_bytes());
|
||||
bytes.extend_from_slice(&[0; 2]);
|
||||
} else {
|
||||
bytes.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
}
|
||||
bytes.extend_from_slice(&0u32.to_le_bytes());
|
||||
bytes.extend_from_slice(&pixels);
|
||||
bytes
|
||||
}
|
||||
|
||||
fn big_tiff_gray(stored: &[u8], width: u16, height: u16, orientation: u16) -> Vec<u8> {
|
||||
let mut entries = vec![
|
||||
(256u16, 3u16, 1u64, u64::from(width)),
|
||||
(257, 3, 1, u64::from(height)),
|
||||
(258, 3, 1, 8),
|
||||
(259, 3, 1, 1),
|
||||
(262, 3, 1, 1), // BlackIsZero
|
||||
(273, 4, 1, 0), // StripOffsets, patched below.
|
||||
(274, 3, 1, u64::from(orientation)),
|
||||
(277, 3, 1, 1),
|
||||
(278, 3, 1, u64::from(height)),
|
||||
(279, 4, 1, stored.len() as u64),
|
||||
];
|
||||
entries.sort_by_key(|entry| entry.0);
|
||||
let pixel_offset = 16u64 + 8 + entries.len() as u64 * 20 + 8;
|
||||
if let Some(strip_offset) = entries.iter_mut().find(|entry| entry.0 == 273) {
|
||||
strip_offset.3 = pixel_offset;
|
||||
}
|
||||
|
||||
let mut bytes = b"II+\0\x08\0\0\0\x10\0\0\0\0\0\0\0".to_vec();
|
||||
bytes.extend_from_slice(&(entries.len() as u64).to_le_bytes());
|
||||
for (tag, field_type, count, value) in entries {
|
||||
bytes.extend_from_slice(&tag.to_le_bytes());
|
||||
bytes.extend_from_slice(&field_type.to_le_bytes());
|
||||
bytes.extend_from_slice(&count.to_le_bytes());
|
||||
bytes.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(&0u64.to_le_bytes());
|
||||
bytes.extend_from_slice(stored);
|
||||
bytes
|
||||
}
|
||||
|
||||
fn stored_for_orientation_6_or_8(orientation: u16) -> Vec<u8> {
|
||||
match orientation {
|
||||
6 => vec![3, 6, 2, 5, 1, 4],
|
||||
8 => vec![4, 1, 5, 2, 6, 3],
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_is_zero_is_normalized_once_by_the_tiff_decoder() {
|
||||
let image = decode(&classic_white_is_zero()).expect("synthetic WhiteIsZero TIFF should decode");
|
||||
|
||||
assert_eq!(image.dimensions(), (4, 1));
|
||||
assert_eq!(image.data(), &[255, 191, 63, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_tiff_orientation_6_and_8_are_applied_to_decoded_pixels() {
|
||||
let upright = [1u8, 2, 3, 4, 5, 6];
|
||||
|
||||
for orientation in [6, 8] {
|
||||
let bytes = big_tiff_gray(&stored_for_orientation_6_or_8(orientation), 2, 3, orientation);
|
||||
let image: Image = decode(&bytes).expect("synthetic BigTIFF should decode");
|
||||
|
||||
assert_eq!(exif_orientation(&bytes), orientation);
|
||||
assert_eq!(exif_orientation(&upright_exif(&bytes)), 1);
|
||||
assert_eq!(image.dimensions(), (3, 2), "orientation {orientation}");
|
||||
assert_eq!(image.data(), &upright, "orientation {orientation}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user