diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index c296ece27b..26eca62464 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -114,6 +114,6 @@ next to the files: | `corpus/exr/` (5 files, 2.3 MB) | The deep OpenEXR test images `11`, `42`, `64`, `multivariate` and `objectid.deep.exr` | Contributors to the OpenEXR Project | [openexr `src/test/bin/test_images`](https://github.com/AcademySoftwareFoundation/openexr/tree/main/src/test/bin/test_images) | BSD-3-Clause | | `corpus/affinity/` (60 files) | Public Affinity documents and PNG references: `vector-art/` (9 documents), `afdesignload/` (9), `jac21/` (2), `asset-store-template/` (1), and `affinity-samples/` (18 CC0 documents plus exported references) | samuel-etver; Nick Beeuwsaert; Jeremy Cantu; DAIKI (eviltwo); satoshoe-dev | [vector-art `simple/`](https://github.com/samuel-etver/vector-art/tree/255f8add3c8f0740196e22bd59502b811b532f0b/simple), [AFDesignLoad `testDesigns/`](https://github.com/NickBeeuwsaert/AFDesignLoad/tree/a18dd50a7079fb861a28835eeefa0d015f03f4a1/testDesigns), [Jac21/Branding `Logos/JC/DesignerFiles/Affinity`](https://github.com/Jac21/Branding/tree/57ae3f45bf4637f6927c0b07de25b922c55f944f/Logos/JC/DesignerFiles/Affinity), [AssetStoreTemplate `AssetStoreTemplate/`](https://github.com/eviltwo/AssetStoreTemplate/tree/f671981ee89d15526285b3cc87b4393d3defe644/AssetStoreTemplate), [affinity-samples](https://github.com/satoshoe-dev/affinity-samples/tree/96aca6bd86bbe7a677f1cf0630bd832d23943be8) | CC0-1.0 (vector-art and affinity-samples); MIT, Copyright (c) 2015 Nick Beeuwsaert; MIT, Copyright (c) 2018 Jeremy Cantu; MIT, Copyright (c) 2024 DAIKI | | `corpus/pngsuite/` | PngSuite | Willem van Schaik | | Public domain | -| `corpus/pixls/` (7 files, 226 MB) | Real camera raws, one per decode path: Canon PowerShot SX50 HS (CR2 and its DNG conversion), Nikon D3 uncompressed NEF, Sony DSC-RX0 cRAW ARW, Panasonic DC-G9 high-res RW2, Olympus E-1 GRBG ORF, plus the E-5 packed ORF as the documented known-unsupported case. Per file (each CC0, per its database entry): [IMG_4059.CR2](https://raw.pixls.us/data/Canon/PowerShot%20SX50%20HS/IMG_4059.CR2), [CRW_4061.DNG](https://raw.pixls.us/data/Canon/PowerShot%20SX50%20HS/CRW_4061.DNG), [JD1_8203.NEF](https://raw.pixls.us/data/Nikon/D3/JD1_8203.NEF), [DSC00009.ARW](https://raw.pixls.us/data/Sony/DSC-RX0/DSC00009.ARW), [P1000475.RW2](https://raw.pixls.us/data/Panasonic/DC-G9/P1000475.RW2), [E_1__C106743_gredos.ORF](https://raw.pixls.us/data/Olympus/E-1/E_1__C106743_gredos.ORF), [_7061961_copy.ORF](https://raw.pixls.us/data/Olympus/E-5/_7061961_copy.ORF) | The photographers of the pixls.us raw samples database | [raw.pixls.us](https://raw.pixls.us) `data/`, fetched per [xtask/src/corpus_pins.rs](xtask/src/corpus_pins.rs) | CC0 / public domain, per file as linked | +| `corpus/pixls/` (11 files) | Real camera raws, one per decode path: Canon PowerShot SX50 HS (CR2 and its DNG conversion), Nikon D3 uncompressed NEF and Z6 paired 12/14-bit lossy and packed NEFs, Sony DSC-RX0 cRAW ARW, Panasonic DC-G9 high-res RW2, Olympus E-1 GRBG ORF, plus the E-5 packed ORF as the documented known-unsupported case. Per file (each CC0, per its database entry): [IMG_4059.CR2](https://raw.pixls.us/data/Canon/PowerShot%20SX50%20HS/IMG_4059.CR2), [CRW_4061.DNG](https://raw.pixls.us/data/Canon/PowerShot%20SX50%20HS/CRW_4061.DNG), [JD1_8203.NEF](https://raw.pixls.us/data/Nikon/D3/JD1_8203.NEF), [DSC00009.ARW](https://raw.pixls.us/data/Sony/DSC-RX0/DSC00009.ARW), [P1000475.RW2](https://raw.pixls.us/data/Panasonic/DC-G9/P1000475.RW2), [E_1__C106743_gredos.ORF](https://raw.pixls.us/data/Olympus/E-1/E_1__C106743_gredos.ORF), [_7061961_copy.ORF](https://raw.pixls.us/data/Olympus/E-5/_7061961_copy.ORF), [DSC_0750.NEF](https://raw.pixls.us/data/Nikon/Z%206/DSC_0750.NEF), [DSC_0751.NEF](https://raw.pixls.us/data/Nikon/Z%206/DSC_0751.NEF), [DSC_0753.NEF](https://raw.pixls.us/data/Nikon/Z%206/DSC_0753.NEF), [DSC_0754.NEF](https://raw.pixls.us/data/Nikon/Z%206/DSC_0754.NEF) | The photographers of the pixls.us raw samples database | [raw.pixls.us](https://raw.pixls.us) `data/`, fetched per [xtask/src/corpus_pins.rs](xtask/src/corpus_pins.rs) | CC0 / public domain, per file as linked | Files copied into `corpus/` by hand (tiff, exr, raw) must be MIT, BSD or CC0. diff --git a/crates/raw/README.md b/crates/raw/README.md index b8c75ebff6..fda140d75b 100644 --- a/crates/raw/README.md +++ b/crates/raw/README.md @@ -83,6 +83,12 @@ camera colour tables were copied. | CR3 | Native CRX unsupported; bounded JPEG-track/PRVW preview fallback with CMT1 orientation. I/O labels the result as an 8-bit camera preview | | X-Trans and other periodic non-Bayer CFAs (up to 16×16) | Demosaiced by one pattern-agnostic edge-directed method, whatever `Demosaic` is chosen | +Nikon maker-note compression modes 6/10 select checked little-endian packed 12/14-bit +rows even when TIFF Compression is 34713. The modern lossy `0x44 0x40` curve uses +its smaller encoded domain while preserving sensor ADUs. Valid Nikon CropArea +coordinates take precedence over model fallbacks. HE/HE* and lossy-after-split +remain unsupported. Four pinned CC0 Z6 files cover both bit depths and both paths. + ## Development pipeline 1. Linearization table, per-position black level, scale to the white level. diff --git a/crates/raw/src/nefc.rs b/crates/raw/src/nefc.rs index 9ca5eff5f9..db1f8b1d74 100644 --- a/crates/raw/src/nefc.rs +++ b/crates/raw/src/nefc.rs @@ -33,7 +33,9 @@ //! * Lossless (`0x0096` version `0x46`): the decoded values are the samples. //! Lossy (`0x44`): they index a curve. Type 1 (`0x44 0x10`) stores the whole //! curve, type 2 (`0x44 0x20` / `0x44 0x40`) stores `n` points spaced -//! `2^bits / (n - 1)` codes apart, linearly interpolated. +//! `2^bits / (n - 1)` codes apart, linearly interpolated. Version `0x44 0x40` +//! uses a two-bit-smaller encoded domain (1025 / 4097 levels for 12 / 14-bit +//! samples), not the sensor bit depth; see Bill Claff's NEF Compression tables. //! * Values in the maker note are in the maker note's byte order. //! //! Not decoded (reported as unsupported; the embedded preview is used @@ -50,6 +52,59 @@ pub(crate) const NIKON_COMPRESSION: u32 = 34713; /// Maker note NEFLinearizationTable. pub(crate) const NIKON_LINEARIZATION_TABLE: u16 = 0x0096; +/// Packed uncompressed NEF modes 6 (12-bit) and 10 (14-bit). Measured on CC0 +/// Z6/Z7/D850 samples: little-endian bit packing, rows optionally padded to 16 +/// bytes. Selection comes from the maker note, never from the strip's length. +pub(crate) fn read_packed(t: &Tiff, ifd: &Ifd, bits: u32, limits: &Limits) -> Result { + let w = t.tag_uint(ifd, tag::IMAGE_WIDTH).ok_or_else(|| RawError::malformed("packed NEF has no width"))? as usize; + let h = t.tag_uint(ifd, tag::IMAGE_LENGTH).ok_or_else(|| RawError::malformed("packed NEF has no height"))? as usize; + if !t.le || t.tag_uint(ifd, tag::BITS_PER_SAMPLE) != Some(bits) || t.tag_uint(ifd, tag::SAMPLES_PER_PIXEL).unwrap_or(1) != 1 { + return Err(RawError::unsupported("packed NEF byte order, bit depth or sample layout")); + } + limits.check(w as u64, h as u64, 2)?; + let offsets = t.tag_uints(ifd, tag::STRIP_OFFSETS); + let counts = t.tag_uints(ifd, tag::STRIP_BYTE_COUNTS); + let ([off], [len]) = (offsets.as_slice(), counts.as_slice()) else { + return Err(RawError::unsupported("packed NEF with multiple strips")); + }; + let src = t.bytes(*off as usize, *len as usize).ok_or_else(|| RawError::malformed("packed NEF strip lies outside the file"))?; + let data = unpack_little_rows(src, w, h, bits)?; + Ok(Plane { width: w, height: h, samples: 1, bits, data }) +} + +fn unpack_little_rows(src: &[u8], w: usize, h: usize, bits: u32) -> Result> { + let bad = || RawError::malformed("packed NEF row layout is invalid or truncated"); + if w == 0 || h == 0 || !matches!(bits, 12 | 14) { + return Err(bad()); + } + let row = w.checked_mul(bits as usize).ok_or_else(bad)?.div_ceil(8); + let padded = row.checked_add(15).ok_or_else(bad)? / 16 * 16; + let stride = if row.checked_mul(h) == Some(src.len()) { + row + } else if padded.checked_mul(h) == Some(src.len()) { + padded + } else { + return Err(bad()); + }; + let count = w.checked_mul(h).ok_or_else(bad)?; + let mut out = Vec::with_capacity(count); + let mask = (1u32 << bits) - 1; + for row in src.chunks_exact(stride) { + let mut bytes = row.iter(); + let (mut acc, mut have) = (0u32, 0u32); + for _ in 0..w { + while have < bits { + acc |= u32::from(*bytes.next().ok_or_else(bad)?) << have; + have += 8; + } + out.push((acc & mask) as u16); + acc >>= bits; + have -= bits; + } + } + Ok(out) +} + /// A code word that never occurred in any analysed sample. const UNSEEN: u8 = u8::MAX; @@ -216,7 +271,10 @@ pub(crate) fn parse_table(t: &[u8], le: bool, bits: u32) -> Result if split != 0 { return Err(RawError::unsupported(format!("Nikon \"lossy after split\" compressed NEF (split at row {split})"))); } - let range = 1usize << bits; + // The modern lossy curve has a smaller *encoded* domain while + // its output remains in sensor ADUs. Using 2^bits stretches the + // curve fourfold and puts Z6 image values below its black level. + let range = 1usize << if v1 == 0x40 { bits - 2 } else { bits }; if n - 1 > range || !range.is_multiple_of(n - 1) { return Err(RawError::unsupported(format!("Nikon compressed NEF: {n}-point curve for {bits}-bit data"))); } @@ -503,6 +561,37 @@ mod tests { assert!(parse_table(&[0x44, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], false, 12).is_err()); } + #[test] + fn modern_lossy_curve_preserves_sensor_adus_at_both_depths() { + for bits in [12u32, 14] { + for le in [false, true] { + let put = |v: u16| if le { v.to_le_bytes() } else { v.to_be_bytes() }; + let mut t = vec![0x44, 0x40]; + for _ in 0..4 { + t.extend(put(256)); + } + t.extend(put(257)); + let white = (1u16 << bits) - 1; + for i in 0..=256u32 { + t.extend(put(((i * 64).min(u32::from(white))) as u16)); + } + t.resize(624, 0); + let parsed = parse_table(&t, le, bits).unwrap(); + let Encoding::Lossy(curve) = &parsed.encoding else { panic!() }; + let range = 1usize << (bits - 2); + assert_eq!(curve.len(), range + 1); + assert_eq!(curve[range], white); + // Stored point 16 is 1024 ADU. It belongs to encoded code 256 + // at 14 bits, code 64 at 12 bits, independent of end-point clipping. + assert_eq!(curve[range / 16], 1024.min(white)); + let indices = vec![0, (range / 16) as i32, (range / 8) as i32, (range / 32) as i32]; + let huffman = if bits == 12 { &LOSSY_12 } else { &LOSSY_14 }; + let src = encode(&indices, 2, huffman, parsed.seeds); + assert_eq!(decode(&src, 2, 2, bits, &parsed).unwrap(), vec![0, 1024, 2048, 512]); + } + } + } + /// A 14-bit lossless table (version 0x46) with the given seeds. fn lossless_table(seed: u16, le: bool) -> Vec { let mut t = vec![0x46, 0x30]; diff --git a/crates/raw/src/tiffep.rs b/crates/raw/src/tiffep.rs index 4a75491f5f..25befbddea 100644 --- a/crates/raw/src/tiffep.rs +++ b/crates/raw/src/tiffep.rs @@ -78,6 +78,18 @@ fn nikon_maker_note<'a>(t: &Tiff<'a>, ifds: &[Ifd]) -> Option<(Tiff<'a>, Ifd)> { Some((inner, ifd)) } +/// ExifTool's public Nikon tag documentation: legacy 0x93, or a u16 at offset +/// 10 of MakerNotes0x51. The latter is used by Z6/Z7, despite TIFF Compression +/// remaining 34713 even for uncompressed sensor data. +fn nikon_compression(nikon: Option<&(Tiff<'_>, Ifd)>) -> Option { + let (t, m) = nikon?; + t.tag_uint(m, 0x0093).or_else(|| { + let b = t.raw(m.get(0x0051)?)?; + let v: [u8; 2] = b.get(10..12)?.try_into().ok()?; + Some(u32::from(if t.le { u16::from_le_bytes(v) } else { u16::from_be_bytes(v) })) + }) +} + /// Four levels listed in R, G, G, B order, placed at their 2×2 CFA positions /// (data parity). pub(crate) fn rggb_by_position(cfa: &Cfa, v: &[f64]) -> Option<[f32; 4]> { @@ -130,7 +142,12 @@ pub(crate) fn decode(t: &Tiff, format: RawFormat, limits: &Limits) -> Result nefc::read_packed(t, &raw, 12, limits)?, + Some(10) => nefc::read_packed(t, &raw, 14, limits)?, + Some(13 | 14) => return Err(RawError::unsupported("Nikon High Efficiency / High Efficiency* NEF")), + _ => nefc::read_compressed(t, &raw, nikon.as_ref(), limits)?, + } } else { read_plane(t, &raw, limits, JpegLayout::Quads)? }; @@ -195,6 +212,17 @@ pub(crate) fn decode(t: &Tiff, format: RawFormat, limits: &Limits) -> Result= 0.0 { v.round() as usize } else { 0 }; let r = Rect::new(f(*ox), f(*oy), f(*cw), f(*ch)).intersect(&full); diff --git a/crates/raw/tests/nef_packed.rs b/crates/raw/tests/nef_packed.rs new file mode 100644 index 0000000000..f050681857 --- /dev/null +++ b/crates/raw/tests/nef_packed.rs @@ -0,0 +1,86 @@ +use photocraft_raw::testgen::{Val, nef_compressed}; +use photocraft_raw::{Limits, RawError}; + +fn packed(values: &[u16], w: usize, bits: u32, padded: bool) -> Vec { + let mut out = Vec::new(); + for row in values.chunks_exact(w) { + let start = out.len(); + let (mut acc, mut have) = (0u32, 0u32); + for v in row { + acc |= u32::from(*v) << have; + have += bits; + while have >= 8 { + out.push(acc as u8); + acc >>= 8; + have -= 8; + } + } + if have > 0 { + out.push(acc as u8); + } + if padded { + out.resize(start + (out.len() - start).div_ceil(16) * 16, 0xff); + } + } + out +} + +#[test] +fn packed_nef_uses_maker_mode_not_the_shared_tiff_compression() { + let (w, h) = (22, 5); + for (bits, mode) in [(12u32, 6u16), (14, 10)] { + let values: Vec = (0..w * h).map(|i| ((i * 83 + 11) % (1 << bits)) as u16).collect(); + for padded in [false, true] { + for big_note in [false, true] { + let strip = packed(&values, w, bits, padded); + for modern in [false, true] { + let tag = if modern { + let mut record = b"01000400\0\0".to_vec(); + record.extend(if big_note { mode.to_be_bytes() } else { mode.to_le_bytes() }); + (0x51, Val::Undefined(record)) + } else { + (0x93, Val::Short(vec![mode])) + }; + let bytes = nef_compressed(w, h, bits as u16, strip.clone(), [0, 1, 1, 2], big_note, vec![tag]); + let s = photocraft_raw::decode(&bytes, &Limits::default()).unwrap(); + assert_eq!(s.data, values); + } + } + } + } +} + +#[test] +fn packed_nef_rejects_truncation_wrong_depth_and_unknown_modes() { + let strip = packed(&[1000; 18 * 5], 18, 14, true); + for size in 0..strip.len() { + let bytes = nef_compressed(18, 5, 14, strip[..size].to_vec(), [0, 1, 1, 2], false, vec![(0x93, Val::Short(vec![10]))]); + // The unpadded size is another valid layout; all other truncations fail. + if size != (18 * 14usize).div_ceil(8) * 5 { + assert!(photocraft_raw::decode(&bytes, &Limits::default()).is_err()); + } + } + let bytes = nef_compressed(18, 5, 12, strip.clone(), [0, 1, 1, 2], false, vec![(0x93, Val::Short(vec![10]))]); + assert!(matches!(photocraft_raw::decode(&bytes, &Limits::default()), Err(RawError::Unsupported(_)))); + for mode in [3, 4, 9, 13, 14, 65535] { + let bytes = nef_compressed(18, 5, 14, strip.clone(), [0, 1, 1, 2], false, vec![(0x93, Val::Short(vec![mode]))]); + assert!(matches!(photocraft_raw::decode(&bytes, &Limits::default()), Err(RawError::Unsupported(_)))); + } +} + +#[test] +fn nikon_crop_area_uses_sensor_coordinates_and_ignores_invalid_rectangles() { + let strip = packed(&[1000; 22 * 10], 22, 14, true); + for (rect, expected) in [ + ([2, 2, 18, 6], photocraft_raw::Rect::new(2, 2, 18, 6)), + ([2, 2, 22, 10], photocraft_raw::Rect::new(0, 0, 22, 10)), + ([2, 2, 0, 6], photocraft_raw::Rect::new(0, 0, 22, 10)), + ([u32::MAX, 2, 18, 6], photocraft_raw::Rect::new(0, 0, 22, 10)), + ] { + let bytes = nef_compressed(22, 10, 14, strip.clone(), [0, 1, 1, 2], false, vec![(0x93, Val::Short(vec![10])), (0x45, Val::Long(rect.to_vec()))]); + let sensor = photocraft_raw::decode(&bytes, &Limits::default()).unwrap(); + assert_eq!(sensor.crop, expected); + let developed = photocraft_raw::develop_sensor(&sensor, &Default::default()).unwrap(); + assert_eq!((developed.width as usize, developed.height as usize), (expected.width, expected.height)); + } +} diff --git a/crates/raw/tests/raw_corpus.rs b/crates/raw/tests/raw_corpus.rs index d90e06a6c1..afd168a38a 100644 --- a/crates/raw/tests/raw_corpus.rs +++ b/crates/raw/tests/raw_corpus.rs @@ -144,6 +144,41 @@ fn developing_produces_the_crop_in_16bit_rgb() { } } +/// These paired exposures used to decode below their recorded black levels because the +/// version 0x44 0x40 linearization curve was interpolated across the sensor's full bit range. +#[test] +fn modern_lossy_nef_preserves_brightness_at_both_depths() { + for (file, black, white) in [("DSC_0751.NEF", 1008.0, 16383.0), ("DSC_0754.NEF", 252.0, 4095.0)] { + let bytes = std::fs::read(pixls(file)).unwrap(); + let s = photocraft_raw::decode(&bytes, &Limits::default()).unwrap(); + assert_eq!(s.model.as_deref(), Some("NIKON Z 6")); + assert_eq!((s.width, s.height), (6064, 4040)); + assert_eq!(s.black.values, vec![black; 4]); + assert!(s.white.iter().all(|v| (*v - white).abs() < 100.0)); + // The same scene spans deep shadows and bright highlights at either sensor depth. + let max = s.data.iter().copied().max().unwrap(); + assert!(f32::from(max) > white * 0.9, "{file}: maximum {max}"); + let mean = s.data.iter().map(|v| f64::from(*v)).sum::() / s.data.len() as f64; + assert!(mean > f64::from(black) * 1.5, "{file}: mean {mean}"); + let d = photocraft_raw::develop_sensor(&s, &photocraft_raw::DevelopOptions::default()).unwrap(); + assert_eq!((d.width, d.height), (6048, 4024)); + assert!(d.rgb.iter().copied().max().unwrap() > 32768, "{file}: black development"); + } +} + +#[test] +fn packed_nef_opens_full_sensor_and_metadata_crop_at_both_depths() { + for (file, first) in [("DSC_0750.NEF", [3539, 3761, 3640, 3904]), ("DSC_0753.NEF", [866, 932, 881, 948])] { + let bytes = std::fs::read(pixls(file)).unwrap(); + let s = photocraft_raw::decode(&bytes, &Limits::default()).unwrap(); + assert_eq!((s.width, s.height), (6064, 4040)); + assert_eq!(&s.data[..4], &first); + assert_eq!(s.crop, photocraft_raw::Rect::new(8, 8, 6048, 4024)); + let d = photocraft_raw::develop_sensor(&s, &Default::default()).unwrap(); + assert_eq!((d.width, d.height), (6048, 4024)); + } +} + /// The E-5 file is the documented known-unsupported case: later Olympus bodies store packed /// (compressed) sensor data. The error must say so, never panic, and the file still previews. #[test] diff --git a/xtask/pixls.sha256 b/xtask/pixls.sha256 index 3878b45356..f76f983ba4 100644 --- a/xtask/pixls.sha256 +++ b/xtask/pixls.sha256 @@ -3,5 +3,9 @@ 6c2ddd232f5432fb7bd0e28efc12d66995cedcd847f801fd854bfdfb180db50c IMG_4059.CR2 7c24fefb1f73ad45c42a0be0078e15b7e91deab1d623efceebfc3c4d0d06135c P1000475.RW2 880c60c5f611adf6b70de3d099f8433492de3a3d96866d570122f17f0a651fc8 JD1_8203.NEF +8ca94584578b35a422744c1a610174ce37df0691588edfdecaec3111b3fbbb02 DSC_0751.NEF +3cbd9acf9e9bfd7d0c62c46436109556a101f7535c5fd25f86dad6ea61bf3886 DSC_0754.NEF +b4cdb8ecf8a971ec023dd242251c6cbfb19966914e37c2311dc9813a7ab5e15f DSC_0750.NEF +13c1ec5c2d020b3038d9cb660bc0e0b5a52cb9fe12561dec9ba0e9919e9cbee2 DSC_0753.NEF ab0d54e3819c9372df0fa98c8686dba0ae87c9d43526a5dd6fd26bd2a664483d _7061961_copy.ORF f602f8ddeeb9b062f60bfdd3ba0d560c8e54f1f64dbc20e18df5222e2f37a428 DSC00009.ARW diff --git a/xtask/src/corpus_pins.rs b/xtask/src/corpus_pins.rs index 917412709a..908557feea 100644 --- a/xtask/src/corpus_pins.rs +++ b/xtask/src/corpus_pins.rs @@ -253,6 +253,12 @@ pub const PIXLS_FILES: &[(&str, &str)] = &[ ("Canon/PowerShot%20SX50%20HS/IMG_4059.CR2", "IMG_4059.CR2"), ("Canon/PowerShot%20SX50%20HS/CRW_4061.DNG", "CRW_4061.DNG"), ("Nikon/D3/JD1_8203.NEF", "JD1_8203.NEF"), + // Modern lossy NEF curve: the encoded range is two bits smaller than the sensor ADUs. + ("Nikon/Z%206/DSC_0751.NEF", "DSC_0751.NEF"), + ("Nikon/Z%206/DSC_0754.NEF", "DSC_0754.NEF"), + // Uncompressed packed NEF selected by maker-note mode, despite TIFF Compression 34713. + ("Nikon/Z%206/DSC_0750.NEF", "DSC_0750.NEF"), + ("Nikon/Z%206/DSC_0753.NEF", "DSC_0753.NEF"), ("Sony/DSC-RX0/DSC00009.ARW", "DSC00009.ARW"), ("Panasonic/DC-G9/P1000475.RW2", "P1000475.RW2"), ("Olympus/E-1/E_1__C106743_gredos.ORF", "E_1__C106743_gredos.ORF"),