mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 12:08:10 -06:00
Merge PR #1297
# Conflicts: # docs/parity-checklist.md # docs/raw-parity.md # docs/whats-new.md
This commit is contained in:
Vendored
+128
-6
@@ -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
@@ -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% |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user