diff --git a/core/dr-decode/examples/linear_dng.rs b/core/dr-decode/examples/linear_dng.rs new file mode 100644 index 0000000..922d93c --- /dev/null +++ b/core/dr-decode/examples/linear_dng.rs @@ -0,0 +1,220 @@ +//! TRACES: S15 | FR-MRG-3 +//! Spike S15.1 — does rawler read back a linear DNG this application writes? +//! +//! cargo run -p dr-decode --example linear_dng [-- ] +//! +//! Decides FR-MRG-3's container. A panorama composite is three linear samples +//! per pixel with a camera matrix attached, which is exactly what a +//! `LinearRaw` DNG is; if rawler parses one, the composite re-enters the +//! library as `Format::Dng` and the only new decode work is a `cpp == 3` +//! branch. If it does not, the container is a float TIFF with a decode path +//! of its own. +//! +//! The file is hand-rolled rather than written with the `tiff` crate, whose +//! encoder fixes `PhotometricInterpretation` to RGB and cannot say +//! `LinearRaw`. Eighty lines of IFD is the cheaper thing to own than a fork. + +use rawler::rawsource::RawSource; + +const W: u32 = 64; +const H: u32 = 48; + +fn main() { + let bytes = write_linear_dng(W, H); + if let Some(path) = std::env::args().nth(1) { + std::fs::write(&path, &bytes).expect("write"); + println!("wrote {path} ({} bytes)", bytes.len()); + } + + let source = RawSource::new_from_slice(&bytes); + let decoder = match rawler::get_decoder(&source) { + Ok(d) => d, + Err(e) => { + println!("FAIL get_decoder: {e}"); + std::process::exit(1); + } + }; + println!("ok decoder found"); + + let image = match decoder.raw_image(&source, &Default::default(), false) { + Ok(i) => i, + Err(e) => { + println!("FAIL raw_image: {e}"); + std::process::exit(1); + } + }; + println!( + "ok raw_image: {}×{}, cpp {}, bps {}, {} samples, make {:?} model {:?}", + image.width, + image.height, + image.cpp, + image.bps, + match &image.data { + rawler::RawImageData::Integer(v) => v.len(), + rawler::RawImageData::Float(v) => v.len(), + }, + image.make, + image.model + ); + println!( + " white {:?} black {:?} wb {:?}", + image.whitelevel.0, + image + .blacklevel + .levels + .iter() + .map(|r| r.n as f32 / r.d.max(1) as f32) + .collect::>(), + image.wb_coeffs + ); + + // The pixel at (1, 0) was written as (1000, 2000, 3000): if the samples + // come back interleaved in that order, cpp == 3 means what it says. + if let rawler::RawImageData::Integer(v) = &image.data { + let i = image.cpp * 1; + println!(" pixel (1,0) = {:?}", &v[i..i + image.cpp.min(3)]); + } + + // What dr-decode itself makes of it: the colour matrix rawler parsed into + // the camera definition, and the profile the decoder would build from it. + println!(" rawler color_matrix: {:?}", image.camera.color_matrix); + let dng = dr_decode::profile::read_dng_matrices(decoder.as_ref()); + let profile = dr_decode::CameraProfile::extract(&image, &dng); + println!( + " CameraProfile: {}", + profile + .as_ref() + .map(|p| format!("xyz_to_cam {:?}", p.xyz_to_cam())) + .unwrap_or_else(|| "none".into()) + ); + + match dr_decode::decode(&bytes) { + Ok(r) => println!( + "note dr_decode::decode accepted it as CFA: {}×{}, {} samples — the cpp==3 branch is the work", + r.width, + r.height, + r.data.len() + ), + Err(e) => println!("note dr_decode::decode refused it: {e} — the cpp==3 branch is the work"), + } +} + +/// A minimal `LinearRaw` DNG: one IFD, uncompressed 16-bit RGB, the tags a +/// decoder needs to treat it as a DNG and the matrix a develop chain needs +/// to treat it as a camera. Little-endian, one strip. +fn write_linear_dng(w: u32, h: u32) -> Vec { + // Pixels first, so their offset is known: a ramp with one marker pixel. + let mut pixels: Vec = Vec::with_capacity((w * h * 3) as usize); + for y in 0..h { + for x in 0..w { + if (x, y) == (1, 0) { + pixels.extend([1000, 2000, 3000]); + } else { + let v = ((x + y) * 512).min(65535) as u16; + pixels.extend([v, v / 2, v / 3]); + } + } + } + let pixel_bytes: Vec = pixels.iter().flat_map(|v| v.to_le_bytes()).collect(); + + // Layout: header (8) | pixels | extra data | IFD. + let pixels_off = 8u32; + let extra_off = pixels_off + pixel_bytes.len() as u32; + + // Values that do not fit in four bytes go in `extra`, and the entry + // points at them. + let mut extra: Vec = Vec::new(); + let mut entries: Vec<(u16, u16, u32, [u8; 4])> = Vec::new(); + + fn short(tag: u16, v: u16) -> (u16, u16, u32, [u8; 4]) { + let mut b = [0u8; 4]; + b[..2].copy_from_slice(&v.to_le_bytes()); + (tag, 3, 1, b) + } + fn long(tag: u16, v: u32) -> (u16, u16, u32, [u8; 4]) { + (tag, 4, 1, v.to_le_bytes()) + } + fn ascii(extra: &mut Vec, extra_off: u32, tag: u16, s: &str) -> (u16, u16, u32, [u8; 4]) { + let mut bytes = s.as_bytes().to_vec(); + bytes.push(0); + let off = extra_off + extra.len() as u32; + extra.extend(&bytes); + (tag, 2, bytes.len() as u32, off.to_le_bytes()) + } + + entries.push(long(254, 0)); // NewSubfileType: main image + entries.push(long(256, w)); + entries.push(long(257, h)); + // BitsPerSample ×3 — three shorts, six bytes, so out of line. + { + let off = extra_off + extra.len() as u32; + for _ in 0..3 { + extra.extend(16u16.to_le_bytes()); + } + entries.push((258, 3, 3, off.to_le_bytes())); + } + entries.push(short(259, 1)); // Compression: none + entries.push(short(262, 34892)); // PhotometricInterpretation: LinearRaw + entries.push(ascii(&mut extra, extra_off, 271, "DarkRoom")); + entries.push(ascii(&mut extra, extra_off, 272, "Panorama")); + entries.push(long(273, pixels_off)); // StripOffsets + entries.push(short(274, 1)); // Orientation + entries.push(short(277, 3)); // SamplesPerPixel + entries.push(long(278, h)); // RowsPerStrip + entries.push(long(279, pixel_bytes.len() as u32)); // StripByteCounts + entries.push(short(284, 1)); // PlanarConfiguration: chunky + entries.push((50706, 1, 4, [1, 4, 0, 0])); // DNGVersion + entries.push((50707, 1, 4, [1, 4, 0, 0])); // DNGBackwardVersion + entries.push(ascii(&mut extra, extra_off, 50708, "DarkRoom Panorama")); // UniqueCameraModel + entries.push(long(50717, 65535)); // WhiteLevel + // ColorMatrix1: XYZ → camera, 9 SRATIONALs. A plausible sRGB-ish matrix + // (the inverse of the sRGB D65 primaries), scaled to integers. + { + let m: [(i32, i32); 9] = [ + (32406, 10000), + (-15372, 10000), + (-4986, 10000), + (-9689, 10000), + (18758, 10000), + (415, 10000), + (557, 10000), + (-2040, 10000), + (10570, 10000), + ]; + let off = extra_off + extra.len() as u32; + for (n, d) in m { + extra.extend(n.to_le_bytes()); + extra.extend(d.to_le_bytes()); + } + entries.push((50721, 10, 9, off.to_le_bytes())); + } + // AsShotNeutral: 3 RATIONALs, neutral. + { + let off = extra_off + extra.len() as u32; + for _ in 0..3 { + extra.extend(1u32.to_le_bytes()); + extra.extend(1u32.to_le_bytes()); + } + entries.push((50728, 5, 3, off.to_le_bytes())); + } + entries.push(short(50778, 21)); // CalibrationIlluminant1: D65 + + entries.sort_by_key(|e| e.0); + + let ifd_off = extra_off + extra.len() as u32; + let mut out = Vec::new(); + out.extend(b"II"); + out.extend(42u16.to_le_bytes()); + out.extend(ifd_off.to_le_bytes()); + out.extend(&pixel_bytes); + out.extend(&extra); + out.extend((entries.len() as u16).to_le_bytes()); + for (tag, ty, count, value) in &entries { + out.extend(tag.to_le_bytes()); + out.extend(ty.to_le_bytes()); + out.extend(count.to_le_bytes()); + out.extend(value); + } + out.extend(0u32.to_le_bytes()); // no next IFD + out +} diff --git a/docs/panorama.md b/docs/panorama.md index e04bfec..2c287ce 100644 --- a/docs/panorama.md +++ b/docs/panorama.md @@ -167,7 +167,7 @@ the way S9 calibrates R1. FR-MRG-3. Scene-linear, ≥ 16 bits, wide gamut, the first source's capture metadata, named from the first source with a `-pano` suffix, beside it. -Two containers are candidates and S15.1 decides: +Two containers were candidates and S15.1 decided, on 2026-09-19: - **Linear DNG.** `PhotometricInterpretation = LinearRaw`, three samples per pixel, `ColorMatrix1` carried from the first source. Re-enters through @@ -178,9 +178,21 @@ Two containers are candidates and S15.1 decides: the existing encoder with a different sample type, and nothing about it is uncertain. -Either way the composite enters the pipeline as a non-CFA, *linear* source — -`from_rgba8`'s sibling with `non_linear = false` and the base curve resolved -from the carried matrix — and is developed as any RAW is. +**Linear DNG.** [`examples/linear_dng.rs`](../core/dr-decode/examples/linear_dng.rs) +hand-rolls a 64 × 48 `LinearRaw` DNG — one IFD, uncompressed 16-bit RGB, +`DNGVersion`, `ColorMatrix1`, `AsShotNeutral`, `CalibrationIlluminant1` — and +rawler 0.7 reads it back: `cpp 3`, the samples interleaved as written, the +matrix parsed into the camera definition, and `CameraProfile::extract` builds +the same profile it would for a camera file. ImageMagick's libraw reads the +same bytes. What does *not* yet work is `dr_decode::decode`, which accepts the +file as CFA and hands the pipeline three times the samples it expects: the +`cpp == 3` branch is the work, and it is the only decode work. + +The composite therefore enters the pipeline as a non-CFA, *linear* source — +`from_rgba8`'s sibling with `non_linear = false` and the colour matrix carried +from the DNG — and is developed as any RAW is. The writer is the example's +IFD, grown up: tiled rather than one strip (FR-MRG-11 encodes per chunk), and +carrying the first source's EXIF in a sub-IFD as `dr-export` already does. ## 9. Interaction diff --git a/docs/traceability.md b/docs/traceability.md index bd0999d..9526660 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,18 +9,18 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 354 | -| TRACES tags found | 1502 | +| Source files scanned | 355 | +| TRACES tags found | 1503 | | Requirements defined | 184 | | Requirements deferred (post-v1) | 24 | -| Requirements covered | 142 | -| **Coverage** | **77.2%** (142/184) | +| Requirements covered | 143 | +| **Coverage** | **77.7%** (143/184) | ### By type | Type | Covered | Defined | |---|---|---| -| FR | 104 | 127 | +| FR | 105 | 127 | | NFR | 34 | 50 | | R | 4 | 7 | @@ -98,6 +98,7 @@ _None._ | FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:992`](../ui/dr-ui/src/export.rs#L992), [`ui/dr-ui/src/lib.rs:262`](../ui/dr-ui/src/lib.rs#L262), [`ui/dr-ui/src/lib.rs:2752`](../ui/dr-ui/src/lib.rs#L2752), [`ui/dr-ui/src/lib.rs:601`](../ui/dr-ui/src/lib.rs#L601), [`ui/dr-ui/src/lib.rs:647`](../ui/dr-ui/src/lib.rs#L647), [`ui/dr-ui/src/lib.rs:674`](../ui/dr-ui/src/lib.rs#L674), [`ui/dr-ui/src/library_ui.rs:4275`](../ui/dr-ui/src/library_ui.rs#L4275), [`ui/dr-ui/src/library_ui.rs:719`](../ui/dr-ui/src/library_ui.rs#L719), [`ui/dr-ui/src/library_ui.rs:8026`](../ui/dr-ui/src/library_ui.rs#L8026), [`ui/dr-ui/src/library_ui.rs:8103`](../ui/dr-ui/src/library_ui.rs#L8103), [`ui/dr-ui/src/library_ui.rs:8115`](../ui/dr-ui/src/library_ui.rs#L8115), [`ui/dr-ui/src/library_ui.rs:819`](../ui/dr-ui/src/library_ui.rs#L819), [`ui/dr-ui/src/library_ui.rs:876`](../ui/dr-ui/src/library_ui.rs#L876), [`ui/dr-ui/ui/app.slint:1091`](../ui/dr-ui/ui/app.slint#L1091), [`ui/dr-ui/ui/app.slint:1630`](../ui/dr-ui/ui/app.slint#L1630), [`ui/dr-ui/ui/library.slint:1583`](../ui/dr-ui/ui/library.slint#L1583), [`ui/dr-ui/ui/library.slint:4278`](../ui/dr-ui/ui/library.slint#L4278) | | FR-EXP-8 | [`core/dr-decode/src/lib.rs:330`](../core/dr-decode/src/lib.rs#L330), [`core/dr-decode/src/lib.rs:354`](../core/dr-decode/src/lib.rs#L354), [`core/dr-decode/src/lib.rs:368`](../core/dr-decode/src/lib.rs#L368), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:655`](../core/dr-types/src/lib.rs#L655), [`core/dr-types/src/settings.rs:647`](../core/dr-types/src/settings.rs#L647), [`ui/dr-ui/src/develop.rs:4539`](../ui/dr-ui/src/develop.rs#L4539), [`ui/dr-ui/src/develop.rs:4651`](../ui/dr-ui/src/develop.rs#L4651), [`ui/dr-ui/src/develop.rs:729`](../ui/dr-ui/src/develop.rs#L729), [`ui/dr-ui/src/export.rs:467`](../ui/dr-ui/src/export.rs#L467), [`ui/dr-ui/src/export.rs:634`](../ui/dr-ui/src/export.rs#L634), [`ui/dr-ui/src/export.rs:680`](../ui/dr-ui/src/export.rs#L680), [`ui/dr-ui/src/export.rs:702`](../ui/dr-ui/src/export.rs#L702), [`ui/dr-ui/src/export.rs:826`](../ui/dr-ui/src/export.rs#L826), [`ui/dr-ui/src/export.rs:844`](../ui/dr-ui/src/export.rs#L844), [`ui/dr-ui/src/lib.rs:262`](../ui/dr-ui/src/lib.rs#L262), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`ui/dr-ui/src/lib.rs:633`](../ui/dr-ui/src/lib.rs#L633), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-9 | [`core/dr-decode/src/lib.rs:510`](../core/dr-decode/src/lib.rs#L510), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:4672`](../ui/dr-ui/src/develop.rs#L4672), [`ui/dr-ui/src/develop.rs:4710`](../ui/dr-ui/src/develop.rs#L4710), [`ui/dr-ui/src/develop.rs:6878`](../ui/dr-ui/src/develop.rs#L6878), [`ui/dr-ui/src/lib.rs:601`](../ui/dr-ui/src/lib.rs#L601), [`ui/dr-ui/src/library.rs:4052`](../ui/dr-ui/src/library.rs#L4052), [`ui/dr-ui/src/library.rs:4522`](../ui/dr-ui/src/library.rs#L4522), [`ui/dr-ui/src/library.rs:4558`](../ui/dr-ui/src/library.rs#L4558), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | +| FR-MRG-3 | [`core/dr-decode/examples/linear_dng.rs:1`](../core/dr-decode/examples/linear_dng.rs#L1) | | FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:361`](../core/dr-sync/src/account.rs#L361), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) | | FR-NC-10 | [`core/dr-sync/src/account.rs:226`](../core/dr-sync/src/account.rs#L226), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:674`](../ui/dr-ui/src/lib.rs#L674), [`ui/dr-ui/src/library.rs:1170`](../ui/dr-ui/src/library.rs#L1170), [`ui/dr-ui/src/library.rs:2463`](../ui/dr-ui/src/library.rs#L2463), [`ui/dr-ui/src/library.rs:589`](../ui/dr-ui/src/library.rs#L589), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:2062`](../ui/dr-ui/src/library_ui.rs#L2062), [`ui/dr-ui/src/library_ui.rs:4275`](../ui/dr-ui/src/library_ui.rs#L4275), [`ui/dr-ui/src/library_ui.rs:660`](../ui/dr-ui/src/library_ui.rs#L660), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:1022`](../core/dr-sync-nextcloud/src/lib.rs#L1022), [`core/dr-sync-nextcloud/src/lib.rs:40`](../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:87`](../core/dr-sync/src/account.rs#L87), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | @@ -208,7 +209,7 @@ Defined in `requirements.md` and marked `(post-v1)` on the defining line. Not in ## Not yet tagged -42 of 184 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built. +41 of 184 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
Show untagged requirements @@ -224,7 +225,6 @@ Defined in `requirements.md` and marked `(post-v1)` on the defining line. Not in - FR-MRG-10 - FR-MRG-11 - FR-MRG-2 -- FR-MRG-3 - FR-MRG-4 - FR-MRG-5 - FR-MRG-6