Apply #580's updated head (drops the DSC-R1 SR2 refusal)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-10 01:02:09 -04:00
co-authored by Claude Opus 5.5
parent 2ecb495d18
commit 4a3f9e9b6d
4 changed files with 17 additions and 72 deletions
+4 -49
View File
@@ -64,10 +64,6 @@ pub(crate) fn dynamic_range_optimizer(note: &makernote::MakerNote) -> Option<boo
_ => None,
}
}
/// Maker-note `FileFormat` (ExifTool Sony tag names): `1 0 0 0` is SR2, `2 0 0 0` ARW 1.0, `3 …` and later ARW 2.x+.
/// The DSC-R1's SR2 stores 16-bit samples that are spread uniformly over 2–65340 (median ~33000, three quarters
/// above any white level): encrypted, so it is refused and opens from its preview instead of decoding to noise.
const MN_FILE_FORMAT: u16 = 0xb000;
/// Inverse of Sony's maker-note byte substitution. ExifTool's Sony tag documentation states that the data of
/// tags `0x2010`, `0x9050` and `0x94xx` "is encrypted by a simple substitution cipher" (no decoder source was
@@ -494,13 +490,6 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
let tiff = Tiff::parse(bytes)?;
let ifd0 = &tiff.ifds[0];
let raw = raw_ifd(&tiff).ok_or_else(|| RawError::Unsupported("ARW without a CFA image IFD (old ARW or SR2)".into()))?;
let mn = tiff
.exif()
.and_then(|e| e.get(t::MAKER_NOTE))
.and_then(|e| makernote::parse_makernote(bytes, e.offset, e.count() as u64, tiff.order, &ifd0.string(t::MAKE).unwrap_or_default()));
if mn.as_ref().and_then(|m| m.ifd.u64s(MN_FILE_FORMAT)).as_deref() == Some(&[1, 0, 0, 0]) {
return Err(RawError::Unsupported("Sony SR2 (DSC-R1): the raw data is encrypted, not decoded yet".into()));
}
let info = raw.image()?;
let (w, h) = (info.width as usize, info.height as usize);
if w * h > crate::MAX_SAMPLES {
@@ -579,6 +568,10 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
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))
};
let model = ifd0.string(t::MODEL).unwrap_or_default();
let mn = tiff
.exif()
.and_then(|e| e.get(t::MAKER_NOTE))
.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)
.and_then(|v| rggb_gains(&v))
@@ -678,44 +671,6 @@ mod tests {
assert_eq!(recorded_size_crop((4240, 2832), Some((4240, 0)), 4288, 2848), Some(Rect::new(0, 0, 4240, 2832)));
}
/// A Sony CFA file whose maker note carries only `FileFormat` (tag 0xb000) = `format`.
fn sony_with_file_format(format: [u8; 4]) -> Vec<u8> {
use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value};
// Sony maker note: 12-byte header, then an IFD (one entry whose 4-byte value is stored inline)
let mut note = b"SONY DSC \0\0\0".to_vec();
note.extend(1u16.to_le_bytes());
note.extend(MN_FILE_FORMAT.to_le_bytes());
note.extend(1u16.to_le_bytes()); // BYTE
note.extend(4u32.to_le_bytes());
note.extend(format);
note.extend(0u32.to_le_bytes());
let mut exif = IfdBuilder::new();
exif.set(t::MAKER_NOTE, Value::Undefined(note));
let mut raw = IfdBuilder::new();
raw.set(t::MAKE, Value::Ascii("SONY".into()));
raw.set(t::MODEL, Value::Ascii("DSC-R1".into()));
raw.set(t::IMAGE_WIDTH, Value::Long(vec![8]));
raw.set(t::IMAGE_LENGTH, Value::Long(vec![4]));
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![14]));
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![1]));
raw.set_child(t::EXIF_IFD, exif);
raw.set_image(ImageData::Strips { rows_per_strip: 4, strips: vec![vec![0x10u8; 8 * 4 * 2]] });
TiffWriter::default().write(&[raw]).unwrap()
}
#[test]
fn sr2_is_refused_not_decoded_as_noise() {
for mode in [Mode::Header, Mode::Full] {
let err = decode(&sony_with_file_format([1, 0, 0, 0]), mode).unwrap_err();
assert!(matches!(err, RawError::Unsupported(ref m) if m.contains("SR2")), "{mode:?}: {err:?}");
// the same file as an ARW 2.x decodes
let ok = decode(&sony_with_file_format([3, 0, 0, 0]), mode);
assert!(ok.is_ok(), "{mode:?}: {ok:?}");
}
}
/// A 32767-compressed ARW whose single strip is `strip` bytes for a 32 x 4 image.
fn arw_with_strip(strip: usize) -> Vec<u8> {
use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value};
+11 -20
View File
@@ -19,7 +19,6 @@ const KNOWN_UNSUPPORTED: &[&str] = &[
"orf-olympus-em", // Olympus compressed ORF
"-he.nef", // Nikon High Efficiency (HE / HE★) NEF, issue #193
"sraw", // Canon sRAW / mRAW
"sony-r1-sr2", // Sony SR2 (DSC-R1): encrypted raw data, opens from its preview (#535)
];
#[test]
@@ -541,21 +540,20 @@ fn corpus_sony_sr2_white_balance() {
/// Issue #535: pre-2017 Sony bodies without crop tags shot in the camera's 16:9 mode. The ILCE-7SM2 records the
/// 16:9 size in `FullImageSize`, the DSLR-A580 only in the Exif image size; both crops are centred vertically (each
/// camera JPEG registered on its raw) and drop the right-edge padding, so the image matches its JPEG's aspect ratio
/// and gets a camera look. The DSC-R1's SR2 stores encrypted samples and must open as its preview, not as noise.
/// and gets a camera look.
#[test]
fn corpus_sony_16x9_crops_and_sr2() {
fn corpus_sony_16x9_crops() {
let dir = corpus_root().join("raw");
let read = |name: &str| match std::fs::read(dir.join(name)) {
Ok(bytes) => Some(bytes),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("skip: {name} absent");
None
}
Err(e) => panic!("{name}: {e}"),
};
let mut seen = 0;
for (name, (x, y, w, h)) in [("arw-sony-a7sm2-16x9.arw", (0, 232, 4240, 2384)), ("arw-sony-a580-16x9.arw", (0, 260, 4912, 2760))] {
let Some(bytes) = read(name) else { continue };
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}"));
let c = img.crop;
assert_eq!((c.x, c.y, c.width, c.height), (x, y, w, h), "{name}: crop");
@@ -567,14 +565,7 @@ fn corpus_sony_16x9_crops_and_sr2() {
assert!((aspect / preview - 1.0).abs() < 0.02, "{name}: crop aspect {aspect} vs camera JPEG {preview}");
seen += 1;
}
if let Some(bytes) = read("arw-sony-r1-sr2.sr2") {
for r in [decode(&bytes).map(|_| ()), probe_info(&bytes).map(|_| ())] {
assert!(matches!(r, Err(RawError::Unsupported(ref why)) if why.contains("SR2")), "DSC-R1 SR2: {r:?}");
}
assert!(embedded_preview(&bytes).is_some(), "DSC-R1 SR2: no preview to open");
seen += 1;
}
eprintln!("Sony 16:9 crops and SR2 checked on {seen} files");
eprintln!("Sony 16:9 crops checked on {seen} files");
}
/// The embedded JPEG as linear RGB, reduced to `gw × gh` cells.
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -2
View File
@@ -405,7 +405,7 @@ const RAW_SAMPLES: &[(&str, &str, &str)] = &[
"7cc338a0abc8fdad32d61006f1f8f412297b93e040c7efba572eb2bd8f8e8be2",
),
// pre-2017 bodies in the camera's 16:9 mode (#535): the ILCE-7SM2 records it in FullImageSize, the DSLR-A580 only
// in the Exif image size; and the DSC-R1's SR2, whose raw data is encrypted (opens from its preview)
// in the Exif image size
(
"arw-sony-a7sm2-16x9.arw",
"https://raw.pixls.us/data/Sony/ILCE-7SM2/DSC01005.ARW",
@@ -416,7 +416,6 @@ const RAW_SAMPLES: &[(&str, &str, &str)] = &[
"https://raw.pixls.us/data/Sony/DSLR-A580/RAW_SONY_A580.ARW",
"5b0924d39151239dce19e92e08318f4f62a5a8ac276bd7180ec8463d2cfee709",
),
("arw-sony-r1-sr2.sr2", "https://raw.pixls.us/data/Sony/DSC-R1/_DSC1477.SR2", "921c5f2513dd1671a9089e73fc778fc3573bc4cb70a3a555d0414661d0e62274"),
// CR2 colour-filter layouts differ by model (issue #85): CR2CFAPattern 3 (GBRG) and 1 (RGGB) samples
(
"cr2-canon-40d.cr2",