# Conflicts:
#	docs/parity-checklist.md
#	docs/raw-parity.md
#	docs/whats-new.md
This commit is contained in:
bflatastic 2026-10-13 11:02:48 +07:00
commit bcbcf3b968
4 changed files with 115 additions and 9 deletions

View File

@ -37,6 +37,10 @@ const SRAW_YCC: u32 = 4;
const CR2_CFA_PATTERN: u16 = 0xc5e0;
const SENSOR_INFO: u16 = 0x00e0;
const COLOR_BALANCE: u16 = 0x4001;
/// ShotInfo, whose value 7 is the white-balance setting.
const SHOT_INFO: u16 = 0x0004;
/// WBInfo, the PowerShot G9's preset levels (it writes no `ColorData`).
const WB_INFO: u16 = 0x0029;
fn raw_ifd(tiff: &Tiff) -> Option<&Ifd> {
tiff.ifds.get(3).or_else(|| tiff.ifds.iter().rev().find(|i| i.contains(CR2_SLICE)))
@ -65,23 +69,51 @@ fn words(v: &Value, order: ByteOrder) -> Vec<u64> {
}
}
/// WB multipliers (R, G, B; G = 1) from one set of levels, when they are plausible.
fn wb_from_levels(r: f32, g1: f32, g2: f32, b: f32) -> Option<[f32; 3]> {
if g1 < 256.0 || g2 < 256.0 || r < 64.0 || b < 64.0 || g1 > 16384.0 || (g1 - g2).abs() > 0.05 * g1 {
return None;
}
let g = (g1 + g2) / 2.0;
let (mr, mb) = (r / g, b / g);
((0.25..6.0).contains(&mr) && (0.25..6.0).contains(&mb)).then_some([mr, 1.0, mb])
}
/// As-shot WB multipliers (R, G, B; G = 1) from the ColorBalance array.
fn wb_from_color_balance(v: &[u64]) -> Option<[f32; 3]> {
for off in [63usize, 25, 24, 34, 71, 85, 105, 69, 77] {
let q = v.get(off..off + 4)?;
let (r, g1, g2, b) = (q[0] as f32, q[1] as f32, q[2] as f32, q[3] as f32);
if g1 < 256.0 || g2 < 256.0 || r < 64.0 || b < 64.0 || g1 > 16384.0 || (g1 - g2).abs() > 0.05 * g1 {
continue;
}
let g = (g1 + g2) / 2.0;
let (mr, mb) = (r / g, b / g);
if (0.25..6.0).contains(&mr) && (0.25..6.0).contains(&mb) {
return Some([mr, 1.0, mb]);
if let Some(wb) = wb_from_levels(q[0] as f32, q[1] as f32, q[2] as f32, q[3] as f32) {
return Some(wb);
}
}
None
}
/// As-shot WB multipliers of a body without `ColorData` (the PowerShot G9): the WBInfo row of the preset that
/// ShotInfo's white-balance value names. WBInfo is int32 values in the maker note's byte order, four per preset in
/// G R B G order, at the rows the ExifTool Canon tag-name documentation gives (2 Auto, 10 Daylight, 18 Cloudy,
/// 26 Tungsten, 34 Fluorescent, 50 Flash, 58 Underwater, 66 Custom 1, 74 Custom 2); other settings (14, Daylight
/// Fluorescent, has no documented row) give none. Measured on the CC0 file IMG_1188.CR2 (Custom 1, row 66:
/// 848 1747 1676 848), whose camera JPEG's neutral areas imply the same gains within 0.1 %.
fn wb_from_wb_info(m: &makernote::MakerNote) -> Option<[f32; 3]> {
let row = match m.ifd.u64s(SHOT_INFO)?.get(7)? {
0 => 2,
1 => 10,
2 => 18,
3 => 26,
4 => 34,
5 => 50,
15 => 66,
16 => 74,
17 => 58,
_ => return None,
};
let b = m.ifd.bytes(WB_INFO)?;
let level = |i: usize| Some(m.order.u32(b.get(4 * (row + i)..4 * (row + i + 1))?.try_into().ok()?) as i32 as f32);
wb_from_levels(level(1)?, level(0)?, level(3)?, level(2)?)
}
/// The optically black columns left of the image area: from column 2 up to two columns before the first column
/// whose mean (over the image rows) rises above the leftmost columns' level by more than 1/256 of the remaining
/// range (at least 16), else up to two columns before the image area. Empty when the border is too narrow.
@ -187,7 +219,11 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
active = Rect::new(l, tp, r - l + 1, b - tp + 1);
}
}
let wb = mn.as_ref().and_then(|m| m.ifd.value(COLOR_BALANCE).map(|v| words(v, m.order))).and_then(|v| wb_from_color_balance(&v));
let wb = mn
.as_ref()
.and_then(|m| m.ifd.value(COLOR_BALANCE).map(|v| words(v, m.order)))
.and_then(|v| wb_from_color_balance(&v))
.or_else(|| wb_from_wb_info(mn.as_ref()?));
let cfa = cfa_from_tag(raw.u64(CR2_CFA_PATTERN)).unwrap_or_else(|| Cfa::bayer_static("RGGB"));
let white = white_from_data(&active_rows(&data, width, active), prec as u32);
let black = match masked_columns(&data, width, active, white) {
@ -384,6 +420,68 @@ pub(crate) mod tests {
crate::decode(&bytes).unwrap()
}
/// A maker note of `entries` (tag, TIFF type, value bytes), each value stored after the IFD, decoded; the value
/// offsets (relative to the TIFF header) are patched once the note's position is known.
fn decode_with_note(entries: &[(u16, u16, Vec<u8>)]) -> RawImage {
let ifd_len = 2 + 12 * entries.len() + 4;
let (mut note, mut data, mut at) = (Vec::new(), Vec::new(), Vec::new());
note.extend_from_slice(&(entries.len() as u16).to_le_bytes());
for (tag, ty, value) in entries {
let count = if *ty == 3 { value.len() / 2 } else { value.len() } as u32;
note.extend_from_slice(&tag.to_le_bytes());
note.extend_from_slice(&ty.to_le_bytes());
note.extend_from_slice(&count.to_le_bytes());
at.push((note.len(), ifd_len + data.len()));
note.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(value);
}
note.extend_from_slice(&0u32.to_le_bytes());
note.extend_from_slice(&data);
let (mut bytes, _) = synthetic_cr2_note(64, 10, None, Some(note));
let tiff = Tiff::parse(&bytes).unwrap();
let off = tiff.exif().unwrap().get(t::MAKER_NOTE).unwrap().offset as usize;
for (field, value) in at {
bytes[off + field..off + field + 4].copy_from_slice(&((off + value) as u32).to_le_bytes());
}
crate::decode(&bytes).unwrap()
}
/// Issue #1273: the PowerShot G9 has no `ColorData`; its preset levels are WBInfo (`0x0029`: BYTE, int32 values,
/// four per preset in G R B G order), and ShotInfo (`0x0004`) value 7 says which preset was used (15 = Custom 1,
/// row 66: 848 1747 1676 848 on the CC0 file IMG_1188.CR2).
#[test]
fn white_balance_from_wb_info_when_there_is_no_color_data() {
let shot_info = |wb: u16| {
let mut v = [0u16; 34];
v[7] = wb;
v.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>()
};
let mut rows = vec![0i32; 512];
rows[2..6].copy_from_slice(&[850, 1805, 1684, 850]);
rows[66..70].copy_from_slice(&[848, 1747, 1676, 848]);
rows[74..78].copy_from_slice(&[820, 1901, 4000, 820]);
rows[18..22].copy_from_slice(&[850, 1805, 6000, 850]);
let wb_info: Vec<u8> = rows.iter().flat_map(|x| x.to_le_bytes()).collect();
let wb = |si: u16| decode_with_note(&[(0x0004, 3, shot_info(si)), (0x0029, 1, wb_info.clone())]).wb_multipliers;
assert_eq!(wb(15), Some([1747.0 / 848.0, 1.0, 1676.0 / 848.0]));
assert_eq!(wb(0), Some([1805.0 / 850.0, 1.0, 1684.0 / 850.0]));
// each preset its own row (Custom 2: row 74); an empty row (Daylight here), an implausible one (Cloudy:
// B 7.1) or a value without a documented row (14) gives none
assert_eq!(wb(16), Some([1901.0 / 820.0, 1.0, 4000.0 / 820.0]));
assert_eq!(wb(1), None);
assert_eq!(wb(2), None);
assert_eq!(wb(14), None);
// without ShotInfo, or with WBInfo too short for the row
assert_eq!(decode_with_note(&[(0x0029, 1, wb_info.clone())]).wb_multipliers, None);
assert_eq!(decode_with_note(&[(0x0004, 3, shot_info(15)), (0x0029, 1, wb_info[..4 * 68].to_vec())]).wb_multipliers, None);
// ColorData, when it gives levels, wins
let mut color_data = vec![0u8; 2 * 120];
color_data[0..2].copy_from_slice(&[0xfd, 0xff]);
color_data[142..150].copy_from_slice(&[0xc4, 0x07, 0xe9, 0x02, 0xe9, 0x02, 0x69, 0x06]);
let both = decode_with_note(&[(0x0004, 3, shot_info(15)), (0x0029, 1, wb_info.clone()), (COLOR_BALANCE, 7, color_data)]);
assert_eq!(both.wb_multipliers, Some([1988.0 / 745.0, 1.0, 1641.0 / 745.0]));
}
/// `ColorData` stored as UNDEFINED bytes (versions -3 / -4: PowerShot, EOS M) gives the as-shot WB from word 71,
/// exactly as the same words stored as SHORT do.
#[test]

File diff suppressed because one or more lines are too long

View File

@ -270,6 +270,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-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% |

View File

@ -44,6 +44,11 @@
### Bold text in other scripts
- Arabic names and other Arabic text in headings, chosen tabs and other bold places are now drawn bold, as Latin text
beside them is; they used to stay at regular weight.
## 2026-10-12
### 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-11