A hand-rolled 64×48 LinearRaw DNG — one IFD, 16-bit RGB, DNGVersion, ColorMatrix1, AsShotNeutral — comes back through rawler 0.7 with cpp 3, the samples in the order written and the matrix parsed into the camera definition; CameraProfile::extract builds a profile from it. ImageMagick reads the same bytes. dr_decode::decode currently accepts the file as CFA and passes three times the samples on, so the cpp == 3 branch is the decode work FR-MRG-3 needs, and the only decode work. panorama.md §8 records the result.
221 lines
7.9 KiB
Rust
221 lines
7.9 KiB
Rust
//! 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 [-- <out.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::<Vec<_>>(),
|
||
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<u8> {
|
||
// Pixels first, so their offset is known: a ramp with one marker pixel.
|
||
let mut pixels: Vec<u16> = 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<u8> = 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<u8> = 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<u8>, 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
|
||
}
|