Merge PR #575
# Conflicts: # crates/engine/src/media.rs # crates/raw/tests/corpus.rs # docs/parity.md # xtask/src/main.rs
This commit is contained in:
commit
4a8241dde4
|
|
@ -176,8 +176,8 @@ CR3 lossless Bayer and version 0x100/0x200 C-RAW sensor decoding is verified pix
|
|||
|
||||
Decoded (CC0 corpus from raw.pixls.us, `cargo xtask corpus --download`, `crates/raw/tests/corpus.rs`): DNG (uncompressed,
|
||||
LJ92, lossy JPEG / Smart Previews, Deflate, float, linear, DNG 1.7 JPEG XL tiles: lossless ones bit-exact on synthetic files,
|
||||
lossy ones unclipped in the declared linear primaries, checked on one real file), CR2, ARW (uncompressed, ARW2, LJ92; as-shot white balance and black level of pre-2017 bodies from the enciphered
|
||||
maker-note `Tag2010` and the encrypted `SR2SubIFD`, both recovered by black-box analysis, `crates/raw/src/vendor/arw.rs`), NEF/NRW uncompressed and Huffman-compressed (lossless, lossy type 1/2, 12/14-bit), RAF uncompressed and lossless/lossy compressed (Bayer and
|
||||
lossy ones unclipped in the declared linear primaries, checked on one real file), CR2, ARW (uncompressed, ARW2, LJ92; as-shot white balance and black level of pre-2017 bodies from the encrypted
|
||||
`SR2SubIFD`, decrypted with a keystream recovered by black-box analysis, `crates/raw/src/vendor/sr2.rs`, `crates/raw/src/vendor/arw.rs`), NEF/NRW uncompressed and Huffman-compressed (lossless, lossy type 1/2, 12/14-bit), RAF uncompressed and lossless/lossy compressed (Bayer and
|
||||
X-Trans, 14/16-bit real-file verification), RW2 / Leica RWL / Panasonic RAW in every raw format (compressed formats 4 and 6, the prefix-coded strips of format 8,
|
||||
packed formats 2, 5 and 7, the 16-bit words of the 2005–2007 bodies; all recovered by black-box analysis of 178 CC0 files from
|
||||
118 bodies, `crates/raw/src/vendor/rw2.rs`), PEF (uncompressed and Huffman), ORF uncompressed (16-bit and 12-bit packed). Supported containers yield their embedded JPEG preview when present (including CR3). The engine uses it for RAW variants it cannot decode yet; HEVC-only CR3 previews are not decoded.
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ impl SettingsHashes {
|
|||
}
|
||||
|
||||
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
|
||||
pub const RENDER_CACHE_VERSION: u64 = 30;
|
||||
pub const RENDER_CACHE_VERSION: u64 = 31;
|
||||
|
||||
/// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
|
||||
pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];
|
||||
|
|
|
|||
|
|
@ -14,11 +14,16 @@
|
|||
//! a 128-bit little-endian block: 11-bit max, 11-bit min, 4-bit index of the max, 4-bit index of the min and 14
|
||||
//! seven-bit deltas above the min, scaled by the smallest shift that fits `max − min` into 7 bits.
|
||||
//!
|
||||
//! White balance: the raw IFD's `0x7313` levels when present (bodies from about 2017 on); otherwise the
|
||||
//! `WB_RGBLevels` of the maker note's enciphered `Tag2010` block (see [`DECIPHER`] and [`TAG2010_WB`]). Without
|
||||
//! either, an ARW opened with unit multipliers, i.e. a strong green cast (issue #148). Black level: the raw IFD's
|
||||
//! `0x7310`, else the level stored in the encrypted `SR2SubIFD` (see [`SR2_BLACK_AT`]; 800 rather than the
|
||||
//! default 512 on 1″-sensor bodies such as the RX100 series), else 512 (14-bit) / 128 (12-bit).
|
||||
//! White balance: `WB_RGGBLevels` (`0x7313`), the gains the camera applied. Bodies from about 2017 on write them in
|
||||
//! plain form in the raw IFD; every ARW from the DSLR-A200 on has them in the encrypted `SR2SubIFD` (see
|
||||
//! [`super::sr2`]), and the two agree on every file that has both. Only when neither can be read do we fall back on
|
||||
//! the `WB_RGBLevels` of the maker note's enciphered `Tag2010` block (see [`DECIPHER`] and [`TAG2010_WB`]), which
|
||||
//! can differ widely from what the camera applied (issue #535). Without any, an ARW opened with unit multipliers,
|
||||
//! i.e. a strong green cast (issue #148).
|
||||
//!
|
||||
//! Black level: the raw IFD's `0x7310`, else the level stored in the encrypted `SR2SubIFD` (see [`sr2_black`]; 800
|
||||
//! rather than the default 512 on 1″-sensor bodies such as the RX100 series), else 512 (14-bit) / 128 (12-bit).
|
||||
//! Downsized lossless (YCbCr) data sit [`YCBCR_OFFSET`] above that.
|
||||
//!
|
||||
//! Also: uncompressed 16-bit ARW, and lossless-compressed ARW (Compression 7, ILCE-7M4 and later): LJ92 tiles whose
|
||||
//! frames hold one 2×2 CFA cell per four-component sample ([`read_quad_tiles`]). Other lossless-JPEG layouts go
|
||||
|
|
@ -79,9 +84,12 @@ const DECIPHER: [u8; 256] = {
|
|||
};
|
||||
|
||||
/// `Tag2010` layouts (ExifTool "Sony Tag2010a" … "Tag2010i" tables): the models each is documented for and the
|
||||
/// byte offset of `WB_RGBLevels` (three `u16`, the as-shot white balance as R, G, B gains; Tag2010g and h share
|
||||
/// the offset). Newer bodies (Tag2010i) also write the levels in plain form in the raw IFD (`0x7313`), which takes
|
||||
/// precedence.
|
||||
/// byte offset of `WB_RGBLevels` (three `u16`, R, G, B gains; Tag2010g and h share the offset). Only a fallback
|
||||
/// for files whose `WB_RGGBLevels` (`0x7313`, plain or in the `SR2SubIFD`) cannot be read. On the 45 CC0
|
||||
/// raw.pixls.us ARWs that have both but no plain `0x7313`, the two agree within 5 % on 35 and differ by 8–100 %
|
||||
/// on the other 10: five of the eight shot with a preset, colour temperature or custom white balance (ILCE-3500 at
|
||||
/// 5600 K: R −34 %, B +100 %) and five in Auto (SLT-A37: R −32 %, B +53 %). Where the camera JPEG has enough
|
||||
/// neutral pixels to tell, the gains the camera applied are `WB_RGGBLevels`, not these.
|
||||
const TAG2010_WB: &[(&[&str], usize)] = &[
|
||||
(&["NEX-5N"], 4476),
|
||||
(&["SLT-A65", "SLT-A77", "NEX-7", "NEX-VG20E"], 4480),
|
||||
|
|
@ -171,9 +179,9 @@ const TAG2010_WB: &[(&[&str], usize)] = &[
|
|||
),
|
||||
];
|
||||
|
||||
/// As-shot white balance from the maker note's enciphered `Tag2010` block (bodies that do not write `0x7313` in
|
||||
/// the raw IFD: most ARWs before 2017). `model` is the Exif model; Sony appends a regional "V" to some names
|
||||
/// (SLT-A77V), which the documented lists omit.
|
||||
/// White balance from the maker note's enciphered `Tag2010` block, the fallback described at [`TAG2010_WB`].
|
||||
/// `model` is the Exif model; Sony appends a regional "V" to some names (SLT-A77V), which the documented lists
|
||||
/// omit.
|
||||
fn tag2010_wb(model: &str, block: &[u8], order: lightcraft_tiff::ByteOrder) -> Option<[f32; 3]> {
|
||||
let model = model.trim();
|
||||
let &(_, offset) = TAG2010_WB.iter().find(|(models, _)| models.iter().any(|m| model == *m || model.strip_suffix('V') == Some(*m)))?;
|
||||
|
|
@ -188,6 +196,18 @@ fn tag2010_wb(model: &str, block: &[u8], order: lightcraft_tiff::ByteOrder) -> O
|
|||
((0.2..=8.0).contains(&r) && (0.2..=8.0).contains(&b)).then_some([r, 1.0, b])
|
||||
}
|
||||
|
||||
/// White-balance gains `[R/G, 1, B/G]` from `WB_RGGBLevels` (R, G, G, B); `None` unless there are four levels
|
||||
/// with a positive green and plausible ratios.
|
||||
fn rggb_gains(levels: &[f64]) -> Option<[f32; 3]> {
|
||||
let &[r, g1, g2, b] = levels else { return None };
|
||||
let g = (g1 + g2) / 2.0;
|
||||
if !g.is_finite() || g <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let (r, b) = (r / g, b / g);
|
||||
((0.2..=8.0).contains(&r) && (0.2..=8.0).contains(&b)).then_some([r as f32, 1.0, b as f32])
|
||||
}
|
||||
|
||||
/// The inverse tone curve: 11-bit code → 14-bit sensor value.
|
||||
pub(crate) fn code_curve(thresholds: &[u64]) -> Vec<u16> {
|
||||
let mut bp = [0usize, 4095, 4095, 4095, 4095, 4095];
|
||||
|
|
@ -319,6 +339,18 @@ fn read_quad_tiles(bytes: &[u8], info: &ImageInfo) -> Result<Vec<u16>> {
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
/// Sony's downsized lossless ARWs (M and S sizes: linear YCbCr, already white-balanced) are stored this far above the
|
||||
/// sensor black level the raw IFD records (`0x7310`, 512 in every file seen); their reference levels say Y black 0.
|
||||
/// Measured against a full-size coding of the same scene shot seconds apart, on the pixels where both camera JPEGs
|
||||
/// agree: YCbCr = k · (full-size − black) · WB + b, with b 1015–1052 and green k 0.98–1.02, on 11 of 12 files from
|
||||
/// four bodies (ILCE-7M4 M, S and APS-C S; ILCE-7RM5 M, S and S35 S; ILCE-7CR M, S and S35 S; ILCE-9M3 M, S and
|
||||
/// APS-C S; the twelfth's reference was shot 24 s later, 7 % brighter). Their darkest 0.1 % lies at 1047–1127. So
|
||||
/// black and white both move up by 512: the scale stays the full-size raw's, and an M/S file renders like its
|
||||
/// full-size twin. Subtracting only 512 left a pedestal of 3 % of white, lifting the shadows (issue #535).
|
||||
const YCBCR_OFFSET: f32 = 512.0;
|
||||
/// White of the YCbCr data before [`YCBCR_OFFSET`]: the reference levels' Y white (required by [`read_ycbcr_tiles`]).
|
||||
const YCBCR_WHITE: f32 = 16383.0;
|
||||
|
||||
/// Downsized M/S ARWs are linear YCbCr 4:2:0 / 4:2:2, not CFA, despite retaining dummy CFA tags.
|
||||
/// T.81 supplies the lossless coding; TIFF 6.0 supplies the YCbCr coefficients and reference
|
||||
/// levels. Sony's reference chroma black and white are identical (the neutral offset).
|
||||
|
|
@ -384,47 +416,23 @@ fn read_ycbcr_tiles(bytes: &[u8], info: &ImageInfo, raw: &Ifd, mode: Mode) -> Re
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
/// `SR2Private` tags (ExifTool "Sony SR2Private" table; the IFD is referenced by IFD0's `DNGPrivateData`).
|
||||
const SR2_SUBIFD_OFFSET: u16 = 0x7200;
|
||||
const SR2_SUBIFD_LENGTH: u16 = 0x7201;
|
||||
const SR2_SUBIFD_KEY: u16 = 0x7221;
|
||||
/// Black level inside the encrypted `SR2SubIFD` (bodies that do not write `0x7310` in the raw IFD), recovered by
|
||||
/// black-box known-plaintext analysis of CC0 raw.pixls.us samples; no description of Sony's cipher was used.
|
||||
/// The black level stored in the encrypted `SR2SubIFD` (`0x7310`; bodies that do not write it in the raw IFD keep
|
||||
/// it only there), in 14-bit units; `None` unless it is SHORT, count 4, and the four levels are equal and plausible.
|
||||
///
|
||||
/// Every Sony ARW we examined (16 files, 15 bodies, 2012–2021) stores the same key (`11 22 33 44`), so its
|
||||
/// keystream is the same in every file. The DSC-RX100M6 and ILCE-7M3 also write their black levels in plain form
|
||||
/// (`0x7310`: 800 and 512); XOR-ing their `SR2SubIFD`s against each other matched `pack(512×4) ^ pack(800×4)` at
|
||||
/// exactly one position, byte 2510, which gave the eight keystream bytes there. Applied to the other files at that
|
||||
/// position they decode to four equal levels that agree with each sensor's dark-pixel floor: 800 on every 1″-sensor
|
||||
/// body (RX10, RX100 … RX100M5), 512 on the APS-C/full-frame ones (NEX-5N/6, SLT-A77, ILCE-6000/7/7M2/7RM2), in
|
||||
/// both `SR2SubIFD` sizes seen (29252 and 56958 bytes). Newer layouts (ILCE-7M4, 35398 bytes) put other data there
|
||||
/// and are rejected by the plausibility checks; those bodies write `0x7310` anyway.
|
||||
const SR2_BLACK_AT: usize = 2510;
|
||||
const SR2_BLACK_KEYSTREAM: [u8; 8] = [0x43, 0xcb, 0x86, 0xb6, 0x11, 0xd2, 0x1a, 0x73];
|
||||
const SR2_KEY: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
|
||||
|
||||
/// The per-channel black level from the encrypted `SR2SubIFD` (see [`SR2_BLACK_AT`]), in 14-bit units; `None`
|
||||
/// unless the file uses the known key and the four decoded levels are equal and plausible.
|
||||
fn sr2_black(bytes: &[u8], ifd0: &Ifd, order: lightcraft_tiff::ByteOrder) -> Option<f32> {
|
||||
let private = ifd0.bytes(t::DNG_PRIVATE_DATA)?;
|
||||
let at = order.read_u32(private, 0)? as u64;
|
||||
let opts = lightcraft_tiff::ParseOptions { max_ifds: 1, max_depth: 0, follow_children: false, ..Default::default() };
|
||||
let (sr2, _) = lightcraft_tiff::parse_ifd_at(bytes, at, order, 0, false, &opts).ok()?;
|
||||
if sr2.bytes(SR2_SUBIFD_KEY)? != SR2_KEY {
|
||||
return None;
|
||||
/// Its value sits at a layout-dependent position (1638 … 2786 bytes into the block), which the directory entry
|
||||
/// gives. Reading a fixed position instead (2510, right for the 29252, 33210 and 56958-byte layouts) read other data
|
||||
/// on the DSLR-A450/A500/A550 (27152 bytes: 354–365) and the DSLR-A700 (62112 bytes: 975), whose black is 512 by
|
||||
/// ExifTool and by the data floor (issue #535).
|
||||
fn sr2_black(sr2: &super::sr2::SubIfd) -> Option<f32> {
|
||||
match sr2.short_bytes(BLACK_LEVEL)? {
|
||||
(3, plain) if plain.len() == 8 => sr2_black_levels(plain, sr2.order()),
|
||||
_ => None,
|
||||
}
|
||||
let (start, len) = (usize::try_from(sr2.u64(SR2_SUBIFD_OFFSET)?).ok()?, usize::try_from(sr2.u64(SR2_SUBIFD_LENGTH)?).ok()?);
|
||||
if len < SR2_BLACK_AT + 8 {
|
||||
return None;
|
||||
}
|
||||
sr2_black_levels(bytes.get(start.checked_add(SR2_BLACK_AT)?..start.checked_add(SR2_BLACK_AT + 8)?)?, order)
|
||||
}
|
||||
|
||||
/// Decipher the eight `SR2SubIFD` bytes at [`SR2_BLACK_AT`] into one black level (the mean of four near-equal,
|
||||
/// plausible per-channel levels).
|
||||
fn sr2_black_levels(cipher: &[u8], order: lightcraft_tiff::ByteOrder) -> Option<f32> {
|
||||
let plain: Vec<u8> = cipher.iter().zip(SR2_BLACK_KEYSTREAM).map(|(c, k)| c ^ k).collect();
|
||||
let levels: Vec<u16> = (0..4).filter_map(|i| order.read_u16(&plain, 2 * i)).collect();
|
||||
/// One black level from the four deciphered per-channel levels (their mean), when they are near-equal and plausible.
|
||||
fn sr2_black_levels(plain: &[u8], order: lightcraft_tiff::ByteOrder) -> Option<f32> {
|
||||
let levels: Vec<u16> = (0..4).filter_map(|i| order.read_u16(plain, 2 * i)).collect();
|
||||
let (&lo, &hi) = (levels.iter().min()?, levels.iter().max()?);
|
||||
(levels.len() == 4 && (64..=4096).contains(&lo) && hi - lo <= 64).then(|| levels.iter().map(|&v| f32::from(v)).sum::<f32>() / 4.0)
|
||||
}
|
||||
|
|
@ -523,15 +531,19 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
|||
_ => Cfa::bayer_static("RGGB"),
|
||||
};
|
||||
let default_black = if scale_bits >= 14 { 512.0 } else { 128.0 };
|
||||
// the decrypted SR2SubIFD, read once and only when a plain black level or white balance is missing
|
||||
let sr2_cell = std::cell::OnceCell::new();
|
||||
let sr2 = || sr2_cell.get_or_init(|| super::sr2::SubIfd::read(bytes, ifd0, tiff.order)).as_ref();
|
||||
let black = match raw.f64s(BLACK_LEVEL).as_deref() {
|
||||
Some(v) if linear_rgb && !v.is_empty() => BlackLevel::uniform((v.iter().sum::<f64>() / v.len() as f64) as f32),
|
||||
Some(v) if linear_rgb && !v.is_empty() => BlackLevel::uniform((v.iter().sum::<f64>() / v.len() as f64) as f32 + YCBCR_OFFSET),
|
||||
Some([a, b, c, d]) => {
|
||||
BlackLevel { repeat_rows: 2, repeat_cols: 2, values: vec![*a as f32, *b as f32, *c as f32, *d as f32], ..Default::default() }
|
||||
}
|
||||
_ => BlackLevel::uniform(sr2_black(bytes, ifd0, tiff.order).filter(|_| scale_bits >= 14).unwrap_or(default_black)),
|
||||
_ if linear_rgb => BlackLevel::uniform(default_black + YCBCR_OFFSET),
|
||||
_ => BlackLevel::uniform((scale_bits >= 14).then(|| sr2().and_then(sr2_black)).flatten().unwrap_or(default_black)),
|
||||
};
|
||||
let white = if linear_rgb {
|
||||
16383.0
|
||||
YCBCR_WHITE + YCBCR_OFFSET
|
||||
} else {
|
||||
raw.f64(t::WHITE_LEVEL).map(|v| v as f32).filter(|v| *v > 0.0).unwrap_or_else(|| super::white_from_data(samples, scale_bits))
|
||||
};
|
||||
|
|
@ -542,11 +554,8 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
|||
.and_then(|e| makernote::parse_makernote(bytes, e.offset, e.count() as u64, tiff.order, &ifd0.string(t::MAKE).unwrap_or_default()));
|
||||
let wb = raw
|
||||
.f64s(WB_RGGB)
|
||||
.filter(|v| v.len() == 4 && v[1] > 0.0 && v[0] > 0.0 && v[3] > 0.0)
|
||||
.map(|v| {
|
||||
let g = (v[1] + v[2]) / 2.0;
|
||||
[(v[0] / g) as f32, 1.0, (v[3] / g) as f32]
|
||||
})
|
||||
.and_then(|v| rggb_gains(&v))
|
||||
.or_else(|| rggb_gains(&sr2()?.shorts(WB_RGGB)?))
|
||||
.or_else(|| mn.as_ref().and_then(|m| tag2010_wb(&model, m.ifd.bytes(MN_TAG2010)?, m.order)));
|
||||
let crop = match (raw.u64s(CROP_TOP_LEFT).as_deref(), raw.u64s(CROP_SIZE).as_deref()) {
|
||||
(Some([x, y]), Some([cw, ch])) if *cw > 0 && *ch > 0 => Rect::new(*x as usize, *y as usize, *cw as usize, *ch as usize).clipped(w, h),
|
||||
|
|
@ -608,6 +617,9 @@ mod tests {
|
|||
assert!(full.cfa.is_none());
|
||||
assert_eq!(full.data.len(), 48);
|
||||
assert_eq!(full.wb_multipliers, Some([1.0; 3]));
|
||||
// the YCbCr data sit 512 above the recorded sensor black level; white moves with it
|
||||
assert_eq!((full.black.mean(), full.white_at(0)), (1024.0, 16895.0));
|
||||
assert_eq!((header.black.mean(), header.white_at(0)), (1024.0, 16895.0));
|
||||
let RawData::U16(ref pixels) = full.data else {
|
||||
panic!("integer ARW");
|
||||
};
|
||||
|
|
@ -833,19 +845,219 @@ mod tests {
|
|||
assert_eq!(crate::embedded_preview_dynamic_range_optimized(&lightcraft_tiff::TiffWriter::new(Little, false).write(&[ifd0]).unwrap()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rggb_gains_need_four_plausible_levels() {
|
||||
assert_eq!(rggb_gains(&[2048.0, 1024.0, 1024.0, 1536.0]), Some([2.0, 1.0, 1.5]));
|
||||
assert_eq!(rggb_gains(&[2048.0, 1000.0, 1048.0, 1536.0]), Some([2.0, 1.0, 1.5]));
|
||||
for bad in [
|
||||
&[2048.0, 1024.0, 1024.0][..],
|
||||
&[2048.0, 1024.0, 1024.0, 1536.0, 1.0],
|
||||
&[2048.0, 1024.0, -1024.0, 1536.0],
|
||||
&[2048.0, 0.0, 0.0, 1536.0],
|
||||
&[2048.0, f64::NAN, 1024.0, 1536.0],
|
||||
&[-2048.0, 1024.0, 1024.0, 1536.0],
|
||||
&[9000.0, 1024.0, 1024.0, 1536.0],
|
||||
&[2048.0, 1024.0, 1024.0, f64::INFINITY],
|
||||
] {
|
||||
assert_eq!(rggb_gains(bad), None, "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A 32 × 4 ARW2 file whose white balance can come from the raw IFD's `0x7313`, an encrypted `SR2SubIFD`
|
||||
/// (reached through `DNGPrivateData` and the `SR2Private` IFD, as Sony stores it, with `key`; black level 800)
|
||||
/// and a maker note whose `Tag2010` block (RX100 III layout) holds `WB_RGBLevels`.
|
||||
fn arw_with_wb(plain: Option<[u16; 4]>, sr2: Option<[u16; 4]>, tag2010: Option<[u16; 3]>, key: [u8; 4]) -> Vec<u8> {
|
||||
use lightcraft_tiff::{ByteOrder::Little, IfdBuilder, ImageData, TiffWriter, Value};
|
||||
let mut tag2010_block = vec![0u8; 700];
|
||||
for (i, v) in tag2010.unwrap_or([0; 3]).iter().enumerate() {
|
||||
tag2010_block[612 + 2 * i..614 + 2 * i].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
let tag2010_block = encipher(&tag2010_block);
|
||||
// the maker note: "SONY DSC " header, then an IFD whose offsets are file offsets
|
||||
let note = |at: usize| -> Vec<u8> {
|
||||
let mut n = b"SONY DSC \0\0\0".to_vec();
|
||||
n.extend([1, 0]);
|
||||
n.extend(MN_TAG2010.to_le_bytes());
|
||||
n.extend(7u16.to_le_bytes());
|
||||
n.extend((tag2010_block.len() as u32).to_le_bytes());
|
||||
n.extend(((at + n.len() + 8) as u32).to_le_bytes());
|
||||
n.extend([0; 4]);
|
||||
n.extend(&tag2010_block);
|
||||
n
|
||||
};
|
||||
let mut raw = IfdBuilder::new();
|
||||
raw.set(t::MAKE, Value::Ascii("SONY".into()));
|
||||
raw.set(t::MODEL, Value::Ascii("DSC-RX100M3".into()));
|
||||
raw.set(t::IMAGE_WIDTH, Value::Long(vec![32]));
|
||||
raw.set(t::IMAGE_LENGTH, Value::Long(vec![4]));
|
||||
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![12]));
|
||||
raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
|
||||
raw.set(t::PHOTOMETRIC, Value::Short(vec![photometric::CFA]));
|
||||
raw.set(t::COMPRESSION, Value::Short(vec![32767]));
|
||||
raw.set(TONE_CURVE, Value::Short(vec![8000, 10400, 12900, 14100]));
|
||||
raw.set(t::DNG_PRIVATE_DATA, Value::Byte(vec![0; 4]));
|
||||
if let Some(levels) = plain {
|
||||
raw.set(WB_RGGB, Value::Short(levels.to_vec()));
|
||||
}
|
||||
if tag2010.is_some() {
|
||||
raw.set_child(t::EXIF_IFD, IfdBuilder::new().with(t::MAKER_NOTE, Value::Undefined(note(0))));
|
||||
}
|
||||
raw.set_image(ImageData::Strips { rows_per_strip: 4, strips: vec![vec![0u8; 32 * 4]] });
|
||||
let mut file = TiffWriter::default().write(&[raw]).unwrap();
|
||||
let parsed = Tiff::parse(&file).unwrap();
|
||||
if let Some(e) = parsed.exif().and_then(|e| e.get(t::MAKER_NOTE)) {
|
||||
let at = e.offset as usize;
|
||||
file[at..at + e.count()].copy_from_slice(¬e(at));
|
||||
}
|
||||
if let Some(levels) = sr2 {
|
||||
// SR2Private IFD (three entries), then the encrypted SR2SubIFD
|
||||
let private = file.len();
|
||||
let start = private + 2 + 3 * 12 + 4;
|
||||
let block = super::super::sr2::tests::block(start, &[(BLACK_LEVEL, 3, &[800; 4]), (WB_RGGB, 8, &levels)], Little);
|
||||
file.extend([3, 0]);
|
||||
for (tag, kind, value) in
|
||||
[(0x7200u16, 4u16, (start as u32).to_le_bytes()), (0x7201, 4, (block.len() as u32).to_le_bytes()), (0x7221, 7, key)]
|
||||
{
|
||||
file.extend(tag.to_le_bytes());
|
||||
file.extend(kind.to_le_bytes());
|
||||
file.extend(if kind == 7 { 4u32 } else { 1 }.to_le_bytes());
|
||||
file.extend(value);
|
||||
}
|
||||
file.extend([0; 4]);
|
||||
file.extend(block);
|
||||
let at = parsed.ifds[0].get(t::DNG_PRIVATE_DATA).unwrap().offset as usize;
|
||||
file[at..at + 4].copy_from_slice(&(private as u32).to_le_bytes());
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_balance_prefers_the_applied_levels() {
|
||||
const KEY: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
|
||||
let (plain, sr2, tag2010) = ([2048, 1024, 1024, 1536], [2932, 1024, 1024, 1576], [484, 256, 788]);
|
||||
let wb = |file: Vec<u8>| {
|
||||
let header = decode(&file, Mode::Header).unwrap();
|
||||
let full = decode(&file, Mode::Full).unwrap();
|
||||
assert_eq!(header.wb_multipliers, full.wb_multipliers);
|
||||
assert_eq!(header.black, full.black);
|
||||
full.wb_multipliers
|
||||
};
|
||||
// the same decrypted directory gives the black level, with the known key only
|
||||
let black = |file: Vec<u8>| decode(&file, Mode::Full).unwrap().black.mean();
|
||||
assert_eq!(black(arw_with_wb(None, Some(sr2), None, KEY)), 800.0);
|
||||
assert_eq!(black(arw_with_wb(None, Some(sr2), None, [1, 2, 3, 4])), 512.0);
|
||||
assert_eq!(black(arw_with_wb(None, None, None, KEY)), 512.0);
|
||||
let sr2_gains = Some([2932.0 / 1024.0, 1.0, 1576.0 / 1024.0]);
|
||||
// the encrypted SR2SubIFD's WB_RGGBLevels over Tag2010 (issue #535: ILCE-3500 at 5600 K) and when alone
|
||||
assert_eq!(wb(arw_with_wb(None, Some(sr2), Some(tag2010), KEY)), sr2_gains);
|
||||
assert_eq!(wb(arw_with_wb(None, Some(sr2), None, KEY)), sr2_gains);
|
||||
// the plain levels over both
|
||||
assert_eq!(wb(arw_with_wb(Some(plain), Some(sr2), Some(tag2010), KEY)), Some([2.0, 1.0, 1.5]));
|
||||
// Tag2010 when the SR2SubIFD has another key, implausible levels or none at all
|
||||
let fallback = Some([484.0 / 256.0, 1.0, 788.0 / 256.0]);
|
||||
assert_eq!(wb(arw_with_wb(None, Some(sr2), Some(tag2010), [1, 2, 3, 4])), fallback);
|
||||
assert_eq!(wb(arw_with_wb(None, Some([9000, 1024, 1024, 1576]), Some(tag2010), KEY)), fallback);
|
||||
assert_eq!(wb(arw_with_wb(None, None, Some(tag2010), KEY)), fallback);
|
||||
assert_eq!(wb(arw_with_wb(None, None, None, KEY)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broken_sr2_private_data_is_ignored() {
|
||||
const KEY: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
|
||||
let file = arw_with_wb(None, Some([2932, 1024, 1024, 1576]), None, KEY);
|
||||
let at = Tiff::parse(&file).unwrap().ifds[0].get(t::DNG_PRIVATE_DATA).unwrap().offset as usize;
|
||||
let private = u32::from_le_bytes(file[at..at + 4].try_into().unwrap()) as usize;
|
||||
let patched = |pos: usize, bytes: &[u8]| {
|
||||
let mut f = file.clone();
|
||||
f[pos..pos + bytes.len()].copy_from_slice(bytes);
|
||||
decode(&f, Mode::Full).map(|img| img.wb_multipliers)
|
||||
};
|
||||
// DNGPrivateData pointing nowhere or into the image; SR2SubIFD offset or length past the end, or enormous
|
||||
for (pos, bytes) in [
|
||||
(at, u32::MAX.to_le_bytes()),
|
||||
(at, 8u32.to_le_bytes()),
|
||||
(private + 2 + 8, u32::MAX.to_le_bytes()),
|
||||
(private + 2 + 12 + 8, u32::MAX.to_le_bytes()),
|
||||
(private + 2 + 12 + 8, (file.len() as u32).to_le_bytes()),
|
||||
(private + 2 + 12 + 8, (1u32 << 21).to_le_bytes()),
|
||||
] {
|
||||
assert_eq!(patched(pos, &bytes).unwrap(), None, "{pos} {bytes:?}");
|
||||
}
|
||||
// the file cut anywhere inside the SR2 data still decodes (the image comes first), without white balance
|
||||
for len in [private, private + 20, file.len() - 30, file.len() - 1] {
|
||||
let img = decode(&file[..len], Mode::Full).unwrap();
|
||||
assert_eq!(img.wb_multipliers, None, "{len}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sr2_black_level_decodes_and_rejects_garbage() {
|
||||
use lightcraft_tiff::ByteOrder::Little;
|
||||
let cipher =
|
||||
|levels: [u16; 4]| -> Vec<u8> { levels.iter().flat_map(|v| v.to_le_bytes()).zip(SR2_BLACK_KEYSTREAM).map(|(p, k)| p ^ k).collect() };
|
||||
assert_eq!(sr2_black_levels(&cipher([800; 4]), Little), Some(800.0));
|
||||
assert_eq!(sr2_black_levels(&cipher([512, 512, 514, 512]), Little), Some(512.5));
|
||||
assert_eq!(sr2_black_levels(&cipher([65320, 64928, 65260, 488]), Little), None); // ILCE-7M4's layout
|
||||
assert_eq!(sr2_black_levels(&cipher([0; 4]), Little), None);
|
||||
assert_eq!(sr2_black_levels(&cipher([800; 4])[..5], Little), None);
|
||||
let plain = |levels: [u16; 4]| -> Vec<u8> { levels.iter().flat_map(|v| v.to_le_bytes()).collect() };
|
||||
assert_eq!(sr2_black_levels(&plain([800; 4]), Little), Some(800.0));
|
||||
assert_eq!(sr2_black_levels(&plain([512, 512, 514, 512]), Little), Some(512.5));
|
||||
assert_eq!(sr2_black_levels(&plain([65320, 64928, 65260, 488]), Little), None);
|
||||
assert_eq!(sr2_black_levels(&plain([0; 4]), Little), None);
|
||||
assert_eq!(sr2_black_levels(&plain([800; 4])[..5], Little), None);
|
||||
assert_eq!(sr2_black_levels(&[], Little), None);
|
||||
}
|
||||
|
||||
/// An encrypted `SR2SubIFD` of `len` bytes at file offset `start` whose first entry is a black level stored at
|
||||
/// `value_pos` (relative), holding `levels`; `other` puts four more levels at position 2510 (where the old
|
||||
/// fixed read looked).
|
||||
fn sr2_block(start: usize, len: usize, tag: u16, value_pos: usize, levels: [u16; 4], other: Option<[u16; 4]>) -> Vec<u8> {
|
||||
let mut plain = vec![0u8; len];
|
||||
plain[0..2].copy_from_slice(&180u16.to_le_bytes());
|
||||
plain[2..4].copy_from_slice(&tag.to_le_bytes());
|
||||
plain[4..6].copy_from_slice(&3u16.to_le_bytes());
|
||||
plain[6..10].copy_from_slice(&4u32.to_le_bytes());
|
||||
plain[10..14].copy_from_slice(&((start + value_pos) as u32).to_le_bytes());
|
||||
for (i, v) in levels.iter().enumerate() {
|
||||
plain[value_pos + 2 * i..value_pos + 2 * i + 2].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
if let Some(o) = other {
|
||||
for (i, v) in o.iter().enumerate() {
|
||||
plain[2510 + 2 * i..2510 + 2 * i + 2].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
super::super::sr2::tests::encrypt(&mut plain);
|
||||
plain
|
||||
}
|
||||
|
||||
/// [`sr2_black`] on the encrypted `SR2SubIFD` bytes `block`, which start at file offset `start`.
|
||||
fn sr2_black_in(block: &[u8], start: usize, order: lightcraft_tiff::ByteOrder) -> Option<f32> {
|
||||
sr2_black(&super::super::sr2::SubIfd::decrypt(block, start, order))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sr2_black_level_follows_the_first_entry_to_its_layout() {
|
||||
use lightcraft_tiff::ByteOrder::Little;
|
||||
let start = 37584;
|
||||
// the value position of every layout seen, e.g. the DSLR-A700's (62112 bytes, value at 1638) and the
|
||||
// DSLR-A450/A500/A550's (27152 bytes, value at 2166); the first four are those of bodies without a plain 0x7310
|
||||
for pos in [1638, 2166, 2418, 2510, 2030, 2102, 2186, 2558, 2786] {
|
||||
for level in [512, 800] {
|
||||
let block = sr2_block(start, 27152, BLACK_LEVEL, pos, [level; 4], None);
|
||||
assert_eq!(sr2_black_in(&block, start, Little), Some(f32::from(level)), "value at {pos}");
|
||||
}
|
||||
}
|
||||
// the A700's layout holds four other equal values at 2510 (975 each); the old fixed read took those
|
||||
let a700 = sr2_block(start, 62112, BLACK_LEVEL, 1638, [512; 4], Some([975; 4]));
|
||||
assert_eq!(sr2_black_in(&a700, start, Little), Some(512.0));
|
||||
// not a black-level entry, another type, a value outside the block, before it
|
||||
assert_eq!(sr2_black_in(&sr2_block(start, 27152, 0x7311, 2166, [512; 4], None), start, Little), None);
|
||||
let mut long = sr2_block(start, 27152, BLACK_LEVEL, 2166, [512; 4], None);
|
||||
long[4] ^= 3 ^ 4; // SHORT -> LONG (type field of the first entry)
|
||||
assert_eq!(sr2_black_in(&long, start, Little), None);
|
||||
assert_eq!(sr2_black_in(&sr2_block(start, 27152, BLACK_LEVEL, 2166, [512; 4], None)[..2170], start, Little), None);
|
||||
let block = sr2_block(start, 27152, BLACK_LEVEL, 2166, [512; 4], None);
|
||||
assert_eq!(sr2_black_in(&block, start + 4000, Little), None, "value offset before the block");
|
||||
assert_eq!(sr2_black_in(&block[..10], start, Little), None, "truncated directory");
|
||||
assert_eq!(sr2_black_in(&[], start, Little), None);
|
||||
// implausible levels at a known position
|
||||
assert_eq!(sr2_black_in(&sr2_block(start, 27152, BLACK_LEVEL, 2166, [4; 4], None), start, Little), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quad_tiles_place_cfa_cells() {
|
||||
// 14×12 mosaic in 8×8 tiles (the right and bottom tiles overhang), each tile a 4×4 frame of 2×2 cells,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub mod pef;
|
|||
pub mod raf;
|
||||
mod rafc;
|
||||
pub mod rw2;
|
||||
mod sr2;
|
||||
pub mod srw;
|
||||
|
||||
use crate::{BlackLevel, Cfa, Rect};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,301 @@
|
|||
//! Sony's encrypted `SR2SubIFD`.
|
||||
//!
|
||||
//! Sony ARWs from the DSLR-A200 on carry a private directory, the `SR2SubIFD`, reached from IFD0's
|
||||
//! `DNGPrivateData` through the `SR2Private` IFD (`SR2SubIFDOffset` `0x7200`, `SR2SubIFDLength` `0x7201`,
|
||||
//! `SR2SubIFDKey` `0x7221`; tag names from ExifTool's Sony tag documentation). The block is a TIFF directory in the
|
||||
//! file's byte order whose value offsets are file offsets into the block itself, and the whole block is encrypted.
|
||||
//! Among its entries are the black level (`0x7310`) and the white balance the camera applied, `WB_RGGBLevels`
|
||||
//! (`0x7313`: R, G, G, B); bodies from about 2017 on also write both in plain form in the raw IFD.
|
||||
//!
|
||||
//! This module is the only place the block is decrypted: the black level and the white balance are both read through
|
||||
//! [`SubIfd`] (issue #535; it replaces positional keystream tables recovered earlier for the black level alone).
|
||||
//!
|
||||
//! **Clean-room provenance.** The decryption was derived only by black-box known-plaintext analysis of CC0 files. No
|
||||
//! raw-decoder source (dcraw, LibRaw, rawspeed, rawloader/rawler, darktable, RawTherapee, ExifTool's Perl code, …)
|
||||
//! and no description of Sony's cipher or keystream generator was read or consulted. The steps:
|
||||
//!
|
||||
//! 1. **Files.** The 126 ARWs with an `SR2SubIFD` among the CC0 Sony samples of raw.pixls.us collected for issue
|
||||
//! #535: 97 bodies from the DSLR-A200 to the ILCE-9M3 and ZV-E1, every Sony body on the site that has one, with
|
||||
//! every coding of the bodies named in open issues.
|
||||
//! 2. **Plaintext.** `exiftool -v5 FILE`, run as a black-box tool, prints the decrypted `SR2SubIFD` as a hex dump
|
||||
//! with file offsets. XOR-ing each dumped byte with the byte at the same offset in the file gives the keystream
|
||||
//! byte at that position of the block.
|
||||
//! 3. **Comparison.** All 126 files store the key `11 22 33 44`, and their keystreams agree byte for byte at every
|
||||
//! position they share: 15 layouts from 25150 to 62112 bytes, 5.2 MB of keystream in all. Bytes after the last
|
||||
//! whole 4-byte word (two, in every block whose length is not a multiple of four) are stored in the clear.
|
||||
//! 4. **Structure.** Read as 32-bit words, the longest keystream (the DSLR-A700 sample `DSC07249.ARW`, SHA-256
|
||||
//! `3159e288…`, 15528 words) was tested for every recurrence `k[n] = k[n − a] ⊕ k[n − b]` with
|
||||
//! `1 ≤ a < b < 300`: an exhaustive search, not a guided one. Exactly two pairs hold on every word: `(63, 127)`
|
||||
//! and `(126, 254)`, the second a consequence of the first. With `(63, 127)`, any 127 consecutive words fix all
|
||||
//! later ones, so [`SEED`] is simply the first 127 words of that file's keystream.
|
||||
//! 5. **Checks.** Generated from [`SEED`], the keystream equals the one recovered from each of the 126 files at every
|
||||
//! position. It also reproduces bytes recovered independently from plain tag values before the generator existed
|
||||
//! (8 bytes at position 2510, issue #148, from the plain black levels of the DSC-RX100M6 and ILCE-7M3) and the
|
||||
//! positional tables the black level was read with until issue #535; the unit tests pin all of them.
|
||||
//!
|
||||
//! Only that key is supported. The one other key seen, in a DSC-R1 SR2 file, gives a different keystream that obeys
|
||||
//! the same recurrence from its own first 127 words, but one sample per key cannot show how a seed follows from a
|
||||
//! key, so files with another key get `None`.
|
||||
|
||||
use lightcraft_tiff::{ByteOrder, Ifd, ParseOptions, tags as t};
|
||||
|
||||
/// `SR2Private` tags.
|
||||
const SUBIFD_OFFSET: u16 = 0x7200;
|
||||
const SUBIFD_LENGTH: u16 = 0x7201;
|
||||
const SUBIFD_KEY: u16 = 0x7221;
|
||||
/// The key stored by every ARW examined; [`SEED`] is its keystream.
|
||||
const KEY: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
|
||||
/// Upper bound on the block length we decrypt. The largest seen is 62112 bytes (DSLR-A700).
|
||||
const MAX_LEN: usize = 1 << 20;
|
||||
/// TIFF field types of the levels: SHORT and SSHORT.
|
||||
const SHORT: u16 = 3;
|
||||
const SSHORT: u16 = 8;
|
||||
|
||||
/// The first 127 keystream words for [`KEY`] (each XOR-ed with four block bytes, most significant byte first).
|
||||
/// Later words follow `k[n] = k[n − 63] ⊕ k[n − 127]`; see the module docs.
|
||||
const SEED: [u32; 127] = [
|
||||
0x54c2c4d6, 0x49c478bc, 0x34ae2f8e, 0x25d80504, 0xc0d9d6b0, 0xd838fb71, 0xe8eff27d, 0xfbc1fcea, 0x506c499a, 0x47f20f37, 0x710777cf, 0x7867e7ba,
|
||||
0x42d67caa, 0x7f2bd11a, 0x67a216ca, 0x0e986d40, 0x4ae8d4c0, 0xe36778b4, 0x5a958415, 0xdbfe2be8, 0x20faa1aa, 0x7132a6b8, 0xf4de4b7f, 0x55991aa1,
|
||||
0xa849d5aa, 0x49577832, 0xb92f3daa, 0x399cc526, 0x22cdd000, 0xe1977a29, 0x37c5db55, 0xb0177e1e, 0x2a1016aa, 0xa300086e, 0x3bab9bfe, 0x262eece0,
|
||||
0x23771aa9, 0x0a5dc91c, 0x31b902ae, 0x58e64bf8, 0x259c300e, 0xa57705c8, 0x284a6541, 0xfb229c60, 0x1bacaa9e, 0xbcab3350, 0x67cd9fbe, 0x8f135e60,
|
||||
0xf8c26a40, 0x6770da61, 0x3e1febfd, 0xd0c70803, 0x8dbb037b, 0x6f6fa4c5, 0x6749d10d, 0x7f51598d, 0xd5e5a4ec, 0x207dfa91, 0x6558ebc2, 0xbe594639,
|
||||
0x617a9e5d, 0x3c497950, 0x0844eb3f, 0x04207ed2, 0xd27ceac4, 0x70d20f05, 0xb47003f6, 0xe9e4e3ae, 0xcc19d265, 0x326dd956, 0xf0d3a327, 0xb71275f0,
|
||||
0x7994e285, 0x0aff594d, 0x128e8345, 0x7bda597a, 0xd634c380, 0xe24a006f, 0x8974818b, 0x3320b22a, 0xbe808417, 0xa2d5648a, 0x6fe80b39, 0x23ebad41,
|
||||
0xa2d11e5d, 0x027d9397, 0x9a722ac8, 0x432c7dac, 0x7146692a, 0x82a3dc77, 0xd66887c5, 0x831f43b7, 0x4e5dddde, 0x03793f81, 0x306ab437, 0x00ccf86c,
|
||||
0xfc6ed3d2, 0x076b8fdb, 0x9808cfca, 0x0f4eef6e, 0xc8cc3830, 0x104ac16a, 0xa189eff4, 0x3e085c08, 0xd28baf88, 0x5c853ac4, 0xe60480f8, 0xc51acd98,
|
||||
0x691e5ee1, 0x333feeb9, 0x1e35bc33, 0xec4a4642, 0xee57c5a5, 0xbeeb51f7, 0xe0c4f32c, 0xa5422f6a, 0x1d266d12, 0x3752fd3b, 0xfbc53c7d, 0x2421a4a3,
|
||||
0xcdc6a2de, 0x26e6b330, 0x6c073d46, 0x058e2f27, 0x43833f30, 0x46d1382e, 0x5f0804ec,
|
||||
];
|
||||
|
||||
/// XOR `block` with the keystream from its first byte on (whole 4-byte words; a shorter tail stays as it is).
|
||||
/// Encrypts and decrypts alike.
|
||||
fn apply_keystream(block: &mut [u8]) {
|
||||
let mut ring = SEED;
|
||||
for (n, word) in block.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
// `ring[i]` holds k[n − 127] until it is replaced by k[n]; k[n − 63] sits at (n − 63) mod 127
|
||||
let i = n % SEED.len();
|
||||
if n >= SEED.len() {
|
||||
ring[i] ^= ring[(i + 64) % SEED.len()];
|
||||
}
|
||||
for (b, k) in word.iter_mut().zip(ring[i].to_be_bytes()) {
|
||||
*b ^= k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A decrypted `SR2SubIFD`.
|
||||
pub(crate) struct SubIfd {
|
||||
/// File offset of the block: the directory's value offsets are file offsets.
|
||||
start: usize,
|
||||
plain: Vec<u8>,
|
||||
order: ByteOrder,
|
||||
}
|
||||
|
||||
impl SubIfd {
|
||||
/// Find and decrypt the file's `SR2SubIFD`; `None` when it has none, it lies outside the file, is larger than
|
||||
/// [`MAX_LEN`] or uses another key than [`KEY`].
|
||||
pub(crate) fn read(bytes: &[u8], ifd0: &Ifd, order: ByteOrder) -> Option<Self> {
|
||||
let at = order.read_u32(ifd0.bytes(t::DNG_PRIVATE_DATA)?, 0)?;
|
||||
let opts = ParseOptions { max_ifds: 1, max_depth: 0, follow_children: false, ..Default::default() };
|
||||
let (private, _) = lightcraft_tiff::parse_ifd_at(bytes, u64::from(at), order, 0, false, &opts).ok()?;
|
||||
if private.bytes(SUBIFD_KEY)? != KEY {
|
||||
return None;
|
||||
}
|
||||
let start = usize::try_from(private.u64(SUBIFD_OFFSET)?).ok()?;
|
||||
let len = usize::try_from(private.u64(SUBIFD_LENGTH)?).ok()?;
|
||||
if len > MAX_LEN {
|
||||
return None;
|
||||
}
|
||||
Some(Self::decrypt(bytes.get(start..start.checked_add(len)?)?, start, order))
|
||||
}
|
||||
|
||||
/// Decrypt `block`, the encrypted directory found at file offset `start`.
|
||||
pub(crate) fn decrypt(block: &[u8], start: usize, order: ByteOrder) -> Self {
|
||||
let mut plain = block.to_vec();
|
||||
apply_keystream(&mut plain);
|
||||
Self { start, plain, order }
|
||||
}
|
||||
|
||||
/// The byte order of the directory's values (the file's).
|
||||
pub(crate) fn order(&self) -> ByteOrder {
|
||||
self.order
|
||||
}
|
||||
|
||||
/// The field type and value bytes of a SHORT or SSHORT entry; `None` when the tag is absent, has another type
|
||||
/// or its values lie outside the block.
|
||||
pub(crate) fn short_bytes(&self, tag: u16) -> Option<(u16, &[u8])> {
|
||||
let o = self.order;
|
||||
let entries = usize::from(o.read_u16(&self.plain, 0)?);
|
||||
let entry = (0..entries).map_while(|i| self.plain.get(2 + 12 * i..14 + 12 * i)).find(|e| o.read_u16(e, 0) == Some(tag))?;
|
||||
let kind = o.read_u16(entry, 2)?;
|
||||
if kind != SHORT && kind != SSHORT {
|
||||
return None;
|
||||
}
|
||||
let size = usize::try_from(o.read_u32(entry, 4)?).ok()?.checked_mul(2)?;
|
||||
let values = if size <= 4 {
|
||||
entry.get(8..8 + size)?
|
||||
} else {
|
||||
let at = usize::try_from(o.read_u32(entry, 8)?).ok()?.checked_sub(self.start)?;
|
||||
self.plain.get(at..at.checked_add(size)?)?
|
||||
};
|
||||
Some((kind, values))
|
||||
}
|
||||
|
||||
/// The values of a SHORT or SSHORT entry (see [`SubIfd::short_bytes`]).
|
||||
pub(crate) fn shorts(&self, tag: u16) -> Option<Vec<f64>> {
|
||||
let (kind, values) = self.short_bytes(tag)?;
|
||||
let value = |v: u16| if kind == SSHORT { f64::from(v as i16) } else { f64::from(v) };
|
||||
Some(values.as_chunks::<2>().0.iter().map(|&c| value(self.order.u16(c))).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use lightcraft_tiff::ByteOrder::{Big, Little};
|
||||
|
||||
/// An `SR2SubIFD` block for file offset `start`, encrypted: SHORT/SSHORT entries `(tag, type, values)`, values
|
||||
/// of more than two shorts stored after the table at file offsets, as Sony does.
|
||||
pub(crate) fn block(start: usize, entries: &[(u16, u16, &[u16])], order: ByteOrder) -> Vec<u8> {
|
||||
let mut plain = Vec::new();
|
||||
order.put_u16(&mut plain, entries.len() as u16);
|
||||
let mut data = Vec::new();
|
||||
let data_at = start + 2 + 12 * entries.len() + 4;
|
||||
for &(tag, kind, values) in entries {
|
||||
order.put_u16(&mut plain, tag);
|
||||
order.put_u16(&mut plain, kind);
|
||||
order.put_u32(&mut plain, values.len() as u32);
|
||||
let mut field = Vec::new();
|
||||
values.iter().for_each(|&v| order.put_u16(&mut field, v));
|
||||
if field.len() <= 4 {
|
||||
field.resize(4, 0);
|
||||
plain.extend(field);
|
||||
} else {
|
||||
order.put_u32(&mut plain, (data_at + data.len()) as u32);
|
||||
data.extend(field);
|
||||
}
|
||||
}
|
||||
plain.extend([0; 4]);
|
||||
plain.extend(data);
|
||||
plain.extend([0xa5; 2]); // a length that is not a multiple of four, like most real blocks
|
||||
apply_keystream(&mut plain);
|
||||
plain
|
||||
}
|
||||
|
||||
/// Encrypt (or decrypt) `block` in place, as a file stores an `SR2SubIFD` at its first byte.
|
||||
pub(crate) fn encrypt(block: &mut [u8]) {
|
||||
apply_keystream(block);
|
||||
}
|
||||
|
||||
/// Keystream bytes recovered earlier, each from plain tag values or ExifTool's decrypted dump at one position,
|
||||
/// before the generator existed: the black level's 8 bytes at 2510 (#148), and the positional tables the black
|
||||
/// level was read with until #535 (#542: the directory's count and first entry, and the black-level value in the
|
||||
/// four layouts of bodies without a plain `0x7310`). The generator must reproduce every one of them, so the black
|
||||
/// levels it decrypts are those the tables gave.
|
||||
#[test]
|
||||
fn keystream_matches_bytes_recovered_at_single_positions() {
|
||||
let mut ks = vec![0u8; 62114];
|
||||
apply_keystream(&mut ks);
|
||||
let recovered: [(usize, &[u8]); 5] = [
|
||||
(0, &[0x54, 0xc2, 0xc4, 0xd6, 0x49, 0xc4, 0x78, 0xbc, 0x34, 0xae, 0x2f, 0x8e, 0x25, 0xd8]),
|
||||
(1638, &[0x18, 0x95, 0x65, 0xf0, 0x18, 0x8f, 0x61, 0xc6]),
|
||||
(2166, &[0xc6, 0x17, 0x02, 0xcb, 0x89, 0x21, 0xbc, 0x43]),
|
||||
(2418, &[0xd1, 0xef, 0xda, 0xfe, 0x50, 0xf3, 0xd0, 0xbe]),
|
||||
(2510, &[0x43, 0xcb, 0x86, 0xb6, 0x11, 0xd2, 0x1a, 0x73]),
|
||||
];
|
||||
for (at, bytes) in recovered {
|
||||
assert_eq!(&ks[at..at + bytes.len()], bytes, "keystream at {at}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Known answers recorded from CC0 files without the generator, so that a wrong seed word or recurrence fails
|
||||
/// here even without the corpus: the SHA-256 of the whole keystream recovered from the DSLR-A700 sample
|
||||
/// (`DSC07249.ARW`: the file's bytes XOR ExifTool's decrypted dump; 62112 bytes, the longest block seen, so it
|
||||
/// covers every black-level and white-balance position of every layout), and the encrypted `WB_RGGBLevels` of
|
||||
/// one file per layout of bodies without a plain `0x7313`, with the levels ExifTool decrypts from them.
|
||||
#[test]
|
||||
fn keystream_matches_recorded_known_answers() {
|
||||
use sha2::{Digest, Sha256};
|
||||
let mut ks = vec![0u8; 62112];
|
||||
apply_keystream(&mut ks);
|
||||
let digest: String = Sha256::digest(&ks).iter().map(|b| format!("{b:02x}")).collect();
|
||||
assert_eq!(digest, "d55e96d7bb69cb823d9dfa038dd9f3cfb6b080be4df8c8aa2bc56f71a4a3f183");
|
||||
// (file, position in the block, bytes stored there, WB_RGGBLevels)
|
||||
let cases: [(&str, usize, [u8; 8], [i16; 4]); 4] = [
|
||||
("DSLR-A700 DSC07249", 1654, [0xc9, 0x2a, 0xb6, 0xa4, 0x1f, 0xc1, 0x72, 0x30], [2128, 1024, 1024, 1564]),
|
||||
("DSLR-A500 DSC02421", 2182, [0xb9, 0x6a, 0xed, 0xd3, 0x0d, 0xa8, 0xbb, 0xda], [2212, 1024, 1024, 1416]),
|
||||
("SLT-A33 DSC01867", 2434, [0x3c, 0x43, 0x05, 0x3f, 0x0d, 0xe2, 0x05, 0xcc], [2344, 1024, 1024, 1508]),
|
||||
("ILCE-3500 DSC06923", 2526, [0x37, 0x2e, 0x71, 0x5b, 0x88, 0xb7, 0x21, 0x73], [2932, 1024, 1024, 1576]),
|
||||
];
|
||||
for (file, at, stored, levels) in cases {
|
||||
let mut block = vec![0u8; (at + 8).next_multiple_of(4)];
|
||||
block[at..at + 8].copy_from_slice(&stored);
|
||||
apply_keystream(&mut block);
|
||||
let got: Vec<i16> = block[at..at + 8].chunks(2).map(|c| i16::from_le_bytes([c[0], c[1]])).collect();
|
||||
assert_eq!(got, levels, "{file}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keystream_follows_its_recurrence() {
|
||||
let mut zeros = vec![0u8; 62114];
|
||||
apply_keystream(&mut zeros);
|
||||
// the recurrence, past the seed, and the clear tail
|
||||
let word = |n: usize| u32::from_be_bytes(zeros[4 * n..4 * n + 4].try_into().unwrap());
|
||||
assert_eq!(word(127), SEED[64] ^ SEED[0]);
|
||||
assert!((200..15528).all(|n| word(n) == word(n - 63) ^ word(n - 127)));
|
||||
assert_eq!(zeros[62112..], [0, 0]);
|
||||
// applying it twice restores the input
|
||||
let mut again = zeros.clone();
|
||||
apply_keystream(&mut again);
|
||||
assert!(again[..62112].iter().all(|&b| b == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_entries_inline_and_at_file_offsets() {
|
||||
for order in [Little, Big] {
|
||||
let start = 75964;
|
||||
let enc = block(start, &[(0x7310, SHORT, &[512; 4]), (0x7311, SHORT, &[256]), (0x7313, SSHORT, &[2212, 1024, 1024, 1416])], order);
|
||||
let sub = SubIfd::decrypt(&enc, start, order);
|
||||
assert_eq!(sub.shorts(0x7313), Some(vec![2212.0, 1024.0, 1024.0, 1416.0]), "{order:?}");
|
||||
assert_eq!(sub.shorts(0x7310), Some(vec![512.0; 4]));
|
||||
assert_eq!(sub.shorts(0x7311), Some(vec![256.0]));
|
||||
assert_eq!(sub.shorts(0x7312), None);
|
||||
// SSHORT is signed
|
||||
let enc = block(start, &[(0x7313, SSHORT, &[0xffff, 1024])], order);
|
||||
assert_eq!(SubIfd::decrypt(&enc, start, order).shorts(0x7313), Some(vec![-1.0, 1024.0]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_directories_give_none() {
|
||||
let start = 4096;
|
||||
let good = block(start, &[(0x7313, SSHORT, &[2212, 1024, 1024, 1416])], Little);
|
||||
let read = |b: &[u8], start: usize| SubIfd::decrypt(b, start, Little).shorts(0x7313);
|
||||
assert!(read(&good, start).is_some());
|
||||
// decrypted at another file offset, the value offset points before or past the block
|
||||
assert_eq!(read(&good, start + 64), None);
|
||||
assert_eq!(read(&good, start - 64), None);
|
||||
// every truncation, down to empty
|
||||
for n in 0..good.len() - 2 {
|
||||
assert_eq!(read(&good[..n], start), None, "{n} bytes");
|
||||
}
|
||||
// garbage: still encrypted; 65535 entries of tag 0xffff; a huge count, another type, a wild offset
|
||||
assert_eq!(SubIfd { start, plain: good.clone(), order: Little }.shorts(0x7313), None);
|
||||
assert_eq!(SubIfd { start, plain: vec![0xff; 1000], order: Little }.shorts(0x7313), None);
|
||||
let mut plain = good.clone();
|
||||
apply_keystream(&mut plain);
|
||||
let patched = |at: usize, bytes: &[u8]| {
|
||||
let mut p = plain.clone();
|
||||
p[at..at + bytes.len()].copy_from_slice(bytes);
|
||||
apply_keystream(&mut p);
|
||||
read(&p, start)
|
||||
};
|
||||
assert_eq!(patched(6, &u32::MAX.to_le_bytes()), None); // count
|
||||
assert_eq!(patched(4, &4u16.to_le_bytes()), None); // LONG
|
||||
assert_eq!(patched(10, &u32::MAX.to_le_bytes()), None); // value offset
|
||||
// an entry count past the end of the block: the scan stops where the block ends, so an entry before that
|
||||
// point is still found
|
||||
assert_eq!(patched(0, &u16::MAX.to_le_bytes()), Some(vec![2212.0, 1024.0, 1024.0, 1416.0]));
|
||||
}
|
||||
}
|
||||
|
|
@ -347,17 +347,20 @@ fn corpus_proraw_sky_matte() {
|
|||
}
|
||||
|
||||
/// Issue #148: Sony ARWs from before ~2017 carry no plain white-balance, black-level or crop tags in the raw IFD.
|
||||
/// White balance comes from the maker note's enciphered `Tag2010`, the black level from the encrypted `SR2SubIFD`
|
||||
/// and the crop from `FullImageSize`; without them the RX100 III opened bright green. Expected values: the black
|
||||
/// levels agree with each sensor's dark-pixel floor, the gains with the neutral sky of the camera JPEG.
|
||||
/// White balance and black level come from the encrypted `SR2SubIFD`, the crop from `FullImageSize`; without them
|
||||
/// the RX100 III opened bright green. Expected values: the black levels agree with each sensor's dark-pixel floor;
|
||||
/// the gains are the `SR2SubIFD`'s `WB_RGGBLevels`, within 4 % of the gains that make the camera JPEG's neutral
|
||||
/// pixels neutral (RX100 III 2.59 / 1.69, RX100 2.21 / 2.01, ILCE-7RM2 2.67 / 1.47). Until issue #535 they came
|
||||
/// from the maker note's `Tag2010` (2.61 / 1.72, 2.23 / 2.00, 2.58 / 1.46), which differs from what the camera
|
||||
/// applied on other files (see `corpus_sony_sr2_white_balance`).
|
||||
#[test]
|
||||
fn corpus_sony_pre2017_colour_metadata() {
|
||||
let dir = corpus_root().join("raw");
|
||||
// (file, black, approximate R and B gains, crop width × height)
|
||||
let cases = [
|
||||
("arw-sony-rx100m3.arw", 800.0, [2.61, 1.72], (5472, 3648)),
|
||||
("arw-sony-rx100.arw", 800.0, [2.23, 2.00], (5472, 3648)),
|
||||
("arw-sony-a7rm2-12bit-uncompressed.arw", 512.0, [2.58, 1.46], (7952, 5304)),
|
||||
("arw-sony-rx100m3.arw", 800.0, [2.55, 1.72], (5472, 3648)),
|
||||
("arw-sony-rx100.arw", 800.0, [2.21, 1.99], (5472, 3648)),
|
||||
("arw-sony-a7rm2-12bit-uncompressed.arw", 512.0, [2.58, 1.45], (7952, 5304)),
|
||||
];
|
||||
let mut seen = 0;
|
||||
for (name, black, [r, b], (cw, ch)) in cases {
|
||||
|
|
@ -442,6 +445,98 @@ fn corpus_sony_a7cr_codings() {
|
|||
eprintln!("ILCE-7CR codings checked on {} files", decoded.len());
|
||||
}
|
||||
|
||||
/// Issue #535: Sony black levels against the data. The SR2SubIFD keeps the black level at a position that depends
|
||||
/// on its layout (the DSLR-A500 and A700 read 365 and 975 from a fixed position, where ExifTool and the data say
|
||||
/// 512), and the downsized lossless M and S codings (linear YCbCr) sit 512 above the recorded level (1024, with
|
||||
/// white moved by the same 512). Each file's darkest 0.1 % per channel must lie just above its black level: a black
|
||||
/// level that is too low leaves a pedestal, one that is too high clips the shadows.
|
||||
#[test]
|
||||
fn corpus_sony_black_levels_sit_at_the_data_floor() {
|
||||
use lightcraft_raw::RawData;
|
||||
let dir = corpus_root().join("raw");
|
||||
// (file, black, white): SR2SubIFD black at three layouts, a plain 0x7310, and the lossless L / M / S codings
|
||||
let cases = [
|
||||
("arw-sony-a500.arw", 512.0, None),
|
||||
("arw-sony-a700.arw", 512.0, None),
|
||||
("arw-sony-rx100m3.arw", 800.0, None),
|
||||
("arw-sony-a7m4-lossless-l.arw", 512.0, Some(16383.0)),
|
||||
("arw-sony-a7m4-lossless-m.arw", 1024.0, Some(16895.0)),
|
||||
("arw-sony-a7m4-lossless-s.arw", 1024.0, Some(16895.0)),
|
||||
];
|
||||
let mut seen = 0;
|
||||
for (name, black, white) in cases {
|
||||
let bytes = match std::fs::read(dir.join(name)) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
eprintln!("skip: {name} absent");
|
||||
continue;
|
||||
}
|
||||
Err(e) => panic!("{name}: {e}"),
|
||||
};
|
||||
let img = decode(&bytes).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(img.black.mean(), black, "{name}: black level");
|
||||
if let Some(white) = white {
|
||||
assert_eq!(img.white_at(0), white, "{name}: white level");
|
||||
}
|
||||
let RawData::U16(ref data) = img.data else { panic!("{name}: integer samples expected") };
|
||||
// per CFA position (2×2) or per colour sample, inside the default crop, about a million samples each
|
||||
let channels = if img.cpp == 3 { 3 } else { 4 };
|
||||
let (a, c) = (img.active_area, img.crop);
|
||||
let step = (c.width * c.height / 1_000_000).max(1);
|
||||
let mut samples = vec![Vec::new(); channels];
|
||||
for (k, (x, y)) in (c.y..c.y + c.height).flat_map(|y| (c.x..c.x + c.width).map(move |x| (x + a.x, y + a.y))).enumerate() {
|
||||
if (k / 2) % step != 0 {
|
||||
continue;
|
||||
}
|
||||
if img.cpp == 3 {
|
||||
for (s, v) in samples.iter_mut().enumerate() {
|
||||
v.push(data[(y * img.width + x) * 3 + s]);
|
||||
}
|
||||
} else {
|
||||
samples[(y % 2) * 2 + x % 2].push(data[y * img.width + x]);
|
||||
}
|
||||
}
|
||||
for (i, mut v) in samples.into_iter().enumerate() {
|
||||
v.sort_unstable();
|
||||
let floor = f32::from(v[v.len() / 1000]);
|
||||
assert!((black - 64.0..=black + 128.0).contains(&floor), "{name}: channel {i}: darkest 0.1 % at {floor}, black level {black}");
|
||||
}
|
||||
seen += 1;
|
||||
}
|
||||
eprintln!("Sony black levels checked against the data floor on {seen} files");
|
||||
}
|
||||
|
||||
/// Issue #535: the white balance a Sony ARW records as applied, `WB_RGGBLevels`, read from the encrypted
|
||||
/// `SR2SubIFD` when the raw IFD has no plain copy. Before, the first three opened with no white balance (green) and
|
||||
/// the last two with the maker note's `Tag2010` gains, which there follow the scene rather than the preset used.
|
||||
/// Expected levels: ExifTool's decrypted `WB_RGGBLevels` (green 1024). In brackets, the gains that make the
|
||||
/// camera JPEG's neutral pixels neutral.
|
||||
#[test]
|
||||
fn corpus_sony_sr2_white_balance() {
|
||||
let dir = corpus_root().join("raw");
|
||||
// (file, R and B levels)
|
||||
let cases = [
|
||||
("arw-sony-a500.arw", [2212, 1416]), // 27152-byte SR2SubIFD layout (2.04 / 1.35)
|
||||
("arw-sony-a33.arw", [2344, 1508]), // 29000-byte layout (2.29 / 1.48)
|
||||
("arw-sony-a700.arw", [2128, 1564]), // 62112-byte layout (too few neutral pixels)
|
||||
("arw-sony-a3500-5600k.arw", [2932, 1576]), // 5600 K (2.76 / 1.53; Tag2010 1.89 / 3.08)
|
||||
("arw-sony-a7s-shade.arw", [2932, 1372]), // Shade (2.90 / 1.32; Tag2010 2.63 / 1.52)
|
||||
];
|
||||
let mut seen = 0;
|
||||
for (name, [r, b]) in cases {
|
||||
let Ok(bytes) = std::fs::read(dir.join(name)) else {
|
||||
eprintln!("skip: {name} absent");
|
||||
continue;
|
||||
};
|
||||
let img = decode(&bytes).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let wb = img.wb_multipliers.unwrap_or_else(|| panic!("{name}: no as-shot white balance"));
|
||||
let (er, eb) = (r as f32 / 1024.0, b as f32 / 1024.0);
|
||||
assert!((wb[0] - er).abs() < 1e-4 && wb[1] == 1.0 && (wb[2] - eb).abs() < 1e-4, "{name}: white balance {wb:?}, expected [{er}, 1, {eb}]");
|
||||
seen += 1;
|
||||
}
|
||||
eprintln!("Sony SR2SubIFD white balance checked on {seen} files");
|
||||
}
|
||||
|
||||
/// The embedded JPEG as linear RGB, reduced to `gw × gh` cells.
|
||||
fn jpeg_cells(jpeg: &[u8], gw: usize, gh: usize) -> Vec<[f64; 3]> {
|
||||
use zune_core::{bytestream::ZCursor, colorspace::ColorSpace, options::DecoderOptions};
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -363,7 +363,7 @@ const RAW_SAMPLES: &[(&str, &str, &str)] = &[
|
|||
"https://raw.pixls.us/getfile.php/3480/nice/Sony%20-%20ILCE-7RM4%20-%2014bit%2014bit%20compressed%20%283:2%29.ARW",
|
||||
"e6dafe42643f69ab9d1fd00414b7a1f5104df354bb589201600ac934b29b5e4a",
|
||||
),
|
||||
// pre-2017 bodies: white balance only in the enciphered maker note, black level only in the SR2SubIFD (#148)
|
||||
// pre-2017 bodies: white balance and black level only in the encrypted SR2SubIFD (#148)
|
||||
(
|
||||
"arw-sony-rx100m3.arw",
|
||||
"https://raw.pixls.us/data/Sony/DSC-RX100M3/DSC00734.ARW",
|
||||
|
|
@ -379,6 +379,31 @@ const RAW_SAMPLES: &[(&str, &str, &str)] = &[
|
|||
"https://raw.pixls.us/data/Sony/ILCE-7RM2/12-bit-uncompressed.ARW",
|
||||
"71e0888396ef52c7e6a990b4e72ebb4d8413a1fc1c35bada449870539cef6e39",
|
||||
),
|
||||
// older DSLRs whose SR2SubIFD keeps the black level elsewhere than the bodies above (#535): A500 (27152-byte
|
||||
// layout, like the A450/A550), A700 (62112 bytes)
|
||||
(
|
||||
"arw-sony-a500.arw",
|
||||
"https://raw.pixls.us/data/Sony/DSLR-A500/DSC02421.ARW",
|
||||
"1407fb596a391df67c15b90026a1a76702a2e7386dbdff2f0815b903ea49cead",
|
||||
),
|
||||
(
|
||||
"arw-sony-a700.arw",
|
||||
"https://raw.pixls.us/data/Sony/DSLR-A700/DSC07249.ARW",
|
||||
"3159e28892bf0771d01525cd4b6190f9c15bbb19fa2fab6d2515ced0594e8f40",
|
||||
),
|
||||
// SR2SubIFD white balance (#535): the A500 and A700 above, a third layout of a body that had none before
|
||||
// (SLT-A33, 29000 bytes), and two shots whose maker-note gains differ from the preset applied (5600 K, Shade)
|
||||
("arw-sony-a33.arw", "https://raw.pixls.us/data/Sony/SLT-A33/DSC01867.ARW", "1a59856394f10d4fadb40f5ab9c6d1c89f473f4dbcdefd216fbbbbf1ad8d21f9"),
|
||||
(
|
||||
"arw-sony-a3500-5600k.arw",
|
||||
"https://raw.pixls.us/data/Sony/ILCE-3500/DSC06923.ARW",
|
||||
"04c4fe04425c3e6fff7c8af0f811923c35e750300373357bd54342150c5505d8",
|
||||
),
|
||||
(
|
||||
"arw-sony-a7s-shade.arw",
|
||||
"https://raw.pixls.us/data/Sony/ILCE-7S/DSC04125.ARW",
|
||||
"7cc338a0abc8fdad32d61006f1f8f412297b93e040c7efba572eb2bd8f8e8be2",
|
||||
),
|
||||
// CR2 colour-filter layouts differ by model (issue #85): CR2CFAPattern 3 (GBRG) and 1 (RGGB) samples
|
||||
(
|
||||
"cr2-canon-40d.cr2",
|
||||
|
|
|
|||
Loading…
Reference in New Issue