Preserve CFA anchor in shared raw metadata

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 00:46:33 -05:00
1 parent 3bdf0eaadd
commit 8bac629c0f
10 files changed
+44 -15

No files matched your search

+3 -1
View File
@@ -721,7 +721,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawIma
let scale_bits = if info.compression == 1 && out_bits < 14 && strip_len >= (w * h * 2) as u64 { 14 } else { out_bits };
let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) {
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => {
Cfa { width: 2, height: 2, pattern: p.to_vec(), origin_x: 0, origin_y: 0 }
}
_ => Cfa::bayer_static("RGGB"),
};
let default_black = if scale_bits >= 14 { 512.0 } else { 128.0 };
+6 -2
View File
@@ -410,9 +410,13 @@ fn decode_ifd(bytes: &[u8], tiff: &Tiff, pixels: &Ifd, main: &Ifd, mode: Mode, f
// sensor origin (the crop to the active area shifts it back), so move the anchor out by the area's offset
// (DNGs that LightCraft wrote before its writer followed the spec carry the sensor-origin pattern; they are marked
// by a bare "LightCraft" Software tag and keep their original reading)
let at_origin = Cfa { width: cols, height: rows, pattern };
let mut at_origin = Cfa { width: cols, height: rows, pattern, origin_x: 0, origin_y: 0 };
let legacy_writer = [raw, ifd0].iter().any(|i| i.string(t::SOFTWARE).as_deref().map(str::trim) == Some("LightCraft"));
Some(if legacy_writer { at_origin } else { at_origin.shifted(cols - active_area.x % cols, rows - active_area.y % rows) })
if !legacy_writer {
at_origin.origin_x = active_area.x;
at_origin.origin_y = active_area.y;
}
Some(at_origin)
} else {
None
};
+3 -1
View File
@@ -78,7 +78,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawIma
let (w, h) = (info.width as usize, info.height as usize);
let n = w.checked_mul(h).filter(|n| *n > 0 && *n <= limits.samples()).ok_or(RawError::Limit("image too large"))?;
let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) {
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => {
Cfa { width: 2, height: 2, pattern: p.to_vec(), origin_x: 0, origin_y: 0 }
}
_ => return Err(RawError::Unsupported("Erf raw without a 2x2 CFAPattern".into())),
};
let mn = maker_note(bytes, &tiff);
+3 -1
View File
@@ -68,7 +68,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawIma
let (w, h) = (info.width as usize, info.height as usize);
let n = w.checked_mul(h).filter(|n| *n > 0 && *n <= limits.samples()).ok_or(RawError::Limit("image too large"))?;
let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) {
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => {
Cfa { width: 2, height: 2, pattern: p.to_vec(), origin_x: 0, origin_y: 0 }
}
_ => return Err(RawError::Unsupported("Kodak raw without a 2x2 CFAPattern".into())),
};
let curve: Vec<u16> = raw
+14 -5
View File
@@ -785,6 +785,11 @@ pub struct Cfa {
pub height: usize,
/// `width × height` colour indices, row-major, anchored at image pixel (0, 0).
pub pattern: Vec<u8>,
/// The sensor-space coordinate where this periodic pattern is anchored.
#[serde(default)]
pub origin_x: usize,
#[serde(default)]
pub origin_y: usize,
}
impl Cfa {
@@ -801,13 +806,13 @@ impl Cfa {
})
})
.collect::<Option<_>>()?;
(p.len() == 4).then_some(Cfa { width: 2, height: 2, pattern: p })
(p.len() == 4).then_some(Cfa { width: 2, height: 2, pattern: p, origin_x: 0, origin_y: 0 })
}
/// A 2×2 Bayer layout named in code (`"RGGB"`, `"BGGR"`, `"GRBG"`, `"GBRG"`). Only for literal
/// names (tested below); anything else falls back to RGGB instead of failing.
#[cfg_attr(not(feature = "crw"), allow(dead_code))]
pub(crate) fn bayer_static(s: &'static str) -> Cfa {
Cfa::bayer(s).unwrap_or(Cfa { width: 2, height: 2, pattern: vec![0, 1, 1, 2] })
Cfa::bayer(s).unwrap_or(Cfa { width: 2, height: 2, pattern: vec![0, 1, 1, 2], origin_x: 0, origin_y: 0 })
}
/// The Fujifilm X-Trans 6×6 layout (as commonly documented), anchored at (0, 0).
pub fn xtrans() -> Cfa {
@@ -822,16 +827,20 @@ impl Cfa {
})
})
.collect();
Cfa { width: 6, height: 6, pattern }
Cfa { width: 6, height: 6, pattern, origin_x: 0, origin_y: 0 }
}
#[inline]
pub fn color_at(&self, x: usize, y: usize) -> u8 {
self.pattern[(y % self.height) * self.width + (x % self.width)]
let relative = |v: usize, origin: usize, period: usize| {
let (v, origin) = (v % period, origin % period);
if v >= origin { v - origin } else { period - (origin - v) }
};
self.pattern[relative(y, self.origin_y, self.height) * self.width + relative(x, self.origin_x, self.width)]
}
/// The same pattern anchored at `(dx, dy)` of the current anchor (e.g. after cropping to an active area).
pub fn shifted(&self, dx: usize, dy: usize) -> Cfa {
let pattern = (0..self.height).flat_map(|y| (0..self.width).map(move |x| (x, y))).map(|(x, y)| self.color_at(x + dx, y + dy)).collect();
Cfa { width: self.width, height: self.height, pattern }
Cfa { width: self.width, height: self.height, pattern, origin_x: 0, origin_y: 0 }
}
pub fn is_bayer(&self) -> bool {
if self.width != 2 || self.height != 2 {
+3 -1
View File
@@ -70,7 +70,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawIma
let (w, h) = (info.width as usize, info.height as usize);
let n = w.checked_mul(h).filter(|n| *n > 0 && *n <= limits.samples()).ok_or(RawError::Limit("image too large"))?;
let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) {
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => {
Cfa { width: 2, height: 2, pattern: p.to_vec(), origin_x: 0, origin_y: 0 }
}
_ => return Err(RawError::Unsupported("Mef raw without a 2x2 CFAPattern".into())),
};
let preview = rgb_preview(&tiff, bytes);
+3 -1
View File
@@ -281,7 +281,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawIma
.filter(|v| v.len() >= 2 && v[0] > 0.1 && v[1] > 0.1 && v[0] < 10.0 && v[1] < 10.0)
.map(|v| [v[0] as f32, 1.0, v[1] as f32]);
let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) {
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => {
Cfa { width: 2, height: 2, pattern: p.to_vec(), origin_x: 0, origin_y: 0 }
}
_ => Cfa::bayer_static("RGGB"),
};
let model = ifd0.string(t::MODEL).unwrap_or_default();
+3 -1
View File
@@ -195,7 +195,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawIma
None => dark_trimmed(samples, w, hgt, black_point.as_deref().map(|v| v.iter().sum::<f64>() / 4.0), white),
};
let cfa = match (ifd0.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), ifd0.bytes(t::CFA_PATTERN_EP)) {
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => {
Cfa { width: 2, height: 2, pattern: p.to_vec(), origin_x: 0, origin_y: 0 }
}
// Every body states its layout in the Exif `CFAPattern`, read from the first sample of the stored array, not
// of the image area: the *ist D (origin 19, 11) and the 645D (80, 59) only fit unshifted. The 12-bit 10 MP
// bodies and everything from the K-3 on say RGGB; the K-7 / K-5 / 645D generation says BGGR. Files without the
+3 -1
View File
@@ -125,7 +125,9 @@ fn camera_name(b: &[u8]) -> String {
/// The CFA: X-Trans from the (reversed) layout record, else RGGB.
fn cfa(h: &Header) -> Cfa {
match record(h, XTRANS_LAYOUT) {
Some(l) if l.len() == 36 && l.iter().all(|&c| c <= 2) => Cfa { width: 6, height: 6, pattern: l.iter().rev().copied().collect() },
Some(l) if l.len() == 36 && l.iter().all(|&c| c <= 2) => {
Cfa { width: 6, height: 6, pattern: l.iter().rev().copied().collect(), origin_x: 0, origin_y: 0 }
}
_ => Cfa::bayer_static("RGGB"),
}
}
+3 -1
View File
@@ -134,7 +134,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawIma
.filter(|&(l, tp, r, b)| l < r && tp < b && r <= w && b <= h)
.map_or(Rect::new(0, 0, w, h), |(l, tp, r, b)| Rect::new(l, tp, r - l, b - tp));
let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) {
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => {
Cfa { width: 2, height: 2, pattern: p.to_vec(), origin_x: 0, origin_y: 0 }
}
_ => return Err(RawError::Unsupported("Samsung raw without a 2x2 CFAPattern".into())),
};
// saturation levels and black levels (R, G1, G2, B), in the file's own words