mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 20:48:33 -06:00
M5.6b: header-only raw probe (lightcraft_raw::probe_info); import no longer decodes pixels
Decoders take a Mode: Header reads everything but the samples (DNG, CR2, ARW, RAF, RW2; PEF when it has crop tags). NEF/ORF (and PEF without crop tags) derive geometry/CFA from the samples and still unpack them. decode() stays bit-exact (debug-hash of every corpus/raw decode identical before/after); the corpus test asserts probe_info == decode().info() for all 37 files. Import of 14 corpus raws: 3.4 s -> 1.4-2.0 s; peak RSS through import 1.41-1.49 GB -> 1.12-1.22 GB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
78e50b5fc5
commit
c937ff97ee
@@ -270,7 +270,10 @@ fn snapshot(args: &[String]) -> Result<(), String> {
|
||||
None => Session::new().with_fs(),
|
||||
};
|
||||
if !files.is_empty() {
|
||||
session.execute("library.import", &json!({"paths": expand_paths(&files)})).map_err(|e| e.to_string())?;
|
||||
let ti = Instant::now();
|
||||
let r = session.execute("library.import", &json!({"paths": expand_paths(&files)})).map_err(|e| e.to_string())?;
|
||||
let n = r["imported"].as_array().map_or(0, Vec::len);
|
||||
eprintln!("lightcraft-cli snapshot: imported {n} files in {:.0} ms", ti.elapsed().as_secs_f64() * 1e3);
|
||||
}
|
||||
let services = lightcraft_ui_egui::Services {
|
||||
write: Some(Box::new(|p: &str, b: &[u8]| {
|
||||
|
||||
@@ -36,7 +36,7 @@ fn meta_of(m: &lightcraft_meta::Metadata) -> (Meta, Option<String>) {
|
||||
|
||||
/// Lens corrections embedded in a DNG's `OpcodeList3` (`WarpRectilinear`, `FixVignetteRadial`), re-expressed for
|
||||
/// the default-cropped, EXIF-oriented image. These are the only "profile" corrections LightCraft applies.
|
||||
pub fn embedded_lens(raw: &lightcraft_raw::RawImage) -> Option<lightcraft_develop::EmbeddedLens> {
|
||||
pub fn embedded_lens(raw: &lightcraft_raw::RawInfo) -> Option<lightcraft_develop::EmbeddedLens> {
|
||||
use lightcraft_develop::{EmbeddedLens, EmbeddedVignette, EmbeddedWarp};
|
||||
use lightcraft_geom::Point;
|
||||
let (aw, ah) = (raw.active_area.width as f64, raw.active_area.height as f64);
|
||||
@@ -86,13 +86,14 @@ fn ext_upper(name: &str) -> String {
|
||||
std::path::Path::new(name).extension().map(|e| e.to_string_lossy().to_uppercase()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Probe a file's bytes: kind, dimensions (oriented), metadata.
|
||||
/// Probe a file's bytes: kind, dimensions (oriented), metadata. Raws are described from their
|
||||
/// headers ([`lightcraft_raw::probe_info`]): no pixel data is decompressed.
|
||||
pub fn probe_bytes(name: &str, bytes: &[u8]) -> Result<ProbeInfo, String> {
|
||||
let content_hash = Some(lightcraft_preview::hash_bytes(bytes).to_string());
|
||||
let m = lightcraft_meta::extract(bytes);
|
||||
let (meta, captured) = meta_of(&m);
|
||||
if lightcraft_raw::probe(bytes).is_some() {
|
||||
let raw = match lightcraft_raw::decode(bytes) {
|
||||
let raw = match lightcraft_raw::probe_info(bytes) {
|
||||
Ok(r) => r,
|
||||
Err(lightcraft_raw::RawError::Unsupported(why)) => {
|
||||
// a raw variant we can't decode yet: describe it from its embedded preview
|
||||
@@ -120,7 +121,7 @@ pub fn probe_bytes(name: &str, bytes: &[u8]) -> Result<ProbeInfo, String> {
|
||||
if raw.orientation.swaps_axes() {
|
||||
std::mem::swap(&mut w, &mut h);
|
||||
}
|
||||
let (t, tint) = xy_to_temp_tint(lightcraft_raw::color::as_shot_white_xy(&raw));
|
||||
let (t, tint) = xy_to_temp_tint(lightcraft_raw::color::as_shot_white_xy_of(&raw));
|
||||
let as_shot_wb = Some((t.round(), tint.round()));
|
||||
let embedded_lens = embedded_lens(&raw);
|
||||
return Ok(ProbeInfo {
|
||||
@@ -204,7 +205,7 @@ fn load_bytes_now(bytes: &[u8], max_edge: usize) -> Result<(Rgb32f, SourceInfo),
|
||||
Err(e) => return Err(e.to_string()),
|
||||
};
|
||||
// Embedded lens corrections are applied by the pipeline ("Enable Profile Corrections"), not baked in.
|
||||
let lens = embedded_lens(&raw);
|
||||
let lens = embedded_lens(&raw.info());
|
||||
raw.opcodes.list3.retain(|op| !is_lens_opcode(op));
|
||||
// Previews and thumbnails bin the mosaic straight to (about) the size they need; only
|
||||
// larger levels (exports, 1:1) demosaic the whole sensor.
|
||||
|
||||
+10
-2
@@ -126,14 +126,22 @@ pub fn neutral_to_xy(color: &ColorData, neutral: [f64; 3]) -> Xy {
|
||||
|
||||
/// The as-shot white point: `AsShotWhiteXY`, `AsShotNeutral`, vendor multipliers, else D65.
|
||||
pub fn as_shot_white_xy(raw: &RawImage) -> Xy {
|
||||
let c = &raw.color;
|
||||
as_shot_white(&raw.color, raw.wb_multipliers)
|
||||
}
|
||||
|
||||
/// [`as_shot_white_xy`] from a header-only [`crate::RawInfo`].
|
||||
pub fn as_shot_white_xy_of(info: &crate::RawInfo) -> Xy {
|
||||
as_shot_white(&info.color, info.wb_multipliers)
|
||||
}
|
||||
|
||||
fn as_shot_white(c: &ColorData, wb_multipliers: Option<[f32; 3]>) -> Xy {
|
||||
if let Some(xy) = c.as_shot_white_xy {
|
||||
return xy;
|
||||
}
|
||||
if let Some(n) = c.as_shot_neutral {
|
||||
return neutral_to_xy(c, n);
|
||||
}
|
||||
if let Some(m) = raw.wb_multipliers.filter(|m| m.iter().all(|v| *v > 0.0 && v.is_finite())) {
|
||||
if let Some(m) = wb_multipliers.filter(|m| m.iter().all(|v| *v > 0.0 && v.is_finite())) {
|
||||
return neutral_to_xy(c, [1.0 / m[0] as f64, 1.0 / m[1] as f64, 1.0 / m[2] as f64]);
|
||||
}
|
||||
D65
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! 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 crate::tiffraw::{Packing, read_image_in};
|
||||
use crate::{BlackLevel, Cfa, ColorData, Mat3, Mode, RawData, RawError, RawFormat, RawImage, Rect, Result, opcodes};
|
||||
use lightcraft_color::Xy;
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::tags::{self as t, photometric};
|
||||
@@ -48,7 +48,7 @@ pub(crate) fn color_data(ifd0: &Ifd, raw: &Ifd) -> ColorData {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> 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()))?;
|
||||
@@ -63,7 +63,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
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 mut data = read_image_in(mode, bytes, &info, tiff.order, Packing::Msb)?;
|
||||
let bits = info.bits() as u32;
|
||||
|
||||
// linearization table (integer data only)
|
||||
@@ -162,6 +162,6 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
opcodes,
|
||||
metadata,
|
||||
};
|
||||
img.validate()?;
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
+110
-7
@@ -145,19 +145,75 @@ pub fn probe(bytes: &[u8]) -> Option<RawFormat> {
|
||||
|
||||
/// Decode a raw file.
|
||||
pub fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
decode_with(bytes, Mode::Full)
|
||||
}
|
||||
|
||||
/// Everything [`decode`] learns about a raw file except its samples: geometry, orientation, CFA,
|
||||
/// colour data, as-shot white balance, opcode lists (embedded lens corrections), metadata — read
|
||||
/// from the headers without decompressing the pixel data, for imports. Equal to
|
||||
/// [`RawImage::info`] of the decoded image. Black and white levels are not included (some formats
|
||||
/// measure them from the samples).
|
||||
///
|
||||
/// A few uncompressed vendor formats derive part of this from the samples themselves (Nikon
|
||||
/// NEF: optically masked trailing columns; Olympus ORF: the CFA phase and bit depth; Pentax PEF
|
||||
/// without crop tags: dark borders); for those the samples are read (unpacked, nothing to
|
||||
/// decompress) and dropped.
|
||||
pub fn probe_info(bytes: &[u8]) -> Result<RawInfo> {
|
||||
decode_with(bytes, Mode::Header).map(RawImage::into_info)
|
||||
}
|
||||
|
||||
/// How much of a file a decoder reads.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Mode {
|
||||
/// Headers and samples.
|
||||
Full,
|
||||
/// Headers only: the returned image has no samples, and black/white levels measured from
|
||||
/// the samples are placeholders. Only [`RawImage::into_info`] may look at it.
|
||||
Header,
|
||||
}
|
||||
|
||||
fn decode_with(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
match probe(bytes).ok_or(RawError::NotRaw)? {
|
||||
RawFormat::Dng => dng::decode(bytes),
|
||||
RawFormat::Cr2 => vendor::cr2::decode(bytes),
|
||||
RawFormat::Dng => dng::decode(bytes, mode),
|
||||
RawFormat::Cr2 => vendor::cr2::decode(bytes, mode),
|
||||
RawFormat::Nef | RawFormat::Nrw => vendor::nef::decode(bytes),
|
||||
RawFormat::Arw => vendor::arw::decode(bytes),
|
||||
RawFormat::Raf => vendor::raf::decode(bytes),
|
||||
RawFormat::Rw2 => vendor::rw2::decode(bytes),
|
||||
RawFormat::Pef => vendor::pef::decode(bytes),
|
||||
RawFormat::Arw => vendor::arw::decode(bytes, mode),
|
||||
RawFormat::Raf => vendor::raf::decode(bytes, mode),
|
||||
RawFormat::Rw2 => vendor::rw2::decode(bytes, mode),
|
||||
RawFormat::Pef => vendor::pef::decode(bytes, mode),
|
||||
RawFormat::Orf => vendor::orf::decode(bytes),
|
||||
other => Err(RawError::Unsupported(format!("{other:?} files are not decoded yet"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw file's description without its samples (see [`probe_info`]).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RawInfo {
|
||||
pub format: RawFormat,
|
||||
/// Full sensor data dimensions (including masked borders).
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub cpp: usize,
|
||||
pub cfa: Option<Cfa>,
|
||||
pub bits: u32,
|
||||
pub active_area: Rect,
|
||||
/// Default crop relative to the active area.
|
||||
pub crop: Rect,
|
||||
pub orientation: Orientation,
|
||||
pub color: ColorData,
|
||||
pub wb_multipliers: Option<[f32; 3]>,
|
||||
pub opcodes: OpcodeLists,
|
||||
pub metadata: Metadata,
|
||||
}
|
||||
|
||||
impl RawInfo {
|
||||
/// Size of the developed image before orientation: the default crop, else the active area.
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Sensor samples.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RawData {
|
||||
@@ -377,12 +433,59 @@ pub struct Normalized {
|
||||
}
|
||||
|
||||
impl RawImage {
|
||||
/// The description without the samples (what [`probe_info`] returns for the same file).
|
||||
pub fn info(&self) -> RawInfo {
|
||||
RawInfo {
|
||||
format: self.format,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
cpp: self.cpp,
|
||||
cfa: self.cfa.clone(),
|
||||
bits: self.bits,
|
||||
active_area: self.active_area,
|
||||
crop: self.crop,
|
||||
orientation: self.orientation,
|
||||
color: self.color.clone(),
|
||||
wb_multipliers: self.wb_multipliers,
|
||||
opcodes: self.opcodes.clone(),
|
||||
metadata: self.metadata.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::info`], dropping the samples.
|
||||
pub fn into_info(self) -> RawInfo {
|
||||
RawInfo {
|
||||
format: self.format,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
cpp: self.cpp,
|
||||
cfa: self.cfa,
|
||||
bits: self.bits,
|
||||
active_area: self.active_area,
|
||||
crop: self.crop,
|
||||
orientation: self.orientation,
|
||||
color: self.color,
|
||||
wb_multipliers: self.wb_multipliers,
|
||||
opcodes: self.opcodes,
|
||||
metadata: self.metadata,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate what a decoder read in `mode` ([`Mode::Header`]: everything but the samples).
|
||||
pub(crate) fn validate_for(&self, mode: Mode) -> Result<()> {
|
||||
self.check(mode == Mode::Full)
|
||||
}
|
||||
|
||||
/// Validate internal consistency (dimensions vs data length, CFA shape, rectangles).
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
self.check(true)
|
||||
}
|
||||
|
||||
fn check(&self, samples: bool) -> Result<()> {
|
||||
if self.width == 0 || self.height == 0 || !(1..=4).contains(&self.cpp) {
|
||||
return Err(RawError::Corrupt("bad dimensions".into()));
|
||||
}
|
||||
if self.data.len() != self.width * self.height * self.cpp {
|
||||
if samples && self.data.len() != self.width * self.height * self.cpp {
|
||||
return Err(RawError::Corrupt("data length mismatch".into()));
|
||||
}
|
||||
if let Some(c) = &self.cfa
|
||||
|
||||
@@ -23,8 +23,9 @@ enum ChunkPx {
|
||||
F32(Vec<f32>),
|
||||
}
|
||||
|
||||
/// Decode all chunks of `info` into one buffer of `width × height × cpp` samples.
|
||||
pub fn read_image(data: &[u8], info: &ImageInfo, order: ByteOrder, packing: Packing) -> Result<RawData> {
|
||||
/// The checks [`read_image`] makes before decoding anything (sample format, size limits, data
|
||||
/// present); returns the number of samples.
|
||||
pub fn check_image(data: &[u8], info: &ImageInfo) -> Result<usize> {
|
||||
let (w, h) = (info.width as usize, info.height as usize);
|
||||
let cpp = info.samples_per_pixel as usize;
|
||||
let total = w.checked_mul(h).and_then(|v| v.checked_mul(cpp)).ok_or(RawError::Limit("image too large"))?;
|
||||
@@ -49,6 +50,29 @@ pub fn read_image(data: &[u8], info: &ImageInfo, order: ByteOrder, packing: Pack
|
||||
if available.saturating_mul(2048) < total as u64 {
|
||||
return Err(RawError::Corrupt(format!("{available} bytes of image data cannot hold {total} samples")));
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
/// Read the samples of `info` in `mode`: [`Mode::Header`] only checks them ([`check_image`]) and
|
||||
/// returns no samples (floating-point data as an empty `F32`).
|
||||
pub(crate) fn read_image_in(mode: crate::Mode, data: &[u8], info: &ImageInfo, order: ByteOrder, packing: Packing) -> Result<RawData> {
|
||||
match mode {
|
||||
crate::Mode::Full => read_image(data, info, order, packing),
|
||||
crate::Mode::Header => {
|
||||
check_image(data, info)?;
|
||||
Ok(if info.sample_format == 3 { RawData::F32(Vec::new()) } else { RawData::U16(Vec::new()) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode all chunks of `info` into one buffer of `width × height × cpp` samples.
|
||||
pub fn read_image(data: &[u8], info: &ImageInfo, order: ByteOrder, packing: Packing) -> Result<RawData> {
|
||||
let (w, h) = (info.width as usize, info.height as usize);
|
||||
let total = check_image(data, info)?;
|
||||
let cpp = info.samples_per_pixel as usize;
|
||||
let bits = info.bits() as u32;
|
||||
let float = info.sample_format == 3;
|
||||
let chunks = info.chunks(data.len() as u64);
|
||||
let planar = info.planar == 2 && cpp > 1;
|
||||
let ccpp = if planar { 1 } else { cpp };
|
||||
let decoded: Vec<Result<(Chunk, ChunkPx)>> =
|
||||
|
||||
Vendored
+10
-6
@@ -16,8 +16,8 @@
|
||||
//!
|
||||
//! Also: uncompressed 16-bit ARW, and lossless-JPEG tiled ARW through the generic TIFF path.
|
||||
|
||||
use crate::tiffraw::{Packing, read_image};
|
||||
use crate::{BlackLevel, Cfa, ColorData, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use crate::tiffraw::{Packing, read_image_in};
|
||||
use crate::{BlackLevel, Cfa, ColorData, Mode, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::image::chunk_bytes;
|
||||
use lightcraft_tiff::tags::{self as t, photometric};
|
||||
@@ -94,7 +94,7 @@ fn raw_ifd(tiff: &Tiff) -> Option<&Ifd> {
|
||||
.max_by_key(|i| i.u64(t::IMAGE_WIDTH).unwrap_or(0).saturating_mul(i.u64(t::IMAGE_LENGTH).unwrap_or(0)))
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let tiff = Tiff::parse(bytes)?;
|
||||
let ifd0 = &tiff.ifds[0];
|
||||
let raw = raw_ifd(&tiff).ok_or_else(|| RawError::Unsupported("ARW without a CFA image IFD (old ARW or SR2)".into()))?;
|
||||
@@ -107,6 +107,10 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
let chunks = info.chunks(bytes.len() as u64);
|
||||
let strip_len: u64 = chunks.iter().map(|c| c.len).sum();
|
||||
let (data, out_bits) = match info.compression {
|
||||
32767 if chunks.len() == 1 && strip_len >= (w * h) as u64 && strip_len < (w * h) as u64 * 5 / 4 && mode == Mode::Header => {
|
||||
chunk_bytes(bytes, &chunks[0]).ok_or_else(|| RawError::Corrupt("raw strip outside file".into()))?;
|
||||
(RawData::U16(Vec::new()), 14)
|
||||
}
|
||||
32767 if chunks.len() == 1 && strip_len >= (w * h) as u64 && strip_len < (w * h) as u64 * 5 / 4 => {
|
||||
let src = chunk_bytes(bytes, &chunks[0]).ok_or_else(|| RawError::Corrupt("raw strip outside file".into()))?;
|
||||
let curve = code_curve(&raw.u64s(TONE_CURVE).unwrap_or_else(|| vec![8000, 10400, 12900, 14100]));
|
||||
@@ -123,9 +127,9 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
32767 => return Err(RawError::Unsupported("Sony ARW version 1 / packed compressed variant".into())),
|
||||
1 => {
|
||||
let packing = if strip_len >= (w * h * 2) as u64 { Packing::Word16 } else { Packing::Msb };
|
||||
(read_image(bytes, &info, tiff.order, packing)?, bits)
|
||||
(read_image_in(mode, bytes, &info, tiff.order, packing)?, bits)
|
||||
}
|
||||
_ => (read_image(bytes, &info, tiff.order, Packing::Msb)?, bits),
|
||||
_ => (read_image_in(mode, bytes, &info, tiff.order, Packing::Msb)?, bits),
|
||||
};
|
||||
let RawData::U16(ref samples) = data else { return Err(RawError::Unsupported("float ARW".into())) };
|
||||
|
||||
@@ -171,7 +175,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
};
|
||||
img.validate()?;
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
|
||||
Vendored
+34
-13
@@ -14,7 +14,7 @@
|
||||
//! - sRAW / mRAW (YCbCr, subsampled) are not supported yet.
|
||||
|
||||
use super::{black_from_columns, white_from_data};
|
||||
use crate::{BlackLevel, Cfa, ColorData, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result, ljpeg};
|
||||
use crate::{BlackLevel, Cfa, ColorData, Mode, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result, ljpeg};
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::image::chunk_bytes;
|
||||
use lightcraft_tiff::{Ifd, Tiff, makernote, tags as t};
|
||||
@@ -45,7 +45,7 @@ fn wb_from_color_balance(v: &[u64]) -> Option<[f32; 3]> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let tiff = Tiff::parse(bytes)?;
|
||||
let ifd0 = &tiff.ifds[0];
|
||||
let raw = raw_ifd(&tiff).ok_or_else(|| RawError::Corrupt("CR2 without raw IFD".into()))?;
|
||||
@@ -57,8 +57,24 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
let chunk = lightcraft_tiff::image::Chunk { index: 0, x: 0, y: 0, width: 0, height: 0, plane: 0, offset: off, len };
|
||||
let src = chunk_bytes(bytes, &chunk).ok_or_else(|| RawError::Corrupt("raw strip outside file".into()))?;
|
||||
let (fw, fh, nc, prec) = ljpeg::frame_info(src)?;
|
||||
let frame = ljpeg::decode(src, (fw * fh * nc).min(crate::MAX_SAMPLES))?;
|
||||
let total = frame.data.len();
|
||||
let frame = match mode {
|
||||
Mode::Full => Some(ljpeg::decode(src, (fw * fh * nc).min(crate::MAX_SAMPLES))?),
|
||||
Mode::Header => None,
|
||||
};
|
||||
let total = match &frame {
|
||||
Some(f) => f.data.len(),
|
||||
None => {
|
||||
let total = fw
|
||||
.checked_mul(fh)
|
||||
.and_then(|v| v.checked_mul(nc))
|
||||
.filter(|t| *t > 0)
|
||||
.ok_or_else(|| RawError::Corrupt("bad CR2 frame size".into()))?;
|
||||
if total > crate::MAX_SAMPLES {
|
||||
return Err(RawError::Limit("lossless JPEG frame larger than expected"));
|
||||
}
|
||||
total
|
||||
}
|
||||
};
|
||||
let slices = raw.u64s(CR2_SLICE).filter(|s| s.len() == 3 && s[1] > 0 && s[2] > 0);
|
||||
let widths: Vec<usize> = match &slices {
|
||||
Some(s) => {
|
||||
@@ -74,16 +90,20 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
return Err(RawError::Corrupt(format!("CR2 slices ({width}) do not divide the frame ({total} samples)")));
|
||||
}
|
||||
let height = total / width;
|
||||
let mut data = vec![0u16; total];
|
||||
let mut i = 0;
|
||||
let mut x0 = 0;
|
||||
for &sw in &widths {
|
||||
for y in 0..height {
|
||||
data[y * width + x0..y * width + x0 + sw].copy_from_slice(&frame.data[i..i + sw]);
|
||||
i += sw;
|
||||
let mut data = Vec::new();
|
||||
if let Some(frame) = &frame {
|
||||
data = vec![0u16; total];
|
||||
let mut i = 0;
|
||||
let mut x0 = 0;
|
||||
for &sw in &widths {
|
||||
for y in 0..height {
|
||||
data[y * width + x0..y * width + x0 + sw].copy_from_slice(&frame.data[i..i + sw]);
|
||||
i += sw;
|
||||
}
|
||||
x0 += sw;
|
||||
}
|
||||
x0 += sw;
|
||||
}
|
||||
drop(frame);
|
||||
|
||||
// maker note: sensor borders and white balance
|
||||
let make = ifd0.string(t::MAKE).unwrap_or_default();
|
||||
@@ -126,7 +146,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
};
|
||||
img.validate()?;
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
@@ -183,6 +203,7 @@ pub(crate) mod tests {
|
||||
assert_eq!(crate::probe(&bytes), Some(RawFormat::Cr2));
|
||||
let r = crate::decode(&bytes).unwrap();
|
||||
assert_eq!((r.width, r.height), (64, 10));
|
||||
assert_eq!(crate::probe_info(&bytes).unwrap(), r.info());
|
||||
assert_eq!(r.data, RawData::U16(img));
|
||||
assert_eq!(r.orientation, Orientation::Rotate270);
|
||||
assert_eq!(r.metadata.iso, Some(400));
|
||||
|
||||
Vendored
+16
-10
@@ -17,8 +17,8 @@
|
||||
|
||||
use super::{black_from_columns, white_from_data};
|
||||
|
||||
use crate::tiffraw::{Packing, read_image};
|
||||
use crate::{BlackLevel, Cfa, ColorData, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use crate::tiffraw::{Packing, read_image_in};
|
||||
use crate::{BlackLevel, Cfa, ColorData, Mode, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::image::chunk_bytes;
|
||||
use lightcraft_tiff::{ByteOrder, Tiff, makernote, tags as t};
|
||||
@@ -184,7 +184,7 @@ fn dark_trimmed(d: &[u16], w: usize, h: usize) -> Rect {
|
||||
Rect::new(left, 0, (right - left) & !1, h)
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let tiff = Tiff::parse(bytes)?;
|
||||
let ifd0 = &tiff.ifds[0];
|
||||
let info = ifd0.image()?;
|
||||
@@ -196,6 +196,13 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
let make = ifd0.string(t::MAKE).unwrap_or_default();
|
||||
let mn =
|
||||
tiff.exif().and_then(|e| e.get(t::MAKER_NOTE)).and_then(|e| makernote::parse_makernote(bytes, e.offset, e.count() as u64, tiff.order, &make));
|
||||
let pair = |tag: u16| mn.as_ref().and_then(|m| m.ifd.u64s(tag)).filter(|v| v.len() == 2).map(|v| (v[0] as usize, v[1] as usize));
|
||||
let tagged = match (pair(CROP_ORIGIN), pair(CROP_SIZE)) {
|
||||
(Some((x, y)), Some((cw, ch))) if cw > 0 && ch > 0 && x + cw <= w && y + ch <= hgt => Some(Rect::new(x, y, cw, ch)),
|
||||
_ => None,
|
||||
};
|
||||
// without crop tags the image area is found from the samples (dark borders)
|
||||
let read = if tagged.is_some() { mode } else { Mode::Full };
|
||||
let data = match info.compression {
|
||||
65535 => {
|
||||
let mn = mn.as_ref().ok_or_else(|| RawError::Corrupt("compressed PEF without maker note".into()))?;
|
||||
@@ -208,21 +215,20 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
let src = chunk_bytes(bytes, &c)
|
||||
.or_else(|| bytes.get(first.offset as usize..))
|
||||
.ok_or_else(|| RawError::Corrupt("PEF data outside file".into()))?;
|
||||
RawData::U16(decode_huffman(src, &huff, w, hgt, bits)?)
|
||||
if read == Mode::Full { RawData::U16(decode_huffman(src, &huff, w, hgt, bits)?) } else { RawData::U16(Vec::new()) }
|
||||
}
|
||||
1 => {
|
||||
let strip: u64 = info.chunks(bytes.len() as u64).iter().map(|c| c.len).sum();
|
||||
let packing = if strip >= (w * hgt * 2) as u64 { Packing::Word16 } else { Packing::Msb };
|
||||
read_image(bytes, &info, tiff.order, packing)?
|
||||
read_image_in(read, bytes, &info, tiff.order, packing)?
|
||||
}
|
||||
c => return Err(RawError::Unsupported(format!("PEF compression {c}"))),
|
||||
};
|
||||
let RawData::U16(ref samples) = data else { return Err(RawError::Unsupported("float PEF".into())) };
|
||||
|
||||
let pair = |tag: u16| mn.as_ref().and_then(|m| m.ifd.u64s(tag)).filter(|v| v.len() == 2).map(|v| (v[0] as usize, v[1] as usize));
|
||||
let active = match (pair(CROP_ORIGIN), pair(CROP_SIZE)) {
|
||||
(Some((x, y)), Some((cw, ch))) if cw > 0 && ch > 0 && x + cw <= w && y + ch <= hgt => Rect::new(x, y, cw, ch),
|
||||
_ => dark_trimmed(samples, w, hgt),
|
||||
let active = match tagged {
|
||||
Some(r) => r,
|
||||
None => dark_trimmed(samples, w, hgt),
|
||||
};
|
||||
let cfa = match (ifd0.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), ifd0.bytes(t::CFA_PATTERN_EP)) {
|
||||
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
|
||||
@@ -265,7 +271,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
};
|
||||
img.validate()?;
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
|
||||
Vendored
+12
-8
@@ -21,7 +21,7 @@
|
||||
|
||||
use super::white_from_data;
|
||||
use crate::unpack::{unpack_lsb, unpack_msb};
|
||||
use crate::{BlackLevel, Cfa, ColorData, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use crate::{BlackLevel, Cfa, ColorData, Mode, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::{ByteOrder, Ifd, Tiff};
|
||||
use rayon::prelude::*;
|
||||
@@ -114,7 +114,7 @@ pub(crate) fn unpack_row(src: &[u8], bits: u32, out: &mut [u16]) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let h = header(bytes)?;
|
||||
let raw = h.raw.ok_or_else(|| RawError::Corrupt("RAF without raw data".into()))?;
|
||||
let ifd = raw_ifd(raw)?;
|
||||
@@ -137,11 +137,14 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
return Err(RawError::Unsupported(format!("Fujifilm compressed RAF ({len} bytes for {w}x{hgt} {bits}-bit)")));
|
||||
}
|
||||
let src = raw.get(off..off.saturating_add(len).min(raw.len())).ok_or_else(|| RawError::Corrupt("RAF strip outside file".into()))?;
|
||||
let mut data = vec![0u16; n];
|
||||
data.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
|
||||
let s = src.get(y * stride..(y * stride + packed_row).min(src.len())).unwrap_or(&[]);
|
||||
unpack_row(s, store, row);
|
||||
});
|
||||
let mut data = Vec::new();
|
||||
if mode == Mode::Full {
|
||||
data = vec![0u16; n];
|
||||
data.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
|
||||
let s = src.get(y * stride..(y * stride + packed_row).min(src.len())).unwrap_or(&[]);
|
||||
unpack_row(s, store, row);
|
||||
});
|
||||
}
|
||||
|
||||
let cfa = cfa(&h);
|
||||
let active = match (pair(&h, CROP_TOP_LEFT), pair(&h, CROPPED_SIZE)) {
|
||||
@@ -186,7 +189,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
};
|
||||
img.validate()?;
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
@@ -298,6 +301,7 @@ mod tests {
|
||||
let bytes = raf(w, h, bits, pack(&px, store), Some(layout), b"\xff\xd8\xff\xd9");
|
||||
assert_eq!(crate::probe(&bytes), Some(RawFormat::Raf));
|
||||
let r = crate::decode(&bytes).unwrap_or_else(|e| panic!("{bits}: {e}"));
|
||||
assert_eq!(crate::probe_info(&bytes).unwrap(), r.info());
|
||||
assert_eq!(r.data, RawData::U16(px), "{bits}-bit");
|
||||
assert_eq!(r.cfa, Some(Cfa::xtrans()));
|
||||
assert_eq!(r.active_area, Rect::new(4, 2, 12, 4));
|
||||
|
||||
Vendored
+10
-6
@@ -20,7 +20,7 @@
|
||||
//! reconstruction rule for every scale, and no permissively licensed description exists. Those files report
|
||||
//! [`RawError::Unsupported`]; their embedded preview still works.
|
||||
|
||||
use crate::{BlackLevel, Cfa, ColorData, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use crate::{BlackLevel, Cfa, ColorData, Mode, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::{Tiff, tags as t};
|
||||
use rayon::prelude::*;
|
||||
@@ -97,7 +97,7 @@ fn cfa(code: u16) -> Cfa {
|
||||
Cfa::bayer(s).expect("static")
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let tiff = Tiff::parse(bytes)?;
|
||||
let ifd0 = &tiff.ifds[0];
|
||||
let get = |tag: u16| ifd0.u64(tag).map(|v| v as usize);
|
||||
@@ -118,9 +118,12 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
if src.len() < need {
|
||||
return Err(RawError::Corrupt(format!("RW2 raw data truncated ({} of {need} bytes)", src.len())));
|
||||
}
|
||||
let stream = unrotate(&src[..(need.div_ceil(CHUNK) * CHUNK).min(src.len())]);
|
||||
let mut data = vec![0u16; n];
|
||||
unpack(&stream, bits, &mut data)?;
|
||||
let mut data = Vec::new();
|
||||
if mode == Mode::Full {
|
||||
let stream = unrotate(&src[..(need.div_ceil(CHUNK) * CHUNK).min(src.len())]);
|
||||
data = vec![0u16; n];
|
||||
unpack(&stream, bits, &mut data)?;
|
||||
}
|
||||
|
||||
let active = match (get(BORDER_TOP), get(BORDER_LEFT), get(BORDER_BOTTOM), get(BORDER_RIGHT)) {
|
||||
(Some(tp), Some(l), Some(b), Some(r)) if b > tp && r > l => Rect::new(l, tp, r - l, b - tp).clipped(w, h),
|
||||
@@ -167,7 +170,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
};
|
||||
img.validate()?;
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
@@ -235,6 +238,7 @@ mod tests {
|
||||
let bytes = rw2(w as u16, h as u16, bits as u16, 5, &encode(&px, bits));
|
||||
assert_eq!(crate::probe(&bytes), Some(RawFormat::Rw2));
|
||||
let r = crate::decode(&bytes).unwrap_or_else(|e| panic!("{bits}: {e}"));
|
||||
assert_eq!(crate::probe_info(&bytes).unwrap(), r.info());
|
||||
assert_eq!(r.data, RawData::U16(px), "{bits}-bit");
|
||||
assert_eq!(r.active_area, Rect::new(4, 2, w - 4, h - 2));
|
||||
assert_eq!(r.cfa.as_ref().unwrap().name(), "RGGB");
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! `Unsupported` for one of the variants we know we don't decode yet. Prints decode times
|
||||
//! (`cargo test -p lightcraft-raw --release --test corpus -- --nocapture`).
|
||||
|
||||
use lightcraft_raw::{RawError, RawFormat, decode, embedded_preview, probe};
|
||||
use lightcraft_raw::{RawError, RawFormat, decode, embedded_preview, probe, probe_info};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -55,9 +55,20 @@ fn corpus_raw_decodes() {
|
||||
}
|
||||
let preview_kb = preview.as_ref().map_or(0, |p| p.len() / 1024);
|
||||
let t1 = Instant::now();
|
||||
match decode(&bytes) {
|
||||
let decoded = decode(&bytes);
|
||||
let dt = t1.elapsed().as_secs_f64() * 1e3;
|
||||
// the header-only probe agrees with the full decode, faster
|
||||
let t2 = Instant::now();
|
||||
let info = probe_info(&bytes);
|
||||
let di = t2.elapsed().as_secs_f64() * 1e3;
|
||||
match (&decoded, &info) {
|
||||
(Ok(img), Ok(info)) => assert_eq!(&img.info(), info, "{name}: probe_info differs from decode"),
|
||||
(Err(RawError::Unsupported(_)), Err(RawError::Unsupported(_))) => {}
|
||||
(d, i) => panic!("{name}: decode {:?} but probe_info {:?}", d.as_ref().err(), i.as_ref().err()),
|
||||
}
|
||||
eprintln!("{name:44} probe_info {di:.1} ms");
|
||||
match decoded {
|
||||
Ok(img) => {
|
||||
let dt = t1.elapsed().as_secs_f64() * 1e3;
|
||||
img.validate().unwrap();
|
||||
assert!(img.white_at(0) > img.black.mean(), "{name}: white {} <= black {}", img.white_at(0), img.black.mean());
|
||||
let mp = (img.width * img.height) as f64 / 1e6;
|
||||
|
||||
@@ -100,6 +100,8 @@ fn writer_roundtrip_all_layouts() {
|
||||
assert_eq!(probe(&bytes), Some(RawFormat::Dng));
|
||||
let back = decode(&bytes).unwrap();
|
||||
assert_same(&raw, &back);
|
||||
// the header-only probe describes the same image
|
||||
assert_eq!(lightcraft_raw::probe_info(&bytes).unwrap(), back.info());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user