# Conflicts:
#	docs/parity-checklist.md
#	docs/raw-parity.md
#	docs/whats-new.md
This commit is contained in:
bflatastic
2026-10-13 11:02:49 +07:00
4 changed files with 136 additions and 6 deletions
+128 -6
View File
@@ -54,6 +54,8 @@ const WB_RB: u16 = 0x0100;
const BLACK: u16 = 0x0600;
const MN_WB_RED: u16 = 0x1017;
const MN_WB_BLUE: u16 = 0x1018;
const MN_BLACK: u16 = 0x1012;
const MN_VALID_BITS: u16 = 0x102c;
const CROP: [u16; 4] = [0x0612, 0x0613, 0x0614, 0x0615];
/// The Olympus maker note.
@@ -75,6 +77,17 @@ pub(crate) fn sub_ifd(bytes: &[u8], mn: &MakerNote, tag: u16) -> Option<Ifd> {
lightcraft_tiff::parse_ifd_at(bytes, at, mn.order, mn.base, false, &opts).ok().map(|(i, _)| i)
}
/// Black level of a body without ImageProcessing: the main note's `0x1012` BlackLevel (the mean of its four values)
/// in `0x102c` ValidBits units, scaled to the decoded `bits`. The E-10 states 32 at 10 bits, and its 12-bit samples
/// bottom out at 124 to 132; the two-field compacts state 0 at 12 bits.
fn maker_note_black(m: &MakerNote, bits: u32) -> Option<f32> {
let bits = bits.min(16);
let v = m.ifd.f64s(MN_BLACK).filter(|v| v.len() == 4)?;
let valid = m.ifd.u64(MN_VALID_BITS).and_then(|b| u32::try_from(b).ok()).filter(|b| (8..=bits).contains(b))?;
let level = v.iter().sum::<f64>() / 4.0 * f64::from(1u32 << (bits - valid));
(level >= 0.0 && level < f64::from(1u32 << bits) / 4.0).then_some(level as f32)
}
/// Unpack one row of 12-bit samples stored as little-endian 32-bit words read MSB-first.
pub(crate) fn unpack_row_le32_msb(src: &[u8], bits: u32, out: &mut [u16]) {
let swapped: Vec<u8> = src
@@ -282,7 +295,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
let values = a.pattern.iter().map(|&c| [v[0], (v[1] + v[2]) / 2.0, v[3]][c as usize] as f32).collect();
BlackLevel { repeat_rows: 2, repeat_cols: 2, values, ..Default::default() }
}
_ => BlackLevel::uniform(0.0),
_ => BlackLevel::uniform(mn.as_ref().filter(|_| ip.is_none()).and_then(|m| maker_note_black(m, bits)).unwrap_or(0.0)),
};
let wb = ip
.as_ref()
@@ -290,9 +303,10 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
.filter(|v| v.len() >= 2 && v[0] > 0.0 && v[1] > 0.0)
.map(|v| [(v[0] / 256.0) as f32, 1.0, (v[1] / 256.0) as f32])
.or_else(|| {
// two-field bodies: the red and blue gains lead the entries of the main maker note
let m = mn.as_ref().filter(|_| two_field)?;
let g = |tag| m.ifd.f64s(tag).and_then(|v| v.first().copied()).filter(|g| *g > 0.0);
// the red and blue gains lead the main note's entries 0x1017/0x1018 on the two-field compacts and on
// the E-10 (16-bit words, no ImageProcessing; 384 and 406 on its CC0 file 5424)
let m = mn.as_ref().filter(|_| two_field || ip.is_none())?;
let g = |tag| m.ifd.f64s(tag).and_then(|v| v.first().copied()).filter(|g| *g > 25.6 && *g < 2560.0);
Some([(g(MN_WB_RED)? / 256.0) as f32, 1.0, (g(MN_WB_BLUE)? / 256.0) as f32])
});
let white = white_from_data(samples, bits);
@@ -344,6 +358,11 @@ mod tests {
/// A minimal ORF with one strip in IFD0 and, optionally, an Exif `CFAPattern`.
fn orf_with(w: u32, h: u32, bits: u16, strip: Vec<u8>, cfa_pattern: Option<&[u8]>) -> Vec<u8> {
orf_with_note(w, h, bits, strip, cfa_pattern, None)
}
/// [`orf_with`] plus an optional maker note blob in the Exif IFD.
fn orf_with_note(w: u32, h: u32, bits: u16, strip: Vec<u8>, cfa_pattern: Option<&[u8]>, note: Option<Vec<u8>>) -> Vec<u8> {
let mut ifd = IfdBuilder::new();
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
@@ -352,9 +371,14 @@ mod tests {
ifd.set(t::PHOTOMETRIC, Value::Short(vec![1]));
ifd.set(t::MAKE, Value::Ascii("OLYMPUS IMAGING CORP.".into()));
ifd.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![strip] });
if let Some(p) = cfa_pattern {
if cfa_pattern.is_some() || note.is_some() {
let mut exif = IfdBuilder::new();
exif.set(EXIF_CFA_PATTERN, Value::Undefined(p.to_vec()));
if let Some(p) = cfa_pattern {
exif.set(EXIF_CFA_PATTERN, Value::Undefined(p.to_vec()));
}
if let Some(n) = note {
exif.set(t::MAKER_NOTE, Value::Undefined(n));
}
ifd.set_child(t::EXIF_IFD, exif);
}
let mut b = TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).unwrap();
@@ -363,6 +387,104 @@ mod tests {
b
}
/// An old-style little-endian Olympus maker note (`OLYMP\0`, version 1, then the IFD) of SHORT (type 3) or
/// UNDEFINED (type 7) entries. Values longer than four bytes follow the IFD; their offsets count from the TIFF
/// header, so `at` is where the note sits in the file.
fn olymp_note(at: usize, entries: &[(u16, u16, Vec<u8>)]) -> Vec<u8> {
let ifd_end = 8 + 2 + 12 * entries.len() + 4;
let (mut b, mut extra) = (b"OLYMP\0\x01\0".to_vec(), Vec::new());
b.extend_from_slice(&(entries.len() as u16).to_le_bytes());
for (tag, typ, value) in entries {
b.extend_from_slice(&tag.to_le_bytes());
b.extend_from_slice(&typ.to_le_bytes());
b.extend_from_slice(&(if *typ == 3 { value.len() / 2 } else { value.len() } as u32).to_le_bytes());
if value.len() <= 4 {
let mut v = value.clone();
v.resize(4, 0);
b.extend_from_slice(&v);
} else {
b.extend_from_slice(&((at + ifd_end + extra.len()) as u32).to_le_bytes());
extra.extend_from_slice(value);
}
}
b.extend_from_slice(&0u32.to_le_bytes());
b.extend_from_slice(&extra);
b
}
/// SHORT values, little-endian.
fn shorts(v: &[u16]) -> Vec<u8> {
v.iter().flat_map(|x| x.to_le_bytes()).collect()
}
/// A 16 × 4 ORF of 12-bit samples in 16-bit words with an [`olymp_note`] of `entries`, decoded.
fn decode_noted(entries: &[(u16, u16, Vec<u8>)]) -> RawImage {
let (w, h) = (16u32, 4u32);
let words: Vec<u8> = (0..w * h).flat_map(|i| (((200 + i * 37) % 4096) as u16).to_le_bytes()).collect();
let file = |at| orf_with_note(w, h, 16, words.clone(), Some(RGGB), Some(olymp_note(at, entries)));
let at = file(0).windows(6).position(|b| b == b"OLYMP\0").unwrap();
let bytes = file(at);
assert_eq!(bytes.windows(6).position(|b| b == b"OLYMP\0"), Some(at));
crate::decode(&bytes).unwrap()
}
/// An ImageProcessing sub-directory stored inline (old-style note) with one unrelated entry and no levels.
fn bare_image_processing() -> (u16, u16, Vec<u8>) {
let mut ifd = 1u16.to_le_bytes().to_vec();
ifd.extend_from_slice(&[0, 0, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0]);
ifd.extend_from_slice(&0u32.to_le_bytes());
(IMAGE_PROCESSING, 7, ifd)
}
/// Issue #1273: the E-10 (16-bit words, no ImageProcessing sub-directory) keeps its red and blue gains in
/// maker-note entries `0x1017`/`0x1018`, as the two-field compacts do.
#[test]
fn as_shot_white_balance_from_the_main_maker_note() {
let wb = |red: u16, blue: Option<u16>| {
let mut e = vec![(MN_WB_RED, 3, shorts(&[red, 64]))];
e.extend(blue.map(|b| (MN_WB_BLUE, 3, shorts(&[b, 64]))));
decode_noted(&e).wb_multipliers
};
assert_eq!(wb(384, Some(406)), Some([1.5, 1.0, 406.0 / 256.0]));
// both gains are needed, within 0.1 to 10 (25.6 to 2560 in 256 units)
assert_eq!(wb(384, None), None);
assert_eq!(wb(0, Some(406)), None);
assert_eq!(wb(25, Some(406)), None);
assert_eq!(wb(384, Some(2560)), None);
assert_eq!(wb(27, Some(2559)), Some([27.0 / 256.0, 1.0, 2559.0 / 256.0]));
assert_eq!(decode_noted(&[]).wb_multipliers, None);
// a body with ImageProcessing but without its levels doesn't fall back to these entries
let e = [bare_image_processing(), (MN_WB_RED, 3, shorts(&[384, 64])), (MN_WB_BLUE, 3, shorts(&[406, 64]))];
assert_eq!(decode_noted(&e).wb_multipliers, None);
}
/// Issue #1273: a body without ImageProcessing states its black level in the main note, `0x1012` (four
/// values) in `0x102c` ValidBits units: the E-10's 32 at 10 bits is 128 on its 12-bit samples. The two-field
/// compacts state 0 at 12 bits.
#[test]
fn black_level_from_the_main_maker_note() {
let black = |level: u16, valid: Option<u16>| {
let mut e = vec![(MN_BLACK, 3, shorts(&[level; 4]))];
e.extend(valid.map(|v| (MN_VALID_BITS, 3, shorts(&[v, 0]))));
let r = decode_noted(&e);
assert_eq!(r.bits, 12);
r.black.values
};
assert_eq!(black(32, Some(10)), vec![128.0]);
assert_eq!(black(0, Some(12)), vec![0.0]);
assert_eq!(black(32, Some(12)), vec![32.0]);
// the four values are averaged
let e = [(MN_BLACK, 3, shorts(&[30, 32, 32, 34])), (MN_VALID_BITS, 3, shorts(&[10, 0]))];
assert_eq!(decode_noted(&e).black.values, vec![128.0]);
// no ValidBits, ValidBits above the samples' depth, a level of a quarter of the range or more: no black
assert_eq!(black(32, None), vec![0.0]);
assert_eq!(black(32, Some(14)), vec![0.0]);
assert_eq!(black(256, Some(10)), vec![0.0]);
// ImageProcessing present (without BlackLevel2): the main note isn't read
let e = [bare_image_processing(), (MN_BLACK, 3, shorts(&[32; 4])), (MN_VALID_BITS, 3, shorts(&[10, 0]))];
assert_eq!(decode_noted(&e).black.values, vec![0.0]);
}
/// A 12-bit mosaic stored as 16-bit words: `site(cx, cy)` gives the four values of the 2×2 cell at (cx, cy).
fn mosaic(w: usize, h: usize, site: impl Fn(usize, usize) -> [u16; 4]) -> Vec<u8> {
(0..w * h).flat_map(|i| site(i % w / 2, i / w / 2)[((i / w) & 1) * 2 + ((i % w) & 1)].to_le_bytes()).collect()
File diff suppressed because one or more lines are too long
+1
View File
@@ -271,6 +271,7 @@ Beyond decoding:
| 2026-10-11 | minor | Leica Q3 verified by a recorded check of 38 private DNGs (no decode failure; mean ΔE to the camera JPEG 6.3–13.9): 236 verified, 941 unverified, Leica bucket on a verified body 5% → 21%, weighted shares unchanged |
| 2026-10-12 | patch | CR2 white level from the active area, not the saturated masked rows above it (#1272: EOS 50D, 60D, 450D (Rebel XSi), 500D (Rebel T1i), 550D (Rebel T2i), 1200D (Rebel T5) and 100D (Rebel SL1)); scores unchanged |
| 2026-10-12 | minor | Canon PowerShot G9: as-shot white balance from WBInfo (`0x0029`, the row of the preset in ShotInfo), issue #1273; render unchanged (the camera-look fit already absorbed it), As Shot now the camera's; counts unchanged |
| 2026-10-12 | minor | Olympus E-10: as-shot white balance (`0x1017`/`0x1018`) and black level (`0x1012` in `0x102c` ValidBits units) from the main maker note, issue #1273; ΔE76 against its thumbnail 18.5 → 11.5, camera-look fit now accepted; counts unchanged |
| 2026-10-10 | minor | Compressed ORF: pointer to the two Olympus patents that describe the method, as a clean-room starting point |
| 2026-10-12 | minor | PEF without area tags: masked border rows and columns trimmed against the BlackPoint (issue #1271; KP, KF, K-3 Mark III, K-70, K-1, K-1 Mark II, K-S1, K-S2, 645Z); counts unchanged |
| 2026-10-11 | minor | EOS R5 Mark II verified (CRX QP-map first literal and limited-length escape, odd-height Bayer planes; 4 CC0 files pinned, QP maps of 33,665 private files): 235 verified, 942 unverified; use-weighted 85% (42% on verified bodies); decoding 77%, RAW readiness unchanged at ~56% |
+5
View File
@@ -49,6 +49,11 @@
### Older raw cameras
- Canon PowerShot G9 raws open with the camera's white balance (White Balance ▸ As Shot); it used to start from a
neutral default.
## 2026-10-12
### Older raw cameras
- Olympus E-10 raws open with the camera's white balance (White Balance ▸ As Shot) and without the grey haze in
the shadows: the black level recorded in the file is now subtracted.
## 2026-10-11