diff --git a/crates/raw/README.md b/crates/raw/README.md index fda140d75b..b66b533616 100644 --- a/crates/raw/README.md +++ b/crates/raw/README.md @@ -123,7 +123,8 @@ smallest preview at least 512 pixels long, fits and develops in one go. Camera Raw's tuned import fits at the default exposure and as-shot white balance first, then applies the user's Exposure, Temperature and Tint to that same rendering. The D5100's measured image area is 4928 × 3264 at (8, 8) in a 4992 × 3280 -sensor plane; it removes the border without changing levels, WB or calibration. +sensor plane; the D90's is 4288 × 2848 at (10, 10) in a 4352 × 2868 plane; each +removes the border without changing levels, WB or calibration. Measured image areas preserve an explicit file crop. For a slightly tighter camera JPEG, fitting can align the reference proxy using a bounded diff --git a/crates/raw/src/cameras.rs b/crates/raw/src/cameras.rs index 7439433c3e..84204c6bce 100644 --- a/crates/raw/src/cameras.rs +++ b/crates/raw/src/cameras.rs @@ -127,6 +127,10 @@ const PROFILES: &[Profile] = &[Profile { /// 4992 x 3280 sensor data, giving the camera JPEG's 4928 x 3264 frame. /// * **ILCE-7** (2 files): 12 x 12 pixels in from the left and top of the /// 6048 x 4024 cRAW data (36 and 12 pixels of margin on the right and bottom). +/// * **D90** (1 file): 10 x 10 pixels in from the left and top of the +/// 4352 x 2868 sensor data, giving the camera JPEG's 4288 x 2848 frame +/// (10 pixels of margin on the right and bottom; the right border reads the +/// camera's flat 255 from column 4310 on). struct ImageArea { make: &'static str, model: &'static str, @@ -137,6 +141,7 @@ struct ImageArea { } const IMAGE_AREAS: &[ImageArea] = &[ + ImageArea { make: "NIKON CORPORATION", model: "NIKON D90", size: (4352, 2868), crop: (10, 10, 4288, 2848) }, ImageArea { make: "NIKON CORPORATION", model: "NIKON D5100", size: (4992, 3280), crop: (8, 8, 4928, 3264) }, ImageArea { make: "NIKON CORPORATION", model: "NIKON D800E", size: (7424, 4924), crop: (8, 6, 7360, 4912) }, ImageArea { make: "SONY", model: "ILCE-7", size: (6048, 4024), crop: (12, 12, 6000, 4000) }, @@ -152,6 +157,11 @@ pub(crate) fn apply(s: &mut Sensor) { s.crop = Rect::new(x, y, w, h); } let Some(p) = PROFILES.iter().find(|p| make == p.make && model == p.model && (s.width, s.height) == p.size) else { return }; + // A raw with its own colour calibration (a DNG) keeps it: only the measured image area + // above applies. The measured profile stands in for files that carry no matrix themselves. + if !s.color.calibrations.is_empty() { + return; + } let (x, y, w, h) = p.crop; s.crop = Rect::new(x, y, w, h); s.white = [p.white; 3]; @@ -265,6 +275,36 @@ mod tests { assert_eq!(s.crop, Rect::new(0, 0, 7424, 4924)); } + #[test] + fn d90_image_area_matches_the_camera_jpeg() { + let mut s = nef("NIKON D90", 4352, 2868); + apply(&mut s); + assert_eq!(s.crop, Rect::new(10, 10, 4288, 2848)); + // Only the image area is measured: levels, colour and tone stay untouched. + assert!(s.color.calibrations.is_empty() && s.tone_curve.is_empty()); + assert_eq!(s.white, [16383.0; 3]); + // An explicit crop in the file wins over the measured area. + s.crop = Rect::new(0, 0, 4288, 2848); + apply(&mut s); + assert_eq!(s.crop, Rect::new(0, 0, 4288, 2848)); + } + + #[test] + fn a_file_with_its_own_calibration_keeps_it() { + // A file that carries its own colour calibration (a DNG, or any calibrated raw) keeps + // it: the measured profile must not override it (only the image area would apply). + let mut s = nef("NIKON D4", 4992, 3292); + s.color = ColorInfo { + calibrations: vec![Calibration { temperature: 6504.0, color_matrix: IDENTITY, forward_matrix: None, camera_calibration: IDENTITY }], + ..ColorInfo::default() + }; + apply(&mut s); + assert_eq!(s.color.calibrations.len(), 1); + assert_eq!(s.color.calibrations[0].temperature, 6504.0, "the file's own calibration stands"); + assert!(s.tone_curve.is_empty() && s.black_point == 0.0); + assert_eq!(s.crop, Rect::new(0, 0, 4992, 3292), "the D4 has no measured image area"); + } + #[test] fn other_cameras_and_sizes_are_left_alone() { for (model, w, h) in [("NIKON D4S", 4992, 3292), ("NIKON D4", 2496, 1646), ("NIKON D800", 7424, 4924)] { diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 2651f4c4d1..ae0806374d 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -66,7 +66,7 @@ supports many more, including CR3). | DNG (incl. LJPEG and JPEG XL tiles) | yes; lossy DNG no | FILE-215-11 | | Canon CR2 | yes | | | Canon CR3 | detected, unsupported | `crates/raw/src/lib.rs:219` | -| Nikon NEF/NRW (lossless, lossy) | yes; "lossy after split" no | #50; D5100 image area measured on three 14-bit lossy NEFs; [validation](nikon-nef-development.md) | +| Nikon NEF/NRW (lossless, lossy) | yes; "lossy after split" no | #50; D5100 and D90 image areas measured (14-bit NEFs); [validation](nikon-nef-development.md) | | Sony ARW/SR2/SRF (incl. compressed) | yes | | | Panasonic RW2, Pentax PEF | yes | | | Olympus ORF | uncompressed only | | diff --git a/docs/gaps.md b/docs/gaps.md index 52b467b2dd..d90ce1b74d 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -129,6 +129,10 @@ what an agent can't finish alone. tuning retains the fitted look and applies user gains afterwards. Three D5100 NEFs establish a guarded 4928 × 3264 image area. This is an estimated rendering, not a measured D5100 colour calibration; [validation](nikon-nef-development.md). +- **Progress (2026-10-13):** the D90's image area (4288 × 2848 at (10, 10) in a + 4352 × 2868 plane) is measured by aligning the decoded plane with the camera's own + JPEG, so a D90 NEF opens on the camera's frame instead of the uncropped sensor plane; + `crates/raw/src/cameras.rs` (`IMAGE_AREAS`). - **Estimate:** 120–200 h. ### G15. AI and generative features @@ -263,6 +267,7 @@ what an agent can't finish alone. | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | G14: D90 image area measured (4288 × 2848 at (10, 10)); gap remains open | | 2026-10-14 | minor | Corpus floors raised to the measured results of the CMYK, Lab, 32-bit, Levels, Threshold, Posterize, feather and pass-through fixes landed together: Photoshop-authored 165 → 185 of 258, io 146 → 148 of 170, psd-tools 242 → 249 of 309 | | 2026-10-13 | minor | G1: CMYK Hue/Saturation and Color Balance work on C, M and Y as inverted RGB with black kept, as Photoshop does; cmyk hue-saturation 100% → 0.4–0.7% bad pixels, color-balance 74% → 25%; floor unchanged | | 2026-10-13 | minor | G1: CMYK Posterize works on the stored values (1 − ink); psd-tools posterize_cmyk passes; psd-tools floor → 242 of 309 |