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"),