From b20e7370a6953ce1701abe446be533595d67b087 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 21:34:56 -0500 Subject: [PATCH] craft-raw: mrw::parse, the MRW block container as public API (#1052) The MRW block reader (blocks by name and the sensor data offset) was crate-private, so a metadata reader that wants only the TTW Exif block had to keep its own copy. craft_raw::mrw::{parse, Mrw, MAX_BLOCKS} are now public, behind the existing mrw feature; decoding is unchanged. A new test covers the block limit. --- crates/raw/README.md | 1 + crates/raw/src/lib.rs | 2 +- crates/raw/src/mrw.rs | 29 ++++++++++++++++++++++------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/raw/README.md b/crates/raw/README.md index 4b3977851..2a1e270d2 100644 --- a/crates/raw/README.md +++ b/crates/raw/README.md @@ -56,6 +56,7 @@ One module and one Cargo feature per maker or container family. All are on by de | `cr2` | Canon CR2 | The lossless-JPEG frame of IFD 3 de-sliced (`0xc640`); the layout from `CR2CFAPattern` (`0xc5e0`); image area from `SensorInfo`; black measured on the masked columns; white from the saturation plateau; as-shot white balance from `ColorData` or `WBInfo`; the white-balance presets at their temperatures (`ColorData::kelvin_presets`). sRAW / mRAW (subsampled YCbCr, 4:2:2 and 4:2:0) come out as linear white-balanced RGB with three samples per pixel, the chroma gains measured against the file's own 16-bit RGB image in IFD 2 | | `nef` | Nikon NEF / NRW | Uncompressed (16-bit words, 12-bit packed, Coolpix word packing), the packed layouts labelled compression 34713, Nikon's Huffman coding (lossless and lossy types, 12 and 14 bits, "lossy after split") with the `0x0096` linearisation table, small raw (YCbCr 4:2:2) as linear white-balanced RGB; black from `0x003d` or the NRW block, as-shot white balance from `0x000c`, `CropArea` or the measured image area of bodies that record none, masked trailing columns left out of the active area, the D1X pixel aspect, and the in-file distortion correction as a `WarpRectilinear` opcode ([derivation](docs/nikon-lens-corrections.md)). High Efficiency (HE / HEβ˜…) is refused by name; the previews still read | | `rw2` | Panasonic RW2, Leica RWL, older Panasonic / Leica RAW | Packed blocks (formats 2, 5, 7), formats 4 and 6, format 8 prefix-coded strips and tiles (a sample outside the bit depth makes the strip corrupt), 16-bit words; black per colour from `0x001c`–`0x001e` (plus the 15 counts formats 4 and 6 store above it, 15 when no black is recorded); white from the saturation plateau; sensor borders as the active area and the in-camera aspect crop; mapped-out defect pixels filled; as-shot white balance from `0x0024`–`0x0026`; the in-camera distortion correction (`0x0119`) as a `WarpRectilinear` opcode. Raw formats 1 and 3 and compression 34826 are refused (no samples of them) | +| `mrw` | Minolta / Konica Minolta MRW | The `\0MRM` block container (`PRD`, `WBG`, `TTW`), 12-bit packed and 16-bit word mosaics, as-shot white balance from `WBG`, white from the saturation plateau; `mrw::parse` reads the block container alone | | `raf` | Fujifilm RAF | The RAF header, records and raw IFD; uncompressed Bayer and X-Trans (16-bit words, 12-bit LSB-first, 32-bit word packing); Fujifilm's lossless and lossy striped compression ([derivation](docs/raf-compression.md), [corpus](docs/raf-corpus.sha256)); black, as-shot white balance and, for bodies without colour matrices, the camera's neutral multipliers for Illuminant A and D65 (`ColorData::kelvin_presets`, a neutral pair); orientation, make and model from the embedded JPEG's Exif; the in-file distortion and vignetting corrections as opcodes; the embedded preview, also by range from the header alone (`bounded_preview`, `embedded_preview_from_range`). The pre-2010 FinePix layout without a raw IFD is refused | | `orf` | Olympus / OM System ORF | Uncompressed: 16-bit words (12-bit values in the top bits shifted down), 12-bit packed in 32-bit words (XZ-2), 12-bit two-field (older compacts), 16-byte blocks of ten 12-bit samples (E-300, E-330, E-500, high-resolution modes); CFA from Exif `CFAPattern`; black, as-shot white balance (relative to `WB_GLevel`) and crop from the maker note's ImageProcessing, or from the main note on bodies without it; the CameraSettings preview. Compressed ORF is refused ([why](docs/orf-compression.md)); its preview still reads | | `mrw` | Minolta / Konica Minolta MRW | The `\0MRM` block container (`PRD`, `WBG`, `TTW`), 12-bit packed and 16-bit word mosaics, as-shot white balance from `WBG`, white from the saturation plateau | diff --git a/crates/raw/src/lib.rs b/crates/raw/src/lib.rs index f3b144669..c90e2e5b6 100644 --- a/crates/raw/src/lib.rs +++ b/crates/raw/src/lib.rs @@ -79,7 +79,7 @@ pub mod ljpeg; #[cfg(feature = "mef")] mod mef; #[cfg(feature = "mrw")] -mod mrw; +pub mod mrw; #[cfg(feature = "nef")] mod nef; #[cfg(feature = "nef")] diff --git a/crates/raw/src/mrw.rs b/crates/raw/src/mrw.rs index 780f351bf..0281a3911 100644 --- a/crates/raw/src/mrw.rs +++ b/crates/raw/src/mrw.rs @@ -23,6 +23,9 @@ //! reach on all eight samples. //! - `TTW` is a TIFF with IFD0 (orientation), Exif and the maker note; the preview JPEG sits in it with its first //! byte overwritten (see `preview::scan_for_jpeg`). +//! +//! [`parse`] reads the block container on its own, for a caller that wants a block (the `TTW` Exif, say) +//! without decoding the mosaic. use crate::{ BlackLevel, Camera, Cfa, ColorData, Limits, Mode, OpcodeLists, Orientation, RawData, RawError, RawFormat, RawImage, Rect, Result, unpack, }; @@ -72,22 +75,22 @@ fn prd(b: &[u8]) -> Result { /// The blocks and the offset of the sensor data in an MRW file. #[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct Mrw<'a> { +pub struct Mrw<'a> { /// `(name without the leading NUL, contents)`, in file order. - pub(crate) blocks: Vec<([u8; 3], &'a [u8])>, + pub blocks: Vec<([u8; 3], &'a [u8])>, /// Absolute offset of the sensor data. - pub(crate) data_offset: usize, + pub data_offset: usize, } impl<'a> Mrw<'a> { /// The first block called `name` (e.g. `b"PRD"`). - pub(crate) fn block(&self, name: &[u8; 3]) -> Option<&'a [u8]> { + pub fn block(&self, name: &[u8; 3]) -> Option<&'a [u8]> { self.blocks.iter().find(|(n, _)| n == name).map(|(_, d)| *d) } } /// At most this many blocks are walked (real files have four or five). -const MAX_BLOCKS: usize = 64; +pub const MAX_BLOCKS: usize = 64; fn be32(b: &[u8], at: usize) -> Option { let s = b.get(at..at.checked_add(4)?)?; @@ -95,8 +98,9 @@ fn be32(b: &[u8], at: usize) -> Option { } /// Parse the block list of an MRW file. `None` when the magic is missing or the header length points past the -/// file; a block that runs past the header is dropped together with everything after it. -pub(crate) fn parse(bytes: &[u8]) -> Option> { +/// file; a block that runs past the header is dropped together with everything after it, and at most +/// [`MAX_BLOCKS`] blocks are read. Nothing is decoded and nothing is copied. +pub fn parse(bytes: &[u8]) -> Option> { if !bytes.starts_with(b"\0MRM") { return None; } @@ -318,4 +322,15 @@ mod container_tests { let _ = parse(&f[..n]); } } + + #[test] + fn at_most_max_blocks_are_read() { + let body: Vec = (0..MAX_BLOCKS + 6).flat_map(|_| block(b"PAD", &[0])).collect(); + let mut f = b"\0MRM".to_vec(); + f.extend_from_slice(&(body.len() as u32).to_be_bytes()); + f.extend_from_slice(&body); + let m = parse(&f).unwrap(); + assert_eq!(m.blocks.len(), MAX_BLOCKS); + assert_eq!(m.data_offset, f.len()); + } }