Merge PR #1475 (rebased)

# Conflicts:
#	crates/raw/examples/rawinfo.rs
#	crates/raw/src/lib.rs
#	crates/raw/src/vendor/arw.rs
#	crates/raw/src/vendor/erf.rs
#	crates/raw/src/vendor/mef.rs
#	crates/raw/src/vendor/nef.rs
#	crates/raw/tests/lossy_dng.rs
#	crates/raw/tests/robust.rs
This commit is contained in:
Brandon Thomas committed 2026-10-15 01:13:36 -04:00
commit 51a784cd95
29 files changed
+102 -12

No files matched your search

+3
View File
@@ -1937,6 +1937,7 @@ mod tests {
color: ColorData::default(),
wb_multipliers: Some([1.0; 3]),
linearized: true,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata: lightcraft_meta::Metadata::default(),
lens_metadata: Default::default(),
@@ -2326,6 +2327,7 @@ mod tests {
color,
wb_multipliers: Some([2.0, 1.0, 1.5]),
linearized: false,
pixel_aspect: 1.0,
opcodes: Default::default(),
metadata: lightcraft_raw::Metadata { make: Some(make.to_owned()), model: Some(model.to_owned()), ..Default::default() },
lens_metadata: Default::default(),
@@ -3540,6 +3542,7 @@ mod tests {
color: ColorData::default(),
wb_multipliers: Some([1.0; 3]),
linearized: true,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata: lightcraft_meta::Metadata { make: Some("SONY".into()), model: Some("ILCE-7M4".into()), ..Default::default() },
lens_metadata: Default::default(),
+2 -4
View File
@@ -159,10 +159,8 @@ pub fn probe_bytes(name: &str, bytes: &[u8]) -> Result<ProbeInfo, String> {
}
Err(Err(e)) => return Err(e),
};
let (mut w, mut h) = (raw.crop.width.max(1) as u32, raw.crop.height.max(1) as u32);
if w <= 1 || h <= 1 {
(w, h) = (raw.active_area.width as u32, raw.active_area.height as u32);
}
let (w, h) = raw.developed_size();
let (mut w, mut h) = (w.max(1) as u32, h.max(1) as u32);
if raw.orientation.swaps_axes() {
std::mem::swap(&mut w, &mut h);
}
+1 -2
View File
@@ -64,8 +64,7 @@ fn frame_crop_factor(camera: Option<f64>, focal: f64, focal_35mm: Option<f64>) -
pub fn for_raw(raw: &lightcraft_raw::RawInfo, meta: impl FnOnce() -> lightcraft_meta::Metadata) -> Option<EmbeddedLens> {
crate::files::embedded_lens(raw).or_else(|| {
let db = database()?;
let c = raw.crop.clipped(raw.active_area.width, raw.active_area.height);
let (w, h) = if c.width == 0 || c.height == 0 { (raw.active_area.width, raw.active_area.height) } else { (c.width, c.height) };
let (w, h) = raw.developed_size();
lookup(db, &meta(), w as f64, h as f64, raw.orientation)
})
}
+1
View File
@@ -584,6 +584,7 @@ pub(crate) fn synthetic_dng_of(w: usize, h: usize, cfa: &str, data: Vec<u16>, xm
},
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -309,6 +309,7 @@ mod tests {
linearized: false,
opcodes: OpcodeLists::default(),
lens_metadata: Default::default(),
pixel_aspect: 1.0,
metadata: Metadata::default(),
}
}
+1
View File
@@ -112,6 +112,7 @@ pub fn write_linear_dng(
color: cd,
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists { list3: lens.to_vec(), ..Default::default() },
metadata: meta,
lens_metadata: Default::default(),
+1
View File
@@ -228,6 +228,7 @@ mod tests {
opcodes: OpcodeLists::default(),
metadata: Default::default(),
lens_metadata: Default::default(),
pixel_aspect: 1.0,
};
let path = dir.path("valid.dng");
std::fs::write(&path, write_dng(&raw, &DngWriteOptions::default()).expect("encode generated fixture")).expect("write fixture");
+2 -1
View File
@@ -24,7 +24,7 @@ impl RawImage {
/// and B; Bayer: `k` even, so that every block has the same layout)?
pub fn can_bin(&self, k: usize) -> bool {
let Some(cfa) = &self.cfa else { return false };
if k < 2 || self.cpp != 1 || !cfa.valid() || (cfa.is_bayer() && !k.is_multiple_of(2)) {
if k < 2 || self.cpp != 1 || (self.pixel_aspect - 1.0).abs() >= 0.01 || !cfa.valid() || (cfa.is_bayer() && !k.is_multiple_of(2)) {
return false;
}
// every window position relative to the pattern
@@ -163,6 +163,7 @@ mod tests {
color: ColorData::default(),
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata: Metadata::default(),
lens_metadata: Default::default(),
+1
View File
@@ -287,6 +287,7 @@ mod tests {
color,
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: Default::default(),
metadata: Default::default(),
lens_metadata: Default::default(),
+18
View File
@@ -338,6 +338,7 @@ pub(crate) fn decode_as(bytes: &[u8], mode: Mode, format: RawFormat) -> Result<R
color: color_data(ifd0, raw, tiff.order),
wb_multipliers: None,
linearized,
pixel_aspect: default_scale_aspect(raw.f64s(t::DEFAULT_SCALE).as_deref()),
opcodes,
metadata,
lens_metadata: Default::default(),
@@ -346,8 +347,25 @@ pub(crate) fn decode_as(bytes: &[u8], mode: Mode, format: RawFormat) -> Result<R
Ok(img)
}
/// Pixel height over width from `DefaultScale` (horizontal, vertical): only a clear 1.5 to 4 ratio counts (the rows stretch; a wider-than-tall pixel is left alone).
fn default_scale_aspect(scale: Option<&[f64]>) -> f32 {
match scale {
Some([h, v]) if *h > 0.0 && *v > 0.0 && (1.5..=4.0).contains(&(v / h)) => (v / h) as f32,
_ => 1.0,
}
}
#[cfg(test)]
mod tests {
#[test]
fn default_scale_makes_tall_pixels() {
assert_eq!(default_scale_aspect(Some(&[1.0, 2.0])), 2.0);
assert_eq!(default_scale_aspect(Some(&[1.0, 1.0])), 1.0);
assert_eq!(default_scale_aspect(Some(&[2.0, 1.0])), 1.0);
assert_eq!(default_scale_aspect(Some(&[1.0, 1.2])), 1.0);
assert_eq!(default_scale_aspect(None), 1.0);
}
use super::*;
/// Store `image` the way DNG 1.7 interleaves it: row `r` of the image is written to stored row
+1
View File
@@ -538,6 +538,7 @@ pub fn write_rendered_dng(
color,
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: crate::OpcodeLists::default(),
metadata: metadata.clone(),
lens_metadata: Default::default(),
+40 -4
View File
@@ -435,13 +435,27 @@ pub struct RawInfo {
/// Lens-correction tables the file records, decoded but not necessarily applied (see [`crate::lens_metadata`]).
#[serde(default)]
pub lens_metadata: LensMetadata,
/// See [`RawImage::pixel_aspect`].
#[serde(default = "square_pixels")]
pub pixel_aspect: f32,
}
fn square_pixels() -> f32 {
1.0
}
/// Rows of a picture `height` rows tall once its pixels (taller than wide by `aspect`) are made square.
pub fn stretched_height(height: usize, aspect: f32) -> usize {
if (aspect - 1.0).abs() < 0.01 { height } else { ((height as f32 * aspect).round() as usize).max(1) }
}
impl RawInfo {
/// Size of the developed image before orientation: the default crop, else the active area.
/// Size of the developed image before orientation: the default crop, else the active area, with the rows
/// stretched for non-square pixels.
pub fn developed_size(&self) -> (usize, usize) {
let c = self.crop.clipped(self.active_area.width, self.active_area.height);
if c.width > 1 && c.height > 1 { (c.width, c.height) } else { (self.active_area.width, self.active_area.height) }
let (w, h) = if c.width > 1 && c.height > 1 { (c.width, c.height) } else { (self.active_area.width, self.active_area.height) };
(w, stretched_height(h, self.pixel_aspect))
}
}
@@ -666,6 +680,10 @@ pub struct RawImage {
pub wb_multipliers: Option<[f32; 3]>,
/// Whether a `LinearizationTable` was applied to `data`.
pub linearized: bool,
/// Height of a sensor pixel over its width (1.0 for square pixels). The D1X and D1H write 2.0: their frame is
/// 4028 x 1324 and the picture is 3:2 only once the rows are doubled. [`Self::develop`] stretches the rows by
/// this factor.
pub pixel_aspect: f32,
pub opcodes: OpcodeLists,
pub metadata: Metadata,
/// Lens-correction tables the file records, decoded but not necessarily applied (see [`crate::lens_metadata`]); only
@@ -704,6 +722,7 @@ impl RawImage {
opcodes: self.opcodes.clone(),
metadata: self.metadata.clone(),
lens_metadata: self.lens_metadata.clone(),
pixel_aspect: self.pixel_aspect,
}
}
@@ -724,6 +743,7 @@ impl RawImage {
opcodes: self.opcodes,
metadata: self.metadata,
lens_metadata: self.lens_metadata,
pixel_aspect: self.pixel_aspect,
}
}
@@ -827,10 +847,13 @@ impl RawImage {
pub fn finish_demosaiced(&self, mut rgb: Rgb32f) -> Rgb32f {
opcodes::apply_list3(&self.opcodes.list3, &mut rgb);
let c = self.develop_crop(rgb.width, rgb.height);
if c.x == 0 && c.y == 0 && c.width == rgb.width && c.height == rgb.height {
let rgb =
if c.x == 0 && c.y == 0 && c.width == rgb.width && c.height == rgb.height { rgb } else { rgb.into_crop(c.x, c.y, c.width, c.height) };
let h = stretched_height(rgb.height, self.pixel_aspect);
if h == rgb.height {
return rgb;
}
rgb.into_crop(c.x, c.y, c.width, c.height)
lightcraft_raster::resample::resize(&rgb, rgb.width, h, lightcraft_raster::resample::Filter::Mitchell)
}
/// The part of a `width × height` demosaiced picture (the active area) that [`Self::develop`] keeps: the default
@@ -889,6 +912,7 @@ mod tests {
color: ColorData::default(),
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata: Metadata::default(),
lens_metadata: Default::default(),
@@ -906,6 +930,17 @@ mod tests {
// a crop that pokes out is clipped to the picture
whole.crop = Rect::new(6, 4, 10, 10);
assert_eq!(whole.develop_crop(8, 6), Rect::new(6, 4, 2, 2));
// tall pixels (D1X: 2.0) are stretched to square ones: the rows double, `developed_size` says so
let mut tall = raw.clone();
tall.pixel_aspect = 2.0;
let d = tall.develop(Method::Bilinear).unwrap();
assert_eq!((d.width, d.height), (5, 8));
assert_eq!(tall.info().developed_size(), (5, 8));
assert_eq!(raw.info().developed_size(), (5, 4));
assert!(!tall.can_bin(2));
assert_eq!(stretched_height(1324, 2.0), 2648);
assert_eq!(stretched_height(1324, 1.0), 1324);
}
/// A maker-note crop (`PEF` AOC / `ORF` ImageProcessing) with an origin of `u64::MAX` used to
@@ -933,6 +968,7 @@ mod tests {
opcodes: OpcodeLists::default(),
metadata: Metadata::default(),
lens_metadata: Default::default(),
pixel_aspect: 1.0,
};
assert!(raw.validate().is_err(), "`x + width` wrapped: the area is outside the image");
assert!(raw.normalized().is_err());
+1
View File
@@ -135,6 +135,7 @@ mod tests {
color: ColorData::default(),
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata: Metadata::default(),
lens_metadata: Default::default(),
+1
View File
@@ -810,6 +810,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
// makes a neutral surface magenta. WB edits remain relative to this as-shot RGB.
wb_multipliers: if linear_rgb { Some([1.0; 3]) } else { wb },
linearized: false,
pixel_aspect: 1.0,
// only validated components become opcodes (see `arw_lens`, and `arw_lens_rm6` for the ILCE-7RM6)
opcodes: OpcodeLists { list3: warp.into_iter().chain(rm6).collect(), ..Default::default() },
metadata,
+1
View File
@@ -249,6 +249,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
color: ColorData::default(),
wb_multipliers: wb,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -391,6 +391,7 @@ pub(super) fn decode(bytes: &[u8], tiff: &Tiff, raw: &Ifd, mode: Mode) -> Result
// the camera's white balance is already applied, as in Sony's linear YCbCr
wb_multipliers: Some([1.0; 3]),
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -253,6 +253,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
color: ColorData::default(),
wb_multipliers: levels.wb,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -124,6 +124,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
pixel_aspect: 1.0,
};
img.validate_for(mode)?;
Ok(img)
+1
View File
@@ -115,6 +115,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
color: ColorData::default(),
wb_multipliers: wb,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -122,6 +122,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
pixel_aspect: 1.0,
};
img.validate_for(mode)?;
Ok(img)
+13 -1
View File
@@ -175,6 +175,16 @@ fn measured_body(model: &str, w: usize, h: usize, bits: u32) -> Option<&'static
MEASURED_BODIES.iter().find(|b| b.model == model.trim() && b.size == (w, h) && b.bits == bits)
}
/// Pixel height over width from the raw IFD's resolution tags: the D1X and D1H write 600 x 300 per inch for a
/// 4028 x 1324 frame, rows twice as tall as the columns are wide. Only ratios from 1.5 to 4 count (a frame
/// squashed 3:1 or 2:1), the rest is rounding or a stray tag.
pub(crate) fn pixel_aspect_of(raw: &Ifd) -> f32 {
match (raw.f64(t::X_RESOLUTION), raw.f64(t::Y_RESOLUTION)) {
(Some(x), Some(y)) if x > 0.0 && y > 0.0 && (1.5..=4.0).contains(&(x / y)) => (x / y).round() as f32,
_ => 1.0,
}
}
fn raw_ifd(tiff: &Tiff) -> Option<&Ifd> {
tiff.all_ifds()
.into_iter()
@@ -268,9 +278,10 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
Some(&MeasuredBody { crop: (x, y, cw, ch), .. }) if x + cw <= active_w && y + ch <= h => Rect::new(x, y, cw, ch),
_ => default_crop(mn.as_ref(), active_w, h),
};
let pixel_aspect = pixel_aspect_of(raw);
let mut metadata = lightcraft_meta::from_tiff(&tiff);
metadata.width = Some(crop.width as u32);
metadata.height = Some(crop.height as u32);
metadata.height = Some(crate::stretched_height(crop.height, pixel_aspect) as u32);
let img = RawImage {
format: RawFormat::Nef,
width: w,
@@ -290,6 +301,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
color: ColorData::default(),
wb_multipliers: wb,
linearized: false,
pixel_aspect,
// Nikon's in-file distortion correction, about the optical centre (which follows the sensor shift)
opcodes: OpcodeLists {
list3: super::nef_lens::distortion(&model, raw, Rect::new(0, 0, active_w, h), crop).into_iter().collect(),
+1
View File
@@ -334,6 +334,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
color: ColorData::default(),
wb_multipliers: wb,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -335,6 +335,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
color: ColorData::default(),
wb_multipliers: wb,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -212,6 +212,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
color: ColorData::default(),
wb_multipliers: wb,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists { list3: super::raf_lens::corrections(&camera_name(bytes), &ifd, active), ..Default::default() },
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -614,6 +614,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
color: ColorData::default(),
wb_multipliers: wb,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists { list3: distortion(ifd0, active).into_iter().collect(), ..Default::default() },
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -184,6 +184,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
color: ColorData::default(),
wb_multipliers: wb,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -59,6 +59,7 @@ fn synthetic(w: usize, h: usize, cfa: Option<Cfa>, cpp: usize) -> RawImage {
color,
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata,
lens_metadata: Default::default(),
+1
View File
@@ -38,6 +38,7 @@ fn mosaic(w: usize, h: usize) -> RawImage {
opcodes: OpcodeLists::default(),
metadata: Default::default(),
lens_metadata: Default::default(),
pixel_aspect: 1.0,
}
}
+2
View File
@@ -24,6 +24,7 @@ fn sample(comp: DngCompression) -> Vec<u8> {
color: ColorData::default(),
wb_multipliers: None,
linearized: false,
pixel_aspect: 1.0,
opcodes: OpcodeLists::default(),
metadata: Default::default(),
lens_metadata: Default::default(),
@@ -165,6 +166,7 @@ fn non_finite_black_levels_and_samples_develop() {
opcodes: OpcodeLists::default(),
metadata: Default::default(),
lens_metadata: Default::default(),
pixel_aspect: 1.0,
};
for img in [raw.clone(), lightcraft_raw::decode(&lightcraft_raw::write_dng(&raw, &DngWriteOptions::default()).unwrap()).unwrap()] {
let rgb = img.develop(Method::Ahd).unwrap();