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.
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-11 21:34:56 -05:00
1 parent db616480e2
commit b20e7370a6
3 files changed
+24 -8

No files matched your search

+1
View File
@@ -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 |
+1 -1
View File
@@ -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")]
+22 -7
View File
@@ -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<Prd> {
/// 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<usize> {
let s = b.get(at..at.checked_add(4)?)?;
@@ -95,8 +98,9 @@ fn be32(b: &[u8], at: usize) -> Option<usize> {
}
/// 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<Mrw<'_>> {
/// 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<Mrw<'_>> {
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<u8> = (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());
}
}