//! 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; 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 }