mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 19:47:35 -06:00
- LJ92 (T.81 process 14) decoder + Annex K encoder: predictors 1-7, 1-4 components, 2-16 bit, point transform, restart intervals - DNG: uncompressed (packed 1-16 bit, 16/24/32-bit float), LJ92 tiles/ strips, Deflate with predictors 2/3/34892-34895; CFA + LinearRaw; linearization, BlackLevelRepeatDim/Delta, WhiteLevel, ActiveArea, DefaultCrop; colour tags; OpcodeList1-3 parse/write/apply - DNG writer (uncompressed / LJ92 tiles) for export and round trips - demosaic: bilinear, PPG, AHD (Hirakawa-Parks), X-Trans directional - highlight clip/reconstruct; DNG colour model with dual illuminant, forward matrices, as-shot neutral -> xy, Rec.2020 transform - embedded preview extraction; malformed-input proptests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
7.9 KiB
Rust
168 lines
7.9 KiB
Rust
//! DNG (Adobe Digital Negative Specification 1.7): raw IFD selection, pixel data (via [`crate::tiffraw`]),
|
|
//! linearization, black/white levels, active area, default crop, CFA description, colour tags, opcode lists.
|
|
|
|
use crate::tiffraw::{Packing, read_image};
|
|
use crate::{BlackLevel, Cfa, ColorData, Mat3, RawData, RawError, RawFormat, RawImage, Rect, Result, opcodes};
|
|
use lightcraft_color::Xy;
|
|
use lightcraft_geom::Orientation;
|
|
use lightcraft_tiff::tags::{self as t, photometric};
|
|
use lightcraft_tiff::{Ifd, Tiff};
|
|
|
|
/// The main raw IFD: full-resolution (NewSubfileType 0) CFA or LinearRaw image with the most pixels.
|
|
pub(crate) fn raw_ifd(tiff: &Tiff) -> Option<&Ifd> {
|
|
tiff.all_ifds()
|
|
.into_iter()
|
|
.filter(|i| i.u32(t::NEW_SUBFILE_TYPE).unwrap_or(0) == 0)
|
|
.filter(|i| matches!(i.u16(t::PHOTOMETRIC), Some(photometric::CFA) | Some(photometric::LINEAR_RAW)))
|
|
.max_by_key(|i| i.u64(t::IMAGE_WIDTH).unwrap_or(0) * i.u64(t::IMAGE_LENGTH).unwrap_or(0))
|
|
}
|
|
|
|
fn mat3(v: Option<Vec<f64>>) -> Option<Mat3> {
|
|
let v = v?;
|
|
if v.len() != 9 || v.iter().any(|x| !x.is_finite()) {
|
|
return None;
|
|
}
|
|
let m = Mat3([[v[0], v[1], v[2]], [v[3], v[4], v[5]], [v[6], v[7], v[8]]]);
|
|
(m.determinant().abs() > 1e-12).then_some(m)
|
|
}
|
|
|
|
fn vec3(v: Option<Vec<f64>>) -> Option<[f64; 3]> {
|
|
let v = v?;
|
|
(v.len() == 3 && v.iter().all(|x| x.is_finite() && *x > 0.0)).then(|| [v[0], v[1], v[2]])
|
|
}
|
|
|
|
/// Colour tags: DNG puts them in IFD0, but some writers use the raw IFD; prefer the raw IFD.
|
|
pub(crate) fn color_data(ifd0: &Ifd, raw: &Ifd) -> ColorData {
|
|
let get = |tag: u16| raw.f64s(tag).or_else(|| ifd0.f64s(tag));
|
|
let geti = |tag: u16| raw.u16(tag).or_else(|| ifd0.u16(tag));
|
|
ColorData {
|
|
illuminant: [geti(t::CALIBRATION_ILLUMINANT_1).unwrap_or(0), geti(t::CALIBRATION_ILLUMINANT_2).unwrap_or(0)],
|
|
color_matrix: [mat3(get(t::COLOR_MATRIX_1)), mat3(get(t::COLOR_MATRIX_2))],
|
|
forward_matrix: [mat3(get(t::FORWARD_MATRIX_1)), mat3(get(t::FORWARD_MATRIX_2))],
|
|
camera_calibration: [mat3(get(t::CAMERA_CALIBRATION_1)), mat3(get(t::CAMERA_CALIBRATION_2))],
|
|
analog_balance: vec3(get(t::ANALOG_BALANCE)),
|
|
as_shot_neutral: vec3(get(t::AS_SHOT_NEUTRAL)),
|
|
as_shot_white_xy: get(t::AS_SHOT_WHITE_XY).filter(|v| v.len() == 2 && v[0] > 0.0 && v[1] > 0.0).map(|v| Xy::new(v[0], v[1])),
|
|
baseline_exposure: get(t::BASELINE_EXPOSURE).and_then(|v| v.first().copied()).filter(|v| v.is_finite()).unwrap_or(0.0)
|
|
+ get(t::BASELINE_EXPOSURE_OFFSET).and_then(|v| v.first().copied()).filter(|v| v.is_finite()).unwrap_or(0.0),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
|
let tiff = Tiff::parse(bytes)?;
|
|
let ifd0 = &tiff.ifds[0];
|
|
let raw = raw_ifd(&tiff).ok_or_else(|| RawError::Corrupt("DNG without a raw image IFD".into()))?;
|
|
let info = raw.image()?;
|
|
let (w, h, cpp) = (info.width as usize, info.height as usize, info.samples_per_pixel as usize);
|
|
if !(1..=4).contains(&cpp) {
|
|
return Err(RawError::Unsupported(format!("{cpp} samples per pixel")));
|
|
}
|
|
if info.compression == t::compression::LOSSY_JPEG {
|
|
return Err(RawError::Unsupported("lossy (DCT) JPEG DNG".into()));
|
|
}
|
|
if info.compression == t::compression::JPEG_XL {
|
|
return Err(RawError::Unsupported("JPEG XL DNG".into()));
|
|
}
|
|
let mut data = read_image(bytes, &info, tiff.order, Packing::Msb)?;
|
|
let bits = info.bits() as u32;
|
|
|
|
// linearization table (integer data only)
|
|
let mut linearized = false;
|
|
if let (RawData::U16(v), Some(table)) = (&mut data, raw.u64s(t::LINEARIZATION_TABLE).filter(|t| !t.is_empty())) {
|
|
let table: Vec<u16> = table.iter().map(|&x| x.min(65535) as u16).collect();
|
|
let last = table.len() - 1;
|
|
v.iter_mut().for_each(|s| *s = table[(*s as usize).min(last)]);
|
|
linearized = true;
|
|
}
|
|
|
|
let float = matches!(data, RawData::F32(_));
|
|
let default_white = if float { 1.0 } else { ((1u64 << bits.min(16)) - 1) as f32 };
|
|
let mut white: Vec<f32> = raw.f64s(t::WHITE_LEVEL).unwrap_or_default().into_iter().map(|v| v as f32).filter(|v| *v > 0.0).collect();
|
|
if white.is_empty() {
|
|
white = vec![default_white];
|
|
}
|
|
|
|
let dim = raw.u64s(t::BLACK_LEVEL_REPEAT_DIM).unwrap_or_default();
|
|
let (br, bc) = match dim.as_slice() {
|
|
[r, c] if (1..=16).contains(r) && (1..=16).contains(c) => (*r as usize, *c as usize),
|
|
_ => (1, 1),
|
|
};
|
|
let mut values: Vec<f32> = raw.f64s(t::BLACK_LEVEL).unwrap_or_default().into_iter().map(|v| v as f32).collect();
|
|
if values.len() != br * bc * cpp {
|
|
values = vec![values.first().copied().unwrap_or(0.0)];
|
|
}
|
|
let (br, bc) = if values.len() == 1 { (1, 1) } else { (br, bc) };
|
|
let floats = |tag| raw.f64s(tag).unwrap_or_default().into_iter().map(|v| v as f32).collect::<Vec<f32>>();
|
|
let black =
|
|
BlackLevel { repeat_rows: br, repeat_cols: bc, values, delta_h: floats(t::BLACK_LEVEL_DELTA_H), delta_v: floats(t::BLACK_LEVEL_DELTA_V) };
|
|
|
|
let active_area = match raw.u64s(t::ACTIVE_AREA).as_deref() {
|
|
Some([top, left, bottom, right]) if bottom > top && right > left && (*bottom as usize) <= h && (*right as usize) <= w => {
|
|
Rect::new(*left as usize, *top as usize, (*right - *left) as usize, (*bottom - *top) as usize)
|
|
}
|
|
_ => Rect::new(0, 0, w, h),
|
|
};
|
|
let crop = match (raw.f64s(t::DEFAULT_CROP_ORIGIN).as_deref(), raw.f64s(t::DEFAULT_CROP_SIZE).as_deref()) {
|
|
(Some([x, y]), Some([cw, ch])) if *cw >= 1.0 && *ch >= 1.0 && *x >= 0.0 && *y >= 0.0 => {
|
|
Rect::new(x.round() as usize, y.round() as usize, cw.round() as usize, ch.round() as usize).clipped(active_area.width, active_area.height)
|
|
}
|
|
_ => Rect::new(0, 0, active_area.width, active_area.height),
|
|
};
|
|
|
|
let cfa = if info.photometric == photometric::CFA {
|
|
if raw.u16(t::CFA_LAYOUT).unwrap_or(1) != 1 {
|
|
return Err(RawError::Unsupported("non-rectangular CFA layout".into()));
|
|
}
|
|
let dim = raw.u64s(t::CFA_REPEAT_PATTERN_DIM).unwrap_or_else(|| vec![2, 2]);
|
|
let (rows, cols) = match dim.as_slice() {
|
|
[r, c] if (1..=16).contains(r) && (1..=16).contains(c) => (*r as usize, *c as usize),
|
|
_ => return Err(RawError::Corrupt("bad CFARepeatPatternDim".into())),
|
|
};
|
|
let pat = raw.bytes(t::CFA_PATTERN_EP).ok_or_else(|| RawError::Corrupt("missing CFAPattern".into()))?;
|
|
if pat.len() != rows * cols {
|
|
return Err(RawError::Corrupt("CFAPattern size mismatch".into()));
|
|
}
|
|
let planes = raw.bytes(t::CFA_PLANE_COLOR).map(|b| b.to_vec()).unwrap_or_else(|| vec![0, 1, 2]);
|
|
let pattern: Vec<u8> = pat
|
|
.iter()
|
|
.map(|c| planes.iter().position(|p| p == c).map(|i| i as u8).filter(|&i| i < 3))
|
|
.collect::<Option<_>>()
|
|
.ok_or_else(|| RawError::Unsupported("CFA with more than three colours".into()))?;
|
|
if cpp != 1 {
|
|
return Err(RawError::Unsupported("CFA data with several samples per pixel".into()));
|
|
}
|
|
Some(Cfa { width: cols, height: rows, pattern })
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let opcode_list = |tag| raw.bytes(tag).map(opcodes::parse_list).unwrap_or_default();
|
|
let opcodes =
|
|
crate::OpcodeLists { list1: opcode_list(t::OPCODE_LIST_1), list2: opcode_list(t::OPCODE_LIST_2), list3: opcode_list(t::OPCODE_LIST_3) };
|
|
|
|
let mut metadata = lightcraft_meta::from_tiff(&tiff);
|
|
metadata.width = Some(crop.width as u32);
|
|
metadata.height = Some(crop.height as u32);
|
|
let img = RawImage {
|
|
format: RawFormat::Dng,
|
|
width: w,
|
|
height: h,
|
|
cpp,
|
|
data,
|
|
cfa,
|
|
bits,
|
|
black,
|
|
white,
|
|
active_area,
|
|
crop,
|
|
orientation: Orientation::from_exif(ifd0.u16(t::ORIENTATION).unwrap_or(1)),
|
|
color: color_data(ifd0, raw),
|
|
wb_multipliers: None,
|
|
linearized,
|
|
opcodes,
|
|
metadata,
|
|
};
|
|
img.validate()?;
|
|
Ok(img)
|
|
}
|