Merge PR #1357
# Conflicts: # docs/parity-checklist.md # docs/raw-parity.md # docs/whats-new.md
This commit is contained in:
commit
bcbcf3b968
|
|
@ -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
|
|
@ -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% |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue