mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-02 14:39:41 -06:00
Merge PR #1176 (rebased)
This commit is contained in:
@@ -165,6 +165,7 @@ mod tests {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata: Metadata::default(),
|
||||
lens_metadata: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -289,6 +289,7 @@ mod tests {
|
||||
linearized: false,
|
||||
opcodes: Default::default(),
|
||||
metadata: Default::default(),
|
||||
lens_metadata: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -340,6 +340,7 @@ pub(crate) fn decode_as(bytes: &[u8], mode: Mode, format: RawFormat) -> Result<R
|
||||
linearized,
|
||||
opcodes,
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
@@ -540,6 +540,7 @@ pub fn write_rendered_dng(
|
||||
linearized: false,
|
||||
opcodes: crate::OpcodeLists::default(),
|
||||
metadata: metadata.clone(),
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
let order = ByteOrder::Little;
|
||||
write_dng(&raw, &DngWriteOptions { compression: DngCompression::Deflate { tile: 256, half: true }, order, xmp })
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Lens-correction data a raw file records, kept with where it came from and separately from what LightCraft
|
||||
//! applies.
|
||||
//!
|
||||
//! A readable table is not a correction. Each component carries a [`SonyLensStatus`]: only
|
||||
//! [`SonyLensStatus::Applied`] components were converted into the image's `OpcodeList3`, and that is decided by
|
||||
//! independent validation per model, coding and component (see `docs/sony-lens-corrections.md`), never by the mere
|
||||
//! presence of a table. Everything else here is metadata for diagnostics and later validation work.
|
||||
|
||||
use crate::Rect;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Vendor lens-correction metadata of a raw file (see the module docs). Empty for formats that record none, or
|
||||
/// whose tables are read straight into opcodes.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LensMetadata {
|
||||
/// Sony ARW: the raw image IFD's shading, lateral chromatic aberration and distortion records, with their
|
||||
/// encrypted `SR2SubIFD` copies. `None` when neither directory holds any of them.
|
||||
#[serde(default)]
|
||||
pub sony: Option<SonyLensCorrections>,
|
||||
}
|
||||
|
||||
/// Sony's in-file lens-correction record: three independent components, each a camera setting and a radial table.
|
||||
///
|
||||
/// Tag identities and storage types from the ExifTool EXIF / Sony tag-name documentation: raw-IFD `0x7031` / `0x7032`
|
||||
/// (shading, "VignettingCorrection" / "VignettingCorrParams"), `0x7034` / `0x7035` (lateral chromatic aberration),
|
||||
/// `0x7036` / `0x7037` (distortion), and the `SR2SubIFD` tables `0x797d`, `0x7980`, `0x7982`. The `SR2SubIFD` setting
|
||||
/// tags `0x797c`, `0x797f`, `0x7981` are unnamed there; they sit next to the tables and equal the raw-IFD settings
|
||||
/// in every file inspected.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SonyLensCorrections {
|
||||
/// Shading (vignetting): 16 radial samples whose gain function, radius convention and processing stage are not
|
||||
/// established. Opaque signed data.
|
||||
pub shading: SonyComponent<[i16; 16]>,
|
||||
/// Lateral chromatic aberration: two 16-sample radial curves. A public analysis reads them as red, then blue,
|
||||
/// relative to green; that is not validated here.
|
||||
pub chromatic_aberration: SonyComponent<[[i16; 16]; 2]>,
|
||||
/// Distortion: 16 radial samples (see `docs/sony-lens-corrections.md` for the validated interpretation).
|
||||
pub distortion: SonyComponent<[i16; 16]>,
|
||||
/// Whether the encrypted `SR2SubIFD` was found and decrypted. `false`: no copies to fall back on or compare.
|
||||
pub sr2_read: bool,
|
||||
/// The framing and coding the tables have to be interpreted against.
|
||||
pub geometry: SonyLensGeometry,
|
||||
}
|
||||
|
||||
/// One correction component: the camera's setting, its table, and whether LightCraft applies it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SonyComponent<T> {
|
||||
pub setting: SonyField<SonySetting>,
|
||||
/// The table's samples after the leading sample count, which must equal their number.
|
||||
pub table: SonyField<T>,
|
||||
pub status: SonyLensStatus,
|
||||
}
|
||||
|
||||
/// One field read from the raw IFD and from the `SR2SubIFD`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SonyField<T> {
|
||||
/// The value in use: the raw IFD's when it holds a well-formed one, else the `SR2SubIFD`'s when the raw IFD
|
||||
/// lacks the tag. `None` when no copy is usable, and when the raw IFD's copy is malformed: a corrupt field is
|
||||
/// reported, not silently replaced by the other copy. The raw IFD's copy is the one the TIFF reader that decodes
|
||||
/// the image parsed (its first occurrence, within the reader's limits).
|
||||
pub value: Option<T>,
|
||||
pub source: Option<SonySource>,
|
||||
pub raw_ifd: SonyCopy,
|
||||
pub sr2: SonyCopy,
|
||||
/// Whether the two copies are equal, when both are well-formed. The raw IFD's wins when they differ.
|
||||
pub copies_agree: Option<bool>,
|
||||
/// Diagnostics: the tag occurs more than once in the raw IFD / the `SR2SubIFD`. Only the first occurrence is used,
|
||||
/// as the TIFF reader does (the raw IFD's: the first one whose values it could read).
|
||||
#[serde(default)]
|
||||
pub raw_ifd_repeated: bool,
|
||||
#[serde(default)]
|
||||
pub sr2_repeated: bool,
|
||||
}
|
||||
|
||||
/// Where a value was read.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SonySource {
|
||||
/// The plain raw image IFD.
|
||||
RawIfd,
|
||||
/// The encrypted `SR2SubIFD`.
|
||||
Sr2SubIfd,
|
||||
}
|
||||
|
||||
/// How one copy of a field was found.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SonyCopy {
|
||||
/// Not in that directory (or the `SR2SubIFD` could not be read).
|
||||
Absent,
|
||||
/// Present with exactly the supported type and layout.
|
||||
Valid,
|
||||
/// Present, but refused.
|
||||
Rejected(SonyFieldError),
|
||||
}
|
||||
|
||||
/// Why a present field was refused.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SonyFieldError {
|
||||
/// Another TIFF field type than SSHORT (8) for a table, or SHORT (3) / SSHORT for a setting.
|
||||
FieldType(u16),
|
||||
/// Another number of values than the supported layout's: 17 (shading, distortion), 33 (CA) or 1 (setting).
|
||||
Count(u64),
|
||||
/// The leading sample count is not the layout's 16 (32 for CA). Older bodies store 11 (22) samples padded to the
|
||||
/// same length, a layout that is not interpreted; a count of 0 goes with an "unavailable" setting.
|
||||
CountPrefix(i16),
|
||||
/// The values lie outside the file (raw IFD) or the decrypted block (`SR2SubIFD`).
|
||||
OutsideData,
|
||||
/// In the raw IFD but past what the TIFF reader that decodes the image reads (8,192 entries per directory, a
|
||||
/// total value budget), so the image doesn't use it either.
|
||||
PastReaderLimits,
|
||||
/// A setting stored as a negative SSHORT.
|
||||
NegativeSetting(i16),
|
||||
}
|
||||
|
||||
/// A camera correction setting: the stored number and its documented meaning.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct SonySetting {
|
||||
pub value: u16,
|
||||
pub meaning: SonySettingMeaning,
|
||||
}
|
||||
|
||||
/// The documented meaning of a setting value (ExifTool EXIF tag-name documentation). Values are compared whole;
|
||||
/// they are not read as bit masks.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SonySettingMeaning {
|
||||
/// Shading 256, CA 0, distortion 0. The table is still recorded: Off does not mean absent.
|
||||
Off,
|
||||
/// Shading 257 or 272, CA 1, distortion 1.
|
||||
Auto,
|
||||
/// Distortion 17: Auto, fixed by the lens.
|
||||
ForcedAuto,
|
||||
/// "No correction params available": shading 511, CA 255, distortion 255.
|
||||
Unavailable,
|
||||
/// Any other value (e.g. a shading "Low" setting, whose number is not documented). Never treated as Off.
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
/// Whether a component is applied, and if not, why.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SonyLensStatus {
|
||||
/// Converted into the image's `OpcodeList3` (today: ILCE-7RM4A and ILCE-7RM6 Bayer distortion in native 3:2 framing).
|
||||
Applied,
|
||||
/// No usable table.
|
||||
NoTable,
|
||||
/// Decoded only: no independent validation covers this component's rendering semantics on any body (shading,
|
||||
/// lateral CA).
|
||||
NotValidatedComponent,
|
||||
/// Decoded only: the component is validated on other camera models, not on this one.
|
||||
NotValidatedModel,
|
||||
/// Decoded only: the model is validated, but not this coding or framing (linear YCbCr, a non-3:2 crop) or a
|
||||
/// table found only in the `SR2SubIFD`.
|
||||
NotValidatedMode,
|
||||
/// Validated scope, but the table is all zeros: a valid no-op, nothing to apply.
|
||||
Neutral,
|
||||
/// Validated scope, but the conversion refused this table (values outside the validated range, a degenerate
|
||||
/// crop, or a fitted map that misses the table or folds).
|
||||
Rejected,
|
||||
}
|
||||
|
||||
/// The framing and coding evidence for interpreting the tables, all in raw-frame pixels.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SonyLensGeometry {
|
||||
/// The stored raw frame, including any padding (lossless L files are padded).
|
||||
pub storage_width: usize,
|
||||
pub storage_height: usize,
|
||||
/// Sony's crop tags (`0x74c7` / `0x74c8`) as recorded.
|
||||
pub sony_crop: Option<Rect>,
|
||||
/// The raw IFD's `DefaultCropOrigin` / `DefaultCropSize` as recorded.
|
||||
pub default_crop: Option<Rect>,
|
||||
/// The image's crop (the decoder's precedence: Sony's tags, then the default crop, then the recorded size).
|
||||
pub image_crop: Rect,
|
||||
/// The rectangle the validated distortion conversion is centred on and normalised to: the default crop when
|
||||
/// the file has one, else the image crop. Clipped to the frame.
|
||||
pub reference: Rect,
|
||||
/// TIFF compression of the raw image (1 uncompressed, 7 lossless, 32767 ARW2, 32766 ARW6).
|
||||
pub compression: u16,
|
||||
/// Downsized lossless M/S data (linear YCbCr, not sensor CFA).
|
||||
pub linear_ycbcr: bool,
|
||||
/// Both the image crop and the reference are 3:2 within 0.5 %, the only framing validated.
|
||||
pub native_3_2: bool,
|
||||
}
|
||||
|
||||
impl<T> SonyField<T> {
|
||||
/// Whether either directory holds the tag (well-formed or not).
|
||||
pub fn is_present(&self) -> bool {
|
||||
self.raw_ifd != SonyCopy::Absent || self.sr2 != SonyCopy::Absent
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ pub mod highlight;
|
||||
#[cfg(feature = "jxl")]
|
||||
mod jxl;
|
||||
pub mod kelvin_presets;
|
||||
pub mod lens_metadata;
|
||||
pub mod ljpeg;
|
||||
pub(crate) mod llvc;
|
||||
pub mod opcodes;
|
||||
@@ -51,6 +52,7 @@ mod wb_estimate;
|
||||
|
||||
pub use demosaic::{Method, demosaic};
|
||||
pub use dngwrite::{DngCompression, DngWriteOptions, write_dng, write_rendered_dng};
|
||||
pub use lens_metadata::LensMetadata;
|
||||
pub use lightcraft_color::Mat3;
|
||||
pub use lightcraft_geom::Orientation;
|
||||
pub use lightcraft_meta::Metadata;
|
||||
@@ -430,6 +432,9 @@ pub struct RawInfo {
|
||||
pub wb_multipliers: Option<[f32; 3]>,
|
||||
pub opcodes: OpcodeLists,
|
||||
pub metadata: Metadata,
|
||||
/// Lens-correction tables the file records, decoded but not necessarily applied (see [`crate::lens_metadata`]).
|
||||
#[serde(default)]
|
||||
pub lens_metadata: LensMetadata,
|
||||
}
|
||||
|
||||
impl RawInfo {
|
||||
@@ -663,6 +668,10 @@ pub struct RawImage {
|
||||
pub linearized: bool,
|
||||
pub opcodes: OpcodeLists,
|
||||
pub metadata: Metadata,
|
||||
/// Lens-correction tables the file records, decoded but not necessarily applied (see [`crate::lens_metadata`]); only
|
||||
/// validated components are in `opcodes`.
|
||||
#[serde(default)]
|
||||
pub lens_metadata: LensMetadata,
|
||||
}
|
||||
|
||||
/// Black-subtracted, white-normalised data covering the active area.
|
||||
@@ -694,6 +703,7 @@ impl RawImage {
|
||||
wb_multipliers: self.wb_multipliers,
|
||||
opcodes: self.opcodes.clone(),
|
||||
metadata: self.metadata.clone(),
|
||||
lens_metadata: self.lens_metadata.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,6 +723,7 @@ impl RawImage {
|
||||
wb_multipliers: self.wb_multipliers,
|
||||
opcodes: self.opcodes,
|
||||
metadata: self.metadata,
|
||||
lens_metadata: self.lens_metadata,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,6 +891,7 @@ mod tests {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata: Metadata::default(),
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
let direct = raw.develop(Method::Bilinear).unwrap();
|
||||
let by_hand = raw.finish_demosaiced(demosaic(&raw.normalized().unwrap(), Method::Bilinear));
|
||||
@@ -920,6 +932,7 @@ mod tests {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata: Metadata::default(),
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
assert!(raw.validate().is_err(), "`x + width` wrapped: the area is outside the image");
|
||||
assert!(raw.normalized().is_err());
|
||||
|
||||
@@ -137,6 +137,7 @@ mod tests {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata: Metadata::default(),
|
||||
lens_metadata: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+46
-22
@@ -45,8 +45,9 @@
|
||||
//! through the generic TIFF path.
|
||||
|
||||
use super::arw6;
|
||||
use crate::lens_metadata::SonyLensGeometry;
|
||||
use crate::tiffraw::{Packing, check_image, read_image_in};
|
||||
use crate::{BlackLevel, Cfa, ColorData, Mode, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result, ljpeg};
|
||||
use crate::{BlackLevel, Cfa, ColorData, LensMetadata, Mode, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result, ljpeg};
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::image::{Chunk, ImageInfo, Layout, chunk_bytes};
|
||||
use lightcraft_tiff::tags::{self as t, photometric};
|
||||
@@ -553,6 +554,14 @@ fn dng_default_crop(raw: &Ifd, w: usize, h: usize) -> Option<Rect> {
|
||||
None
|
||||
}
|
||||
|
||||
/// A rectangle exactly as an origin tag and a size tag record it (two values each), when both exist and fit `usize`.
|
||||
fn recorded_rect(raw: &Ifd, origin: u16, size: u16) -> Option<Rect> {
|
||||
let (Some(origin), Some(size)) = (raw.u64s(origin), raw.u64s(size)) else { return None };
|
||||
let (&[x, y], &[w, h]) = (origin.as_slice(), size.as_slice()) else { return None };
|
||||
let n = |v: u64| usize::try_from(v).ok();
|
||||
Some(Rect::new(n(x)?, n(y)?, n(w)?, n(h)?))
|
||||
}
|
||||
|
||||
/// The image area for files without Sony's crop tags (`0x74c7/0x74c8`, written since about 2017): the DNG-style
|
||||
/// default crop when the raw IFD has one, else the image size the camera records (see [`recorded_size_crop`]).
|
||||
///
|
||||
@@ -749,6 +758,39 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let mut metadata = lightcraft_meta::from_tiff(&tiff);
|
||||
metadata.width = Some(crop.width as u32);
|
||||
metadata.height = Some(crop.height as u32);
|
||||
// The lens-correction record. The distortion table is centred on the DNG-style default crop when the file has
|
||||
// one: on the ILCE-7RM4A it starts at x = 32 where Sony's crop tags (the image crop, unchanged) start at 0, and
|
||||
// the warp was validated against Sony's exports with that centre. The opcode's centre is in active-area
|
||||
// coordinates, so it stays on the optical centre whichever crop frames the image.
|
||||
let reference = dng_default_crop(raw, w, h).unwrap_or(crop);
|
||||
let geometry = SonyLensGeometry {
|
||||
storage_width: w,
|
||||
storage_height: h,
|
||||
sony_crop: recorded_rect(raw, CROP_TOP_LEFT, CROP_SIZE),
|
||||
default_crop: recorded_rect(raw, t::DEFAULT_CROP_ORIGIN, t::DEFAULT_CROP_SIZE),
|
||||
image_crop: crop,
|
||||
reference,
|
||||
compression: info.compression,
|
||||
linear_ycbcr: linear_rgb,
|
||||
native_3_2: super::arw_lens::is_3_2(crop) && super::arw_lens::is_3_2(reference),
|
||||
};
|
||||
// decrypted for the copies' agreement check too, not only when a plain field is missing: a few dozen kB, found by
|
||||
// reading only three `SR2Private` entries (see `sr2::SubIfd::read`)
|
||||
let (mut sony_lens, warp) = super::arw_lens::corrections(bytes, tiff.order, (raw, tiff.bigtiff), sr2(), &model, geometry);
|
||||
// The ILCE-7RM6's tables follow other conventions, measured against its camera JPEGs ([`super::arw_lens_rm6`]);
|
||||
// its allowlist and `arw_lens`'s are disjoint, so at most one of them produces an opcode. Both the image framing
|
||||
// and the table geometry must stay within the validated 3:2 scope.
|
||||
let rm6: Vec<crate::Opcode> = if linear_rgb || !super::arw_lens::is_3_2(crop) {
|
||||
Vec::new()
|
||||
} else {
|
||||
super::arw_lens_rm6::lens_opcodes(raw, &model, Rect::new(0, 0, w, h), reference)
|
||||
};
|
||||
if !rm6.is_empty()
|
||||
&& let Some(record) = sony_lens.as_mut()
|
||||
{
|
||||
// distortion only: its lateral CA planes are off (see `arw_lens_rm6::lens_opcodes`)
|
||||
record.distortion.status = crate::lens_metadata::SonyLensStatus::Applied;
|
||||
}
|
||||
let img = RawImage {
|
||||
format: RawFormat::Arw,
|
||||
width: w,
|
||||
@@ -768,28 +810,10 @@ 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,
|
||||
// The distortion table is centred on the DNG-style default crop when the file has one: on the ILCE-7RM4A
|
||||
// it starts at x = 32 where Sony's crop tags (the image crop, unchanged) start at 0, and the warp was
|
||||
// validated against Sony's exports with that centre. The opcode's centre is in active-area coordinates, so
|
||||
// it stays on the optical centre whichever crop frames the image. The ILCE-7RM6's tables follow other
|
||||
// conventions, measured against its camera JPEGs ([`super::arw_lens_rm6`]); the two allowlists are
|
||||
// disjoint, so at most one of them produces an opcode.
|
||||
opcodes: OpcodeLists {
|
||||
// DefaultCrop can retain native geometry while Sony's tags select an aspect crop.
|
||||
// Both the image framing and the table geometry must stay within the validated 3:2 scope.
|
||||
list3: if linear_rgb || crop.height == 0 || (crop.width as f64 / crop.height as f64 - 1.5).abs() > 0.005 {
|
||||
Vec::new()
|
||||
} else {
|
||||
let geometry = dng_default_crop(raw, w, h).unwrap_or(crop);
|
||||
let active = Rect::new(0, 0, w, h);
|
||||
super::arw_lens::distortion(&model, raw, active, geometry)
|
||||
.into_iter()
|
||||
.chain(super::arw_lens_rm6::lens_opcodes(raw, &model, active, geometry))
|
||||
.collect()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
// 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,
|
||||
lens_metadata: LensMetadata { sony: sony_lens },
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1055
-46
File diff suppressed because it is too large
Load Diff
Vendored
+1
@@ -251,6 +251,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1
@@ -393,6 +393,7 @@ pub(super) fn decode(bytes: &[u8], tiff: &Tiff, raw: &Ifd, mode: Mode) -> Result
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1
@@ -255,6 +255,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
image.validate_for(mode)?;
|
||||
Ok(image)
|
||||
|
||||
Vendored
+1
@@ -123,6 +123,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1
@@ -117,6 +117,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1
@@ -121,6 +121,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1
@@ -296,6 +296,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
..Default::default()
|
||||
},
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1
@@ -336,6 +336,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1
@@ -337,6 +337,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1
@@ -214,6 +214,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists { list3: super::raf_lens::corrections(&camera_name(bytes), &ifd, active), ..Default::default() },
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+1
@@ -616,6 +616,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists { list3: distortion(ifd0, active).into_iter().collect(), ..Default::default() },
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Vendored
+264
-6
@@ -45,7 +45,7 @@
|
||||
//! of an accident 2⁻⁴⁰⁶⁴). For the DSC-R1's key `63 be 01 3e` the 11932-byte block becomes a well-formed 101-entry
|
||||
//! directory (black `0x7300`, white balance `0x7303`, crop `0x74c3`).
|
||||
|
||||
use lightcraft_tiff::{ByteOrder, Ifd, ParseOptions, tags as t};
|
||||
use lightcraft_tiff::{ByteOrder, EntrySpan, Ifd, ParseOptions, decode_value, directory_entries, entry_span, tags as t};
|
||||
|
||||
/// `SR2Private` tags.
|
||||
const SUBIFD_OFFSET: u16 = 0x7200;
|
||||
@@ -136,11 +136,8 @@ impl SubIfd {
|
||||
/// [`MAX_LEN`].
|
||||
pub(crate) fn read(bytes: &[u8], ifd0: &Ifd, order: ByteOrder) -> Option<Self> {
|
||||
let at = order.read_u32(ifd0.bytes(t::DNG_PRIVATE_DATA)?, 0)?;
|
||||
let opts = ParseOptions { max_ifds: 1, max_depth: 0, follow_children: false, ..Default::default() };
|
||||
let (private, _) = lightcraft_tiff::parse_ifd_at(bytes, u64::from(at), order, 0, false, &opts).ok()?;
|
||||
let key: [u8; 4] = private.bytes(SUBIFD_KEY)?.try_into().ok()?;
|
||||
let start = usize::try_from(private.u64(SUBIFD_OFFSET)?).ok()?;
|
||||
let len = usize::try_from(private.u64(SUBIFD_LENGTH)?).ok()?;
|
||||
let fields = private_fields(bytes, u64::from(at), order);
|
||||
let (start, len, key) = (fields.offset?, fields.length?, fields.key?);
|
||||
if len > MAX_LEN {
|
||||
return None;
|
||||
}
|
||||
@@ -165,6 +162,13 @@ impl SubIfd {
|
||||
self.order
|
||||
}
|
||||
|
||||
/// The decrypted block (the directory from its entry count on) and the file offset it was stored at, which its
|
||||
/// value offsets count from: for readers that check each entry's exact type and count themselves
|
||||
/// (`arw_lens`).
|
||||
pub(crate) fn block(&self) -> (&[u8], usize) {
|
||||
(&self.plain, self.start)
|
||||
}
|
||||
|
||||
/// The field type and value bytes of a SHORT or SSHORT entry; `None` when the tag is absent, has another type
|
||||
/// or its values lie outside the block.
|
||||
pub(crate) fn short_bytes(&self, tag: u16) -> Option<(u16, &[u8])> {
|
||||
@@ -193,9 +197,69 @@ impl SubIfd {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `SR2Private` fields that locate the block.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
struct PrivateFields {
|
||||
offset: Option<usize>,
|
||||
length: Option<usize>,
|
||||
key: Option<[u8; 4]>,
|
||||
}
|
||||
|
||||
/// The block's file offset, length and key from the `SR2Private` directory at `at`, exactly as the TIFF reader would
|
||||
/// give them (`parse_ifd_at` with its default limits, then its `u64` and `bytes` lookups), without decoding the
|
||||
/// directory. The selection uses the reader's own rules: its directory layout and entry limit
|
||||
/// ([`directory_entries`]), which entries it can read ([`entry_span`]) and its value budget; each field is its first
|
||||
/// readable occurrence. Of that entry only the first value is decoded (the key's first five bytes, which settle
|
||||
/// whether it is four bytes), so a hostile directory can't make this allocate memory sized by its other entries,
|
||||
/// which could each alias the whole file, before the block's own [`MAX_LEN`] is checked.
|
||||
fn private_fields(bytes: &[u8], at: u64, order: ByteOrder) -> PrivateFields {
|
||||
let opts = ParseOptions::default();
|
||||
let Ok(dir) = directory_entries(bytes, at, order, false, opts.max_entries) else { return PrivateFields::default() };
|
||||
let mut budget = opts.budget_for(bytes.len());
|
||||
let (mut offset, mut length, mut key) = (None, None, None);
|
||||
for p in dir.positions() {
|
||||
let Some(span) = entry_span(bytes, p, order, false, 0) else { continue };
|
||||
if span.len > budget {
|
||||
continue;
|
||||
}
|
||||
budget -= span.len;
|
||||
match span.tag {
|
||||
SUBIFD_OFFSET if offset.is_none() => offset = Some(first_number(bytes, &span, order)),
|
||||
SUBIFD_LENGTH if length.is_none() => length = Some(first_number(bytes, &span, order)),
|
||||
SUBIFD_KEY if key.is_none() => key = Some(four_bytes(bytes, &span, order)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
PrivateFields { offset: offset.flatten(), length: length.flatten(), key: key.flatten() }
|
||||
}
|
||||
|
||||
/// The entry's first value as the reader's `u64` lookup reads it (an unsigned or non-negative integer), as a `usize`.
|
||||
fn first_number(bytes: &[u8], span: &EntrySpan, order: ByteOrder) -> Option<usize> {
|
||||
if span.count == 0 {
|
||||
return None;
|
||||
}
|
||||
let start = usize::try_from(span.start).ok()?;
|
||||
let first = bytes.get(start..start.checked_add(span.field_type.size())?)?;
|
||||
usize::try_from(decode_value(order, span.field_type, first).get_u64(0)?).ok()
|
||||
}
|
||||
|
||||
/// The entry's bytes as the reader's `bytes` lookup gives them, when they are exactly four. Decoding the first five
|
||||
/// value bytes settles it: BYTE and UNDEFINED give all their bytes, ASCII its text before the first NUL, which
|
||||
/// decoding never makes shorter.
|
||||
fn four_bytes(bytes: &[u8], span: &EntrySpan, order: ByteOrder) -> Option<[u8; 4]> {
|
||||
let start = usize::try_from(span.start).ok()?;
|
||||
let head = bytes.get(start..start.checked_add(usize::try_from(span.len).ok()?.min(5))?)?;
|
||||
<[u8; 4]>::try_from(decode_value(order, span.field_type, head).as_bytes()?).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
/// TIFF field types.
|
||||
const BYTE: u16 = 1;
|
||||
const LONG: u16 = 4;
|
||||
const UNDEFINED: u16 = 7;
|
||||
const IFD: u16 = 13;
|
||||
use lightcraft_tiff::ByteOrder::{Big, Little};
|
||||
|
||||
/// An `SR2SubIFD` block for file offset `start`, encrypted: SHORT/SSHORT entries `(tag, type, values)`, values
|
||||
@@ -345,6 +409,200 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// An `SR2Private` directory of `entries` `(tag, type, count, value field)` at file offset 0.
|
||||
fn private(entries: &[(u16, u16, u32, [u8; 4])], order: ByteOrder) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
order.put_u16(&mut out, entries.len() as u16);
|
||||
for &(tag, kind, count, value) in entries {
|
||||
order.put_u16(&mut out, tag);
|
||||
order.put_u16(&mut out, kind);
|
||||
order.put_u32(&mut out, count);
|
||||
out.extend(value);
|
||||
}
|
||||
order.put_u32(&mut out, 0);
|
||||
out
|
||||
}
|
||||
|
||||
/// `(offset, length, key)` as one of the readers selects them.
|
||||
type Selected = (Option<usize>, Option<usize>, Option<[u8; 4]>);
|
||||
|
||||
/// What `SubIfd::read` on main selects: the TIFF reader's parse of the directory with main's options, then the
|
||||
/// directory's `u64` and `bytes` lookups.
|
||||
fn reader_selects(bytes: &[u8], at: usize, order: ByteOrder) -> Selected {
|
||||
let opts = lightcraft_tiff::ParseOptions { max_ifds: 1, max_depth: 0, follow_children: false, ..Default::default() };
|
||||
let Ok((private, _)) = lightcraft_tiff::parse_ifd_at(bytes, at as u64, order, 0, false, &opts) else { return (None, None, None) };
|
||||
let n = |tag: u16| private.u64(tag).and_then(|v| usize::try_from(v).ok());
|
||||
(n(SUBIFD_OFFSET), n(SUBIFD_LENGTH), private.bytes(SUBIFD_KEY).and_then(|b| <[u8; 4]>::try_from(b).ok()))
|
||||
}
|
||||
|
||||
/// Synthetic `SR2Private` directories at offset 0, with what each tests: filler entries ahead of the fields, and
|
||||
/// unreadable, unusual or valid first occurrences of each field ahead of a valid second one.
|
||||
fn differential_cases(order: ByteOrder) -> Vec<(String, Vec<u8>)> {
|
||||
let w = |v: u32| {
|
||||
let mut b = Vec::new();
|
||||
order.put_u32(&mut b, v);
|
||||
[b[0], b[1], b[2], b[3]]
|
||||
};
|
||||
let h = |a: u16, b: u16| {
|
||||
let mut out = Vec::new();
|
||||
order.put_u16(&mut out, a);
|
||||
order.put_u16(&mut out, b);
|
||||
[out[0], out[1], out[2], out[3]]
|
||||
};
|
||||
let valid = [(SUBIFD_OFFSET, LONG, 1, w(4096)), (SUBIFD_LENGTH, LONG, 1, w(64)), (SUBIFD_KEY, UNDEFINED, 4, KEY)];
|
||||
let mut cases = Vec::new();
|
||||
for fillers in [0usize, 1, 255, 256, 257, 8189, 8190, 8191, 8192, 8193] {
|
||||
let mut entries = vec![(0x7300, LONG, 1, w(7)); fillers];
|
||||
entries.extend(valid);
|
||||
cases.push((format!("{fillers} fillers"), private(&entries, order)));
|
||||
}
|
||||
// a first occurrence ahead of the valid one; pointer values point into a 32-byte tail after the directory
|
||||
let tail_at = 2 + 12 * 4 + 4;
|
||||
let mut tail: Vec<u8> = (1..=32).collect();
|
||||
tail[4..8].copy_from_slice(b"\x11\x22\x33\x44");
|
||||
tail[8] = 0;
|
||||
let numeric: Vec<(&str, u16, u32, [u8; 4])> = vec![
|
||||
("unknown type 0", 0, 1, w(8)),
|
||||
("unknown type 14", 14, 1, w(8)),
|
||||
("unknown type 0xffff", 0xffff, 1, w(8)),
|
||||
("LONG[2] past the file", LONG, 2, w(u32::MAX)),
|
||||
("LONG[2] straddling the end", LONG, 2, w(tail_at as u32 + 28)),
|
||||
("LONG[u32::MAX]", LONG, u32::MAX, w(tail_at as u32)),
|
||||
("DOUBLE[u32::MAX]", 12, u32::MAX, w(tail_at as u32)),
|
||||
("LONG[0]", LONG, 0, w(0)),
|
||||
("LONG[2] in the file", LONG, 2, w(tail_at as u32)),
|
||||
("SHORT[1]", SHORT, 1, h(9, 0)),
|
||||
("SHORT[2] inline", SHORT, 2, h(9, 10)),
|
||||
("BYTE[3]", BYTE, 3, [5, 6, 7, 0]),
|
||||
("SSHORT -1", SSHORT, 1, h(0xffff, 0)),
|
||||
("SSHORT 5", SSHORT, 1, h(5, 0)),
|
||||
("SLONG -1", 9, 1, w(u32::MAX)),
|
||||
("LONG8 at a pointer", 16, 1, w(tail_at as u32)),
|
||||
("RATIONAL", 5, 1, w(tail_at as u32)),
|
||||
("ASCII", 2, 4, *b"abc\0"),
|
||||
("IFD", IFD, 1, w(4096)),
|
||||
];
|
||||
for (tag, other) in [(SUBIFD_OFFSET, [valid[1], valid[2]]), (SUBIFD_LENGTH, [valid[0], valid[2]])] {
|
||||
for (what, kind, count, field) in &numeric {
|
||||
let valid_one = if tag == SUBIFD_OFFSET { valid[0] } else { valid[1] };
|
||||
let mut d = private(&[(tag, *kind, *count, *field), valid_one, other[0], other[1]], order);
|
||||
d.extend(&tail);
|
||||
cases.push((format!("{tag:#x}: first {what}"), d));
|
||||
}
|
||||
}
|
||||
let keys: Vec<(&str, u16, u32, [u8; 4])> = vec![
|
||||
("unknown type", 0, 4, KEY),
|
||||
("UNDEFINED[8] past the file", UNDEFINED, 8, w(u32::MAX)),
|
||||
("UNDEFINED[5] in the file", UNDEFINED, 5, w(tail_at as u32)),
|
||||
("UNDEFINED[3]", UNDEFINED, 3, KEY),
|
||||
("UNDEFINED[0]", UNDEFINED, 0, KEY),
|
||||
("BYTE[4]", BYTE, 4, [1, 2, 3, 4]),
|
||||
("SBYTE[4]", 6, 4, KEY),
|
||||
("ASCII[4]", 2, 4, KEY),
|
||||
("ASCII[4] with NUL", 2, 4, [0x41, 0x42, 0, 0x43]),
|
||||
("ASCII[4] invalid UTF-8", 2, 4, [0xff, 0x41, 0, 0]),
|
||||
("ASCII[4] two invalid bytes", 2, 4, [0xc3, 0x28, 0x41, 0]),
|
||||
("ASCII[9] at a pointer, NUL at 4", 2, 9, w(tail_at as u32 + 4)),
|
||||
("ASCII[8] at a pointer, no NUL", 2, 8, w(tail_at as u32 + 12)),
|
||||
("LONG[1]", LONG, 1, KEY),
|
||||
];
|
||||
for (what, kind, count, field) in &keys {
|
||||
let mut d = private(&[(SUBIFD_KEY, *kind, *count, *field), valid[0], valid[1], valid[2]], order);
|
||||
d.extend(&tail);
|
||||
cases.push((format!("key: first {what}"), d));
|
||||
}
|
||||
// the reader's value budget (8 × the data's length + 1 MiB), used up by entries aliasing the data
|
||||
for aliases in [23u32, 24] {
|
||||
let mut entries = vec![(0x7300, UNDEFINED, 1 << 16, w(0)); aliases as usize];
|
||||
entries.extend(valid);
|
||||
let mut d = private(&entries, order);
|
||||
d.resize(1 << 16, 0);
|
||||
cases.push((format!("{aliases} entries using the value budget"), d));
|
||||
}
|
||||
// no entries; more entries declared than the data holds
|
||||
cases.push(("empty directory".into(), private(&[], order)));
|
||||
let mut long = private(&valid, order);
|
||||
long[..2].copy_from_slice(&if order == Little { 10u16.to_le_bytes() } else { 10u16.to_be_bytes() });
|
||||
long.truncate(2 + 12 * 3);
|
||||
cases.push(("10 entries declared, 3 present".into(), long));
|
||||
cases
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_fields_match_the_tiff_readers_selection() {
|
||||
let mut differ = Vec::new();
|
||||
for order in [Little, Big] {
|
||||
for (case, d) in differential_cases(order) {
|
||||
let f = private_fields(&d, 0, order);
|
||||
let (ours, reader) = ((f.offset, f.length, f.key), reader_selects(&d, 0, order));
|
||||
if ours != reader {
|
||||
differ.push(format!("{order:?} {case}: {ours:?}, reader {reader:?}"));
|
||||
}
|
||||
}
|
||||
assert_eq!(private_fields(&[], 0, order), PrivateFields::default());
|
||||
assert_eq!(private_fields(&private(&[], order), u64::MAX, order), PrivateFields::default());
|
||||
}
|
||||
assert!(differ.is_empty(), "{} cases differ from the TIFF reader:\n{}", differ.len(), differ.join("\n"));
|
||||
// what the reader (and so this) gives in the cases the review named: the limit is 8,192 entries, a later
|
||||
// copy stands in for an unreadable first one, and a used-up value budget hides the fields
|
||||
let found = |d: &[u8]| private_fields(d, 0, Little) == PrivateFields { offset: Some(4096), length: Some(64), key: Some(KEY) };
|
||||
let cases = differential_cases(Little);
|
||||
let case = |name: &str| cases.iter().find(|(c, _)| c == name).map(|(_, d)| d.clone()).unwrap();
|
||||
for name in [
|
||||
"0 fillers",
|
||||
"256 fillers",
|
||||
"8189 fillers",
|
||||
"0x7200: first LONG[2] past the file",
|
||||
"0x7201: first unknown type 14",
|
||||
"key: first UNDEFINED[8] past the file",
|
||||
"23 entries using the value budget",
|
||||
] {
|
||||
assert!(found(&case(name)), "{name}");
|
||||
}
|
||||
for name in ["8190 fillers", "8192 fillers", "24 entries using the value budget", "0x7200: first ASCII", "key: first UNDEFINED[3]"] {
|
||||
assert!(!found(&case(name)), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The case that broke on review: valid entries ahead of the three fields, and an unreadable first offset ahead of
|
||||
/// a valid one. Main's reader finds the block; so must this one (else the black level, white balance and crop it
|
||||
/// holds are lost).
|
||||
#[test]
|
||||
fn the_block_is_found_behind_filler_and_unreadable_entries() {
|
||||
for order in [Little, Big] {
|
||||
for (fillers, unreadable_first) in [(256usize, false), (300, false), (0, true), (256, true)] {
|
||||
let w = |v: u32| {
|
||||
let mut b = Vec::new();
|
||||
order.put_u32(&mut b, v);
|
||||
[b[0], b[1], b[2], b[3]]
|
||||
};
|
||||
let n = fillers + 3 + usize::from(unreadable_first);
|
||||
let start = 2 + 12 * n + 4;
|
||||
let block = block(start, &[(0x7310, SHORT, &[800; 4])], order);
|
||||
let mut entries = vec![(0x7300, LONG, 1, w(7)); fillers];
|
||||
if unreadable_first {
|
||||
entries.push((SUBIFD_OFFSET, LONG, 2, w(u32::MAX)));
|
||||
}
|
||||
entries.extend([
|
||||
(SUBIFD_OFFSET, LONG, 1, w(start as u32)),
|
||||
(SUBIFD_LENGTH, LONG, 1, w(block.len() as u32)),
|
||||
(SUBIFD_KEY, UNDEFINED, 4, KEY),
|
||||
]);
|
||||
let mut file = private(&entries, order);
|
||||
assert_eq!(file.len(), start);
|
||||
file.extend(&block);
|
||||
let mut pointer = Vec::new();
|
||||
order.put_u32(&mut pointer, 0);
|
||||
let ifd0 = Ifd {
|
||||
entries: vec![lightcraft_tiff::Entry { tag: t::DNG_PRIVATE_DATA, value: lightcraft_tiff::Value::Byte(pointer), offset: 0 }],
|
||||
..Default::default()
|
||||
};
|
||||
let black = SubIfd::read(&file, &ifd0, order).and_then(|s| s.shorts(0x7310));
|
||||
assert_eq!(black, Some(vec![800.0; 4]), "{order:?}: {fillers} fillers, unreadable first offset {unreadable_first}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_directories_give_none() {
|
||||
let start = 4096;
|
||||
|
||||
Vendored
+1
@@ -186,6 +186,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
|
||||
Reference in New Issue
Block a user