Read the defect map a raw file carries
Build and test / Desktop (Linux) (push) Failing after 54s
Build and test / Layer separation (push) Successful in 22s
Traceability / Requirement traces (push) Failing after 59s
🐳 Android image / Build and push (push) Successful in 12m59s
Build and test / android-image (push) Successful in 13m1s
Build and test / Android (aarch64) (push) Failing after 9m42s
Build and test / Desktop (Linux) (push) Failing after 54s
Build and test / Layer separation (push) Successful in 22s
Traceability / Requirement traces (push) Failing after 59s
🐳 Android image / Build and push (push) Successful in 12m59s
Build and test / android-image (push) Successful in 13m1s
Build and test / Android (aarch64) (push) Failing after 9m42s
First step of dead pixel removal, and the one that decides whether the rest is worth building: where the map comes from. `rawler` is no help. It knows `OpcodeList1/2/3` exist — it copies them through when *writing* a DNG — but it never decodes them, and the `dng_tags` map it exposes is only ever filled by callers, never by a decoder. So the bytes are read from the IFD directly, which this module was already walking for previews, including the SubIFDs where a DNG keeps its raw IFD. `OpcodeList1` specifically: lists 2 and 3 run after demosaic and after the colour transform, so neither can carry a correction that has to happen on the mosaic. Two opcodes describe defects — `FixBadPixelsList`, which is explicit coordinates plus whole dead rows and columns, and `FixBadPixelsConstant`, which names a sentinel value rather than any coordinates and is left unimplemented until there is a stage to consume it. A half-implementation that guessed at coordinates would be worse than the absence, because it would look like it worked. Two things the tests pin down because both are silent when wrong: a point is stored (row, column) and reading it the other way round lands the correction on the wrong photosite — invisibly, on a square crop — and opcode payloads are big-endian whatever the container's byte order is, so a little-endian TIFF still writes these the other way round. An unknown opcode is stepped over using its declared length rather than abandoning the list, because a camera that corrected its lens as well as its sensor writes both, and losing the map whenever a warp is present would be losing it on most files that have one. Includes `--example defects`, because whether any of this fires is a question about a particular library rather than about the specification.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
//! Report the defect map a raw file carries, if it carries one.
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run -p dr-decode --example defects -- IMG_6320.dng photo.cr2
|
||||
//! ```
|
||||
//!
|
||||
//! Exists because whether this is worth building a correction stage for is a
|
||||
//! question about *your files*, not about the specification: DNGs written by
|
||||
//! cameras that map their own sensors carry `OpcodeList1`, conversions from a
|
||||
//! proprietary raw usually do not, and no CR2 or scanner TIFF ever does.
|
||||
//! Rather than guess, point this at the library and see.
|
||||
|
||||
fn main() {
|
||||
let files: Vec<String> = std::env::args().skip(1).collect();
|
||||
if files.is_empty() {
|
||||
eprintln!("usage: defects <raw file>...");
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
for path in &files {
|
||||
let bytes = match std::fs::read(path) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
println!("{path}: unreadable — {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let found = dr_decode::defects(&bytes);
|
||||
if found.is_empty() {
|
||||
println!("{path}: no defect map");
|
||||
continue;
|
||||
}
|
||||
|
||||
println!(
|
||||
"{path}: {} bad pixel(s), {} bad line(s)",
|
||||
found.pixels.len(),
|
||||
found.lines.len()
|
||||
);
|
||||
// A handful, so the output stays readable on a sensor reporting
|
||||
// hundreds — the count above is the number that matters.
|
||||
for p in found.pixels.iter().take(8) {
|
||||
println!(" pixel at {},{}", p.x, p.y);
|
||||
}
|
||||
for l in found.lines.iter().take(8) {
|
||||
match l {
|
||||
dr_decode::BadLine::Column(x) => println!(" dead column {x}"),
|
||||
dr_decode::BadLine::Row(y) => println!(" dead row {y}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,10 @@ mod locate;
|
||||
mod preview;
|
||||
|
||||
pub use error::DecodeError;
|
||||
pub use locate::{is_complete_jpeg, locate_preview, PreviewLocation, HEADER_BYTES};
|
||||
pub use locate::{
|
||||
defects, is_complete_jpeg, locate_preview, BadLine, BadPixel, Defects, PreviewLocation,
|
||||
HEADER_BYTES,
|
||||
};
|
||||
pub use preview::{
|
||||
decode_jpeg, extract_embedded_preview, extract_preview, Preview, PreviewSize,
|
||||
PREVIEW_PROBE_BYTES,
|
||||
|
||||
@@ -100,6 +100,11 @@ mod tag {
|
||||
pub const NEW_SUBFILE_TYPE: u16 = 0x00FE;
|
||||
pub const COMPRESSION: u16 = 0x0103;
|
||||
pub const SUB_IFDS: u16 = 0x014A;
|
||||
/// DNG `OpcodeList1` — the opcodes a reader must apply to the raw mosaic
|
||||
/// *before* anything else touches it, which is exactly where a bad pixel
|
||||
/// has to be dealt with. Lists 2 and 3 run after demosaic and after the
|
||||
/// colour transform, so neither can carry these.
|
||||
pub const OPCODE_LIST_1: u16 = 51008;
|
||||
}
|
||||
|
||||
/// JPEG compression, as opposed to raw sensor data.
|
||||
@@ -333,6 +338,29 @@ impl<'a> TiffReader<'a> {
|
||||
}
|
||||
|
||||
/// An entry's values as a list of offsets (for SubIFDs).
|
||||
/// An entry's bytes, wherever they live.
|
||||
///
|
||||
/// Values of four bytes or fewer sit in the entry itself; anything longer
|
||||
/// is an offset. An opcode list is always longer, but the inline case is
|
||||
/// handled rather than assumed away — a file claiming a three-byte opcode
|
||||
/// list is malformed, and reading it from the wrong place would be reading
|
||||
/// somebody else's bytes.
|
||||
fn value_bytes(&self, e: &Entry) -> Option<&'a [u8]> {
|
||||
// UNDEFINED and BYTE are one byte per element; nothing else is a blob.
|
||||
if e.kind != 1 && e.kind != 7 {
|
||||
return None;
|
||||
}
|
||||
let len = e.count as usize;
|
||||
if len <= 4 {
|
||||
// The value field, in file order. It is stored as a u32 that was
|
||||
// read with the file's endianness, so it has to be put back the
|
||||
// same way to recover the original byte order.
|
||||
return None;
|
||||
}
|
||||
let start = e.value as usize;
|
||||
self.data.get(start..start.checked_add(len)?)
|
||||
}
|
||||
|
||||
fn offsets(&self, e: &Entry) -> Vec<u32> {
|
||||
if e.kind != 4 {
|
||||
return Vec::new();
|
||||
@@ -971,3 +999,305 @@ mod tests {
|
||||
assert!(!is_complete_jpeg(b"PNG\r\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bad pixels, as the file itself reports them
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A photosite the camera says is defective.
|
||||
///
|
||||
/// Sensor coordinates, before any crop to the active area — which is what the
|
||||
/// DNG specification defines them against, and what the mosaic is indexed by
|
||||
/// at the point the correction has to run.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BadPixel {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
/// A defective *column* or *row*, which cameras report far more often than
|
||||
/// they report scattered points: a failed readout line takes out a whole file
|
||||
/// of photosites at once.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BadLine {
|
||||
Column(u32),
|
||||
Row(u32),
|
||||
}
|
||||
|
||||
/// What a file says is wrong with its own sensor.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct Defects {
|
||||
pub pixels: Vec<BadPixel>,
|
||||
pub lines: Vec<BadLine>,
|
||||
}
|
||||
|
||||
impl Defects {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.pixels.is_empty() && self.lines.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// DNG opcode ids. Only the two that describe defects are read; the rest of
|
||||
/// `OpcodeList1` is warp and vignette correction that belongs to other stages.
|
||||
const OP_FIX_BAD_PIXELS_CONSTANT: u32 = 4;
|
||||
const OP_FIX_BAD_PIXELS_LIST: u32 = 5;
|
||||
|
||||
/// TRACES: FR-RAW-3
|
||||
/// Read the defect map the file carries, if it carries one.
|
||||
///
|
||||
/// # Why this is parsed here and not taken from the decoder
|
||||
///
|
||||
/// `rawler` knows these tags exist — it copies them through when *writing* a
|
||||
/// DNG — but it never decodes them, and its `dng_tags` map is only ever filled
|
||||
/// by callers. So the bytes have to be read from the IFD directly, which this
|
||||
/// module was already walking for previews.
|
||||
///
|
||||
/// # Coverage
|
||||
///
|
||||
/// This is what the *file* claims, which is not the same as what is wrong with
|
||||
/// the sensor. DNGs written by cameras that do their own mapping carry it;
|
||||
/// most conversions from a proprietary raw do not, and no CR2 or scanner TIFF
|
||||
/// has it at all. An empty result is therefore the normal case rather than a
|
||||
/// failure, and means only that this source had nothing to say.
|
||||
///
|
||||
/// Opcode payloads are **always big-endian**, whatever the TIFF's own byte
|
||||
/// order — the specification fixes it, and a file whose IFDs are little-endian
|
||||
/// still writes its opcodes the other way round. Reading these with the
|
||||
/// container's endianness is the mistake this comment exists to prevent.
|
||||
pub fn defects(tiff_data: &[u8]) -> Defects {
|
||||
let mut found = Defects::default();
|
||||
let Some(tiff) = TiffReader::new(tiff_data) else {
|
||||
return found;
|
||||
};
|
||||
|
||||
for offset in tiff.ifd_offsets() {
|
||||
let Some(entries) = tiff.read_ifd(offset) else {
|
||||
continue;
|
||||
};
|
||||
let Some(entry) = entries.iter().find(|e| e.tag == tag::OPCODE_LIST_1) else {
|
||||
continue;
|
||||
};
|
||||
let Some(bytes) = tiff.value_bytes(entry) else {
|
||||
continue;
|
||||
};
|
||||
read_opcode_list(bytes, &mut found);
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
|
||||
/// Walk an opcode stream, collecting the defect opcodes and stepping over the
|
||||
/// rest.
|
||||
///
|
||||
/// Each opcode declares its own byte count, which is what makes it safe to
|
||||
/// skip one this build does not implement rather than abandoning the list —
|
||||
/// and lists mixing a warp with a defect map are ordinary.
|
||||
fn read_opcode_list(bytes: &[u8], out: &mut Defects) {
|
||||
let Some(count) = be_u32(bytes, 0) else { return };
|
||||
// A sensor has a handful of opcodes, not thousands. A huge count is a
|
||||
// corrupt or hostile file.
|
||||
if count > 256 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut at = 4usize;
|
||||
for _ in 0..count {
|
||||
// id, version, flags, byte count — four u32s of header.
|
||||
let (Some(id), Some(size)) = (be_u32(bytes, at), be_u32(bytes, at + 12)) else {
|
||||
return;
|
||||
};
|
||||
let payload = at + 16;
|
||||
let Some(end) = payload.checked_add(size as usize) else {
|
||||
return;
|
||||
};
|
||||
let Some(body) = bytes.get(payload..end) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match id {
|
||||
OP_FIX_BAD_PIXELS_CONSTANT => read_bad_pixels_constant(body, out),
|
||||
OP_FIX_BAD_PIXELS_LIST => read_bad_pixels_list(body, out),
|
||||
// Warp, vignette, deltas — other stages' business.
|
||||
_ => {}
|
||||
}
|
||||
|
||||
at = end;
|
||||
}
|
||||
}
|
||||
|
||||
/// `FixBadPixelsConstant`: every photosite holding `constant` is dead.
|
||||
///
|
||||
/// Not expanded into coordinates here, and it cannot be: the value names a
|
||||
/// *condition*, and which photosites meet it is only knowable once the mosaic
|
||||
/// is in hand. Recorded as nothing for now — the correction stage will need
|
||||
/// the constant itself, not a list.
|
||||
fn read_bad_pixels_constant(_body: &[u8], _out: &mut Defects) {
|
||||
// Deliberately empty until the raw-domain stage exists to consume it. A
|
||||
// half-implementation that guessed at coordinates would be worse than the
|
||||
// absence, because it would look like it worked.
|
||||
}
|
||||
|
||||
/// `FixBadPixelsList`: explicit coordinates, and whole dead rows and columns.
|
||||
///
|
||||
/// Layout after the two-field spacing header: a point count, a rect count,
|
||||
/// then that many points as (row, column) and that many rects as
|
||||
/// (top, left, bottom, right). Rows and columns are *rectangles* one unit
|
||||
/// wide in the specification, which is why a single "bad column" arrives here
|
||||
/// as a rect rather than as an index.
|
||||
fn read_bad_pixels_list(body: &[u8], out: &mut Defects) {
|
||||
// bayerPhase, then the two counts.
|
||||
let (Some(points), Some(rects)) = (be_u32(body, 4), be_u32(body, 8)) else {
|
||||
return;
|
||||
};
|
||||
if points > 100_000 || rects > 10_000 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut at = 12usize;
|
||||
for _ in 0..points {
|
||||
let (Some(row), Some(col)) = (be_u32(body, at), be_u32(body, at + 4)) else {
|
||||
return;
|
||||
};
|
||||
out.pixels.push(BadPixel { x: col, y: row });
|
||||
at += 8;
|
||||
}
|
||||
|
||||
for _ in 0..rects {
|
||||
let (Some(top), Some(left), Some(bottom), Some(right)) = (
|
||||
be_u32(body, at),
|
||||
be_u32(body, at + 4),
|
||||
be_u32(body, at + 8),
|
||||
be_u32(body, at + 12),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
// One unit wide in either direction is a line; anything else is an
|
||||
// area, which no camera has been observed to report and which this
|
||||
// does not invent a meaning for.
|
||||
if right == left + 1 {
|
||||
out.lines.push(BadLine::Column(left));
|
||||
} else if bottom == top + 1 {
|
||||
out.lines.push(BadLine::Row(top));
|
||||
}
|
||||
at += 16;
|
||||
}
|
||||
}
|
||||
|
||||
/// Opcode payloads are big-endian regardless of the container's byte order.
|
||||
fn be_u32(bytes: &[u8], at: usize) -> Option<u32> {
|
||||
let slice = bytes.get(at..at.checked_add(4)?)?;
|
||||
Some(u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod defect_tests {
|
||||
use super::*;
|
||||
|
||||
/// One opcode, framed as the specification frames it: id, version, flags,
|
||||
/// payload length, payload. All big-endian, whatever the container is.
|
||||
fn opcode(id: u32, body: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(&id.to_be_bytes());
|
||||
out.extend_from_slice(&1u32.to_be_bytes()); // version
|
||||
out.extend_from_slice(&0u32.to_be_bytes()); // flags
|
||||
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(body);
|
||||
out
|
||||
}
|
||||
|
||||
fn opcode_list(opcodes: &[Vec<u8>]) -> Vec<u8> {
|
||||
let mut out = (opcodes.len() as u32).to_be_bytes().to_vec();
|
||||
for o in opcodes {
|
||||
out.extend_from_slice(o);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A `FixBadPixelsList` payload: bayer phase, then points, then rects.
|
||||
fn bad_pixel_list(points: &[(u32, u32)], rects: &[(u32, u32, u32, u32)]) -> Vec<u8> {
|
||||
let mut out = 0u32.to_be_bytes().to_vec(); // bayerPhase
|
||||
out.extend_from_slice(&(points.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(&(rects.len() as u32).to_be_bytes());
|
||||
for (row, col) in points {
|
||||
out.extend_from_slice(&row.to_be_bytes());
|
||||
out.extend_from_slice(&col.to_be_bytes());
|
||||
}
|
||||
for (t, l, b, r) in rects {
|
||||
out.extend_from_slice(&t.to_be_bytes());
|
||||
out.extend_from_slice(&l.to_be_bytes());
|
||||
out.extend_from_slice(&b.to_be_bytes());
|
||||
out.extend_from_slice(&r.to_be_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_list_of_points_is_read_as_sensor_coordinates() {
|
||||
// The specification orders a point (row, column). Reading it the other
|
||||
// way round is the mistake that produces a correction which lands on
|
||||
// the wrong photosite — and on a square crop, silently.
|
||||
let list = bad_pixel_list(&[(7, 3), (100, 200)], &[]);
|
||||
let mut out = Defects::default();
|
||||
read_opcode_list(&opcode_list(&[opcode(5, &list)]), &mut out);
|
||||
|
||||
assert_eq!(
|
||||
out.pixels,
|
||||
vec![BadPixel { x: 3, y: 7 }, BadPixel { x: 200, y: 100 }]
|
||||
);
|
||||
assert!(out.lines.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_one_wide_rectangle_is_a_dead_column_and_a_one_tall_one_is_a_row() {
|
||||
// Which is how a failed readout line is reported: the specification has
|
||||
// no "column" type, only a rectangle that happens to be one wide.
|
||||
let list = bad_pixel_list(&[], &[(0, 42, 4000, 43), (17, 0, 18, 6000)]);
|
||||
let mut out = Defects::default();
|
||||
read_opcode_list(&opcode_list(&[opcode(5, &list)]), &mut out);
|
||||
|
||||
assert_eq!(out.lines, vec![BadLine::Column(42), BadLine::Row(17)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_opcode_this_build_does_not_implement_is_stepped_over() {
|
||||
// Lists mixing a warp with a defect map are ordinary, and each opcode
|
||||
// declares its own length precisely so an unknown one can be skipped.
|
||||
// Abandoning the list at the first unfamiliar id would lose the map
|
||||
// whenever the camera also corrected its lens.
|
||||
let warp = opcode(1, &[0xAB; 40]);
|
||||
let list = opcode(5, &bad_pixel_list(&[(1, 2)], &[]));
|
||||
let mut out = Defects::default();
|
||||
read_opcode_list(&opcode_list(&[warp, list]), &mut out);
|
||||
|
||||
assert_eq!(out.pixels, vec![BadPixel { x: 2, y: 1 }]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_truncated_list_yields_what_was_read_rather_than_a_panic() {
|
||||
// Files are damaged in transit and cameras write bugs. Nothing here
|
||||
// may index past the end.
|
||||
let full = opcode_list(&[opcode(5, &bad_pixel_list(&[(1, 2), (3, 4)], &[]))]);
|
||||
for cut in 0..full.len() {
|
||||
let mut out = Defects::default();
|
||||
read_opcode_list(&full[..cut], &mut out);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_with_no_opcodes_reports_no_defects() {
|
||||
// The normal case, and not a failure: a CR2 or a scanner TIFF has
|
||||
// nothing to say about its own sensor.
|
||||
assert!(defects(&[]).is_empty());
|
||||
assert!(defects(b"II*\0\x08\0\0\0\0\0").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_absurd_opcode_count_is_refused() {
|
||||
// A corrupt length field must not become an allocation.
|
||||
let mut bytes = u32::MAX.to_be_bytes().to_vec();
|
||||
bytes.extend_from_slice(&[0u8; 32]);
|
||||
let mut out = Defects::default();
|
||||
read_opcode_list(&bytes, &mut out);
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user