mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 07:46:08 +00:00
craft-raw: Panasonic RW2, RWL and RAW (#1000)
* craft-raw: the file-format layer of camera raw files, with DNG and CRW A new crate for #587 and #778: bytes in, the sensor data out as a neutral RawImage (samples, CFA, levels, active area, crop, orientation, colour tags, opcode lists, profile tables), and back for DNG. One Cargo feature per format: dng (read and write), jxl (JPEG XL DNG), crw (through craft-crw). The base is LightCraft's crates/raw at 5ff567a4: dng, dngwrite, tiffraw, unpack, ljpeg, jxl, semantic, preview, the parsing half of opcodes, profile and gaintable, the model and probe. Demosaicing and everything after it stay with the develop pipeline. Ported from PhotoCraft's and FilmCraft's DNG readers, each with a test: the default crop's per-tag defaults, an unusable crop falling back to the active area, non-finite white levels, the white default of a linearized image, the AsShotWhiteXY range, 8-bit-only lossy JPEG, lossy tiles that must cover their part of the image, and the CinemaDNG frame tags. * craft-raw: Hasselblad 3FR and FFF (feature hasselblad) RawFormat::CfaTiff decodes: the DNG-layout reader plus what these files add. Uncompressed data or Hasselblad's lossless JPEG (selection value 8), RGGB when no CFAPattern is stated, an FFF's colour matrix from the 3FR header it embeds, and the 8-bit RGB preview handed on as a baseline TIFF. Moved from LightCraft's vendor/hasselblad.rs at 5ff567a4 with its tests. Ported from PhotoCraft's reader: four BlackLevel values without a BlackLevelRepeatDim are one per CFA site in a file that is not a DNG. * craft-raw: Minolta MRW, Epson ERF, Mamiya MEF and Kodak DCS520C Four formats only LightCraft decodes, one feature each (mrw, erf, mef, kodak), moved from its vendor modules at 5ff567a4 with their tests; the MRW block container comes from lightcraft-meta. src/shared.rs holds the level helpers the maker decoders share. ERF and MEF state no white balance: the estimate LightCraft made from the preview is develop-side work, so wb_multipliers is None for them here. * craft-raw: shared scan budget for tagged previews, tile-sized lossy JPEG budget, bound on Deflate tile padding Ported from storytold/lightcraft#3695. Candidate preview ranges are deduplicated and charged to one byte budget and one header-walk budget; a lossy JPEG decoded for a tile may hold no more samples than the tile rounded up to whole MCUs (and never more than Limits::max_lossy_tile_samples); a Deflate chunk whose nominal size is mostly padding outside the image is refused before it is inflated. * craft-raw: lossless JPEG sample bound per referenced byte; previews larger than the file are passed over Ported from storytold/lightcraft#3910 (the parts whose code is in craft-raw today). check_image refuses a lossless JPEG image that declares more than 32 samples per referenced byte, counted once per chunk that points at the bytes so identical tiles stored once still decode. The Hasselblad RGB preview search passes over an image whose pixel bytes exceed the file and still takes a smaller complete one. * craft-raw: test for the Deflate padding bound * craft-raw: Pentax PEF and Samsung SRW Moved from LightCraft crates/raw (vendor/pef.rs, vendor/srw.rs) at 5ff567a4. The Huffman difference decoder and bit reader that PEF shares with the Nikon, Panasonic and Sony codings become the huffman module. * craft-raw: Canon CR2, with sRAW and mRAW Moved from LightCraft crates/raw (vendor/cr2.rs, vendor/cr2/sraw.rs) at 5ff567a4. The white-balance presets a CR2 lists come back as data in ColorData::kelvin_presets (the ordering from LightCraft's kelvin_presets.rs; the temperature and tint arithmetic stays with the developer). * craft-raw: an sRAW preview larger than the file is refused before its buffer is reserved Ported from storytold/lightcraft#3910 (cr2::sraw::read_preview). * WIP craft-raw: Nikon NEF/NRW (does not compile yet: limits threading, develop() calls in tests) * craft-raw: Nikon NEF/NRW, with PhotoCraft's maker-note compression routing and 12-bit D4 white level * craft-raw: Panasonic RW2 / RWL / RAW, every raw format * craft-raw: RW2 format 8 refuses strips stating fewer bits than samples Ported from storytold/lightcraft#3910. * craft-raw: RW2 format 8 refuses samples above the declared bit depth Ported from storytold/photocraft#11838. * craft-raw: the RW2 format 8 robustness seed carries a valid stream Its random payload decoded only while out-of-depth samples were clamped; mutations now start from a file that decodes. --------- Co-authored-by: radialmonster <radial@gmail.com> Co-authored-by: wnanbei <wnanbei@gmail.com>
This commit is contained in:
co-authored by
radialmonster
wnanbei
parent
3826abcddf
commit
0da3928eee
@@ -93,7 +93,7 @@ crates/raw (craft-raw) is moved from LightCraft, with behaviour ported from two
|
||||
- LightCraft, crates/raw (src/lib.rs, dng.rs, dngwrite.rs, tiffraw.rs, unpack.rs, ljpeg.rs, jxl.rs,
|
||||
opcodes.rs, profile.rs, gaintable.rs, semantic.rs, preview.rs, vendor/crw.rs, vendor/hasselblad.rs,
|
||||
vendor.rs (shared level helpers), vendor/mrw.rs, vendor/erf.rs, vendor/mef.rs, vendor/kodak.rs, vendor/pef.rs,
|
||||
vendor/srw.rs, vendor/cr2.rs, vendor/cr2/sraw.rs, vendor/nef.rs, vendor/nefc.rs, vendor/nef_lens.rs, docs/nikon-lens-corrections.md, kelvin_presets.rs (the preset ordering), tests/it/vendor_robust.rs and their tests) and crates/meta/src/mrw.rs (the MRW block container),
|
||||
vendor/srw.rs, vendor/cr2.rs, vendor/cr2/sraw.rs, vendor/nef.rs, vendor/nefc.rs, vendor/nef_lens.rs, docs/nikon-lens-corrections.md, vendor/rw2.rs, kelvin_presets.rs (the preset ordering), tests/it/vendor_robust.rs and their tests) and crates/meta/src/mrw.rs (the MRW block container),
|
||||
revision 5ff567a4a2e067bef1b6bce152c11e7f727ccbaf.
|
||||
https://github.com/storytold/lightcraft
|
||||
Copyright (c) 2026 ArtCraft Team and the LightCraft contributors.
|
||||
|
||||
@@ -146,6 +146,7 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
|
||||
what PhotoCraft's and FilmCraft's readers added: container parsing and decompression to a neutral
|
||||
raw image (samples, CFA, black and white levels, active area, crop, orientation, colour tags,
|
||||
opcode lists, embedded previews), one Cargo feature per format. DNG reading and writing,
|
||||
Hasselblad 3FR/FFF, Minolta MRW, Epson ERF, Mamiya MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2, Nikon NEF/NRW, Panasonic RW2 and Canon CRW (through
|
||||
Hasselblad 3FR/FFF, Minolta MRW, Epson ERF, Mamiya MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2, Nikon NEF/NRW and Canon CRW (through
|
||||
`craft-crw`) so far; demosaicing and everything after stay with the develop
|
||||
pipeline.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "craft-raw"
|
||||
description = "Camera raw files, the file-format layer: container parsing, decompression and the sensor data out as a neutral raw image with its levels, crop, orientation, colour tags and embedded previews. DNG (read and write), Hasselblad 3FR/FFF, MRW, ERF, MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2, Nikon NEF/NRW and Canon CRW so far."
|
||||
description = "Camera raw files, the file-format layer: container parsing, decompression and the sensor data out as a neutral raw image with its levels, crop, orientation, colour tags and embedded previews. DNG (read and write), Hasselblad 3FR/FFF, MRW, ERF, MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2, Nikon NEF/NRW, Panasonic RW2 and Canon CRW so far."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -11,7 +11,7 @@ repository.workspace = true
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["dng", "jxl", "crw", "hasselblad", "mrw", "erf", "mef", "kodak", "pef", "srw", "cr2", "nef"]
|
||||
default = ["dng", "jxl", "crw", "hasselblad", "mrw", "erf", "mef", "kodak", "pef", "srw", "cr2", "nef", "rw2"]
|
||||
## Adobe DNG, reading and writing: uncompressed, lossless JPEG, lossy JPEG, Deflate.
|
||||
dng = ["dep:miniz_oxide", "dep:zune-jpeg", "dep:zune-core"]
|
||||
## JPEG XL compressed DNGs (DNG 1.7, compression 52546) via jxl-oxide (pure Rust, MIT OR Apache-2.0).
|
||||
@@ -34,6 +34,8 @@ srw = []
|
||||
cr2 = []
|
||||
## Nikon NEF and NRW: uncompressed, packed, Nikon's Huffman coding (lossless and lossy), small raw.
|
||||
nef = []
|
||||
## Panasonic RW2, Leica RWL and the older Panasonic / Leica RAW: every raw format (packed, 4, 6, 8 and 16-bit words).
|
||||
rw2 = []
|
||||
## Canon CRW (CIFF) through craft-crw, the one CRW decoder.
|
||||
crw = ["dep:craft-crw", "dep:chrono"]
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ One module and one Cargo feature per maker or container family. All are on by de
|
||||
| `crw` | Canon CRW (CIFF) | Decoded by [`craft-crw`](../crw/README.md), the one CRW decoder, and returned through this crate's types |
|
||||
| `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 |
|
||||
| `erf` | Epson ERF (R-D1, R-D1s, R-D1x) | 12-bit samples in 16-byte blocks, black levels from the maker note, the measured picture origin. No white balance (the file states none) |
|
||||
| `mef` | Mamiya MEF (ZD) | Uncompressed 12-bit packed; default crop from the RGB preview's measured window. No white balance (the file states none) |
|
||||
@@ -142,6 +143,13 @@ Left as LightCraft has it, with the reason:
|
||||
LightCraft measured it from the data.
|
||||
LightCraft's built-in white-balance preset table for bodies that record none is not file data and stays with
|
||||
the developer. `CropArea`, the D90 / D5100 / D800E areas and the 14-bit-unit black level are the same in both.
|
||||
- Panasonic RW2 is LightCraft's reader. PhotoCraft's decodes raw formats 3, 4 and 5 and adds 15 to the
|
||||
recorded black for 3 and 4; LightCraft adds the same 15 for formats 4 and 6, measured on about 100 files,
|
||||
and uses 15 when the tags record no black (PhotoCraft assumed 0). Format 3 stays refused: no sample of it
|
||||
is known, and PhotoCraft's reading of it as format 4 is untested. PhotoCraft took white from the linearity
|
||||
limits (`0x000e`–`0x0010`); LightCraft measured those below the samples on extended-ISO shots and uses the
|
||||
saturation plateau. The ISO in the private IFD0 tags (`0x0017`, `0x0037`) is metadata, left to the
|
||||
metadata reader.
|
||||
- Lossy JPEG is accepted for CFA data (one grey component) as in PhotoCraft; FilmCraft refuses it
|
||||
because DNG 1.4 allows the coding for LinearRaw only.
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
//! | `mef` | Mamiya MEF (ZD): uncompressed 12-bit packed |
|
||||
//! | `kodak` | Kodak DCS520C TIFF: two-component lossless JPEG with a response curve |
|
||||
//! | `nef` | Nikon NEF and NRW: uncompressed, packed, Nikon's Huffman coding (lossless and lossy, 12 and 14 bits), small raw |
|
||||
//! | `rw2` | Panasonic RW2, Leica RWL and the older Panasonic / Leica RAW: packed, formats 4 and 6, format 8 prefix-coded strips, 16-bit words |
|
||||
//! | `pef` | Pentax PEF: uncompressed (packed or 16-bit words) and Pentax's Huffman coding (compression 65535) |
|
||||
//! | `srw` | Samsung SRW: the uncompressed variants (compression 32769 words, 32770 at 12 bits packed) |
|
||||
//! | `hasselblad` | Hasselblad 3FR and FFF (and other CFA TIFFs in the DNG layout without a DNG version): uncompressed and Hasselblad's lossless JPEG; implies `dng` |
|
||||
@@ -88,6 +89,8 @@ pub mod opcodes;
|
||||
mod pef;
|
||||
mod preview;
|
||||
pub mod profile;
|
||||
#[cfg(feature = "rw2")]
|
||||
mod rw2;
|
||||
#[cfg(feature = "dng")]
|
||||
pub mod semantic;
|
||||
// (the TIFF pixel plumbing every TIFF-based maker shares; DNG is its only user so far)
|
||||
@@ -336,6 +339,7 @@ impl RawFormat {
|
||||
|| (self == RawFormat::Mef && cfg!(feature = "mef"))
|
||||
|| (self == RawFormat::Cr2 && cfg!(feature = "cr2"))
|
||||
|| (matches!(self, RawFormat::Nef | RawFormat::Nrw) && cfg!(feature = "nef"))
|
||||
|| (self == RawFormat::Rw2 && cfg!(feature = "rw2"))
|
||||
|| (self == RawFormat::Pef && cfg!(feature = "pef"))
|
||||
|| (self == RawFormat::Srw && cfg!(feature = "srw"))
|
||||
}
|
||||
@@ -622,6 +626,8 @@ fn decode_in(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawImage> {
|
||||
RawFormat::Cr2 => cr2::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "nef")]
|
||||
RawFormat::Nef | RawFormat::Nrw => nef::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "rw2")]
|
||||
RawFormat::Rw2 => rw2::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "pef")]
|
||||
RawFormat::Pef => pef::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "srw")]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -87,6 +87,119 @@ pub(crate) fn white_from_data(data: &[u16], bits: u32) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rw2")]
|
||||
/// Output radii sampled to fit a radial warp.
|
||||
const DISTORTION_SAMPLES: usize = 64;
|
||||
|
||||
#[cfg(feature = "rw2")]
|
||||
/// Least-squares radial warp from a camera's forward distortion map.
|
||||
///
|
||||
/// `corrected` maps a source radius to the output radius it lands at; it must rise monotonically up to `t_max`, the
|
||||
/// output radius of the image corners. Radii use the caller's unit; pass `WarpRectilinear`'s normalisation (distance
|
||||
/// from the centre to the **farthest** corner = 1) so the result is directly a `WarpRectilinear` plane. The map is
|
||||
/// scanned at 4096 points up to a source radius of `3 * t_max`: it must reach `t_max` within that range and rise at
|
||||
/// every scan point (monotonicity is checked only there), otherwise `None`; a NaN at a scan point is rejected the
|
||||
/// same way, other non-finite values by the fit. The map is inverted by bisection and
|
||||
/// `source = u·(k0 + k1u² + k2u⁴ + k3u⁶)` is fitted over output radii `u·t_max`, `u` in [0, 1] ([`fit_radial_warp`]):
|
||||
/// the returned `[k0, k1, k2, k3]` give the source radius in the caller's unit as a function of `u`. `None` too when
|
||||
/// the worst fit error exceeds `max_fit_error`.
|
||||
pub(crate) fn warp_from_forward_map(corrected: &dyn Fn(f64) -> f64, t_max: f64, max_fit_error: f64) -> Option<[f64; 4]> {
|
||||
if !(t_max.is_finite() && t_max > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let mut top = None;
|
||||
let mut prev = 0.0;
|
||||
for i in 1..=4096 {
|
||||
let r = 3.0 * t_max * i as f64 / 4096.0;
|
||||
let v = corrected(r);
|
||||
if v.is_nan() || v <= prev {
|
||||
return None;
|
||||
}
|
||||
prev = v;
|
||||
if v >= t_max {
|
||||
top = Some(r);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let top = top?;
|
||||
let source = |t: f64| {
|
||||
let (mut lo, mut hi) = (0.0, top);
|
||||
for _ in 0..60 {
|
||||
let m = 0.5 * (lo + hi);
|
||||
if corrected(m) < t { lo = m } else { hi = m }
|
||||
}
|
||||
0.5 * (lo + hi)
|
||||
};
|
||||
fit_radial_warp(&|u| source(u * t_max), max_fit_error, 1.0)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rw2")]
|
||||
/// Least-squares `source = u·(k0 + k1u² + k2u⁴ + k3u⁶)` through `source_of_u` sampled at `u = i/64`, i = 0..=64
|
||||
/// (`u` = output radius, `WarpRectilinear` units, so the result is directly a plane's `[k0, k1, k2, k3]`). `None` when
|
||||
/// a sample is not finite, the normal equations are singular, or the worst fit error at the samples up to the first
|
||||
/// one at or beyond `gate_until` (the radii the image shows; 1 = all) exceeds `max_fit_error`.
|
||||
pub(crate) fn fit_radial_warp(source_of_u: &dyn Fn(f64) -> f64, max_fit_error: f64, gate_until: f64) -> Option<[f64; 4]> {
|
||||
let samples: Vec<(f64, f64)> = (0..=DISTORTION_SAMPLES)
|
||||
.map(|i| {
|
||||
let u = i as f64 / DISTORTION_SAMPLES as f64;
|
||||
(u, source_of_u(u))
|
||||
})
|
||||
.collect();
|
||||
if samples.iter().any(|&(_, v)| !v.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let basis = |u: f64| {
|
||||
let u2 = u * u;
|
||||
[u, u * u2, u * u2 * u2, u * u2 * u2 * u2]
|
||||
};
|
||||
let mut a = [[0.0; 4]; 4];
|
||||
let mut rhs = [0.0; 4];
|
||||
for &(u, v) in &samples {
|
||||
let f = basis(u);
|
||||
for (i, fi) in f.iter().enumerate() {
|
||||
for (j, fj) in f.iter().enumerate() {
|
||||
a[i][j] += fi * fj;
|
||||
}
|
||||
rhs[i] += fi * v;
|
||||
}
|
||||
}
|
||||
let k = solve4(a, rhs)?;
|
||||
let fit = |u: f64| basis(u).iter().zip(&k).map(|(f, c)| f * c).sum::<f64>();
|
||||
// rounded up to a sample, so the band between the last sample inside and `gate_until` itself is checked too;
|
||||
// capped at 1 (`min` also turns a NaN into 1: every sample is gated)
|
||||
let n = DISTORTION_SAMPLES as f64;
|
||||
let gate_until = (gate_until * n).ceil().min(n) / n;
|
||||
let worst = samples.iter().filter(|&&(u, _)| u <= gate_until).map(|&(u, v)| (fit(u) - v).abs()).fold(0.0, f64::max);
|
||||
(worst <= max_fit_error).then_some(k)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rw2")]
|
||||
/// Solve the 4 × 4 system `a·x = b` (Gaussian elimination with partial pivoting); `None` when singular or not finite.
|
||||
fn solve4(mut a: [[f64; 4]; 4], mut b: [f64; 4]) -> Option<[f64; 4]> {
|
||||
for c in 0..4 {
|
||||
let p = (c..4).max_by(|&i, &j| a[i][c].abs().total_cmp(&a[j][c].abs()))?;
|
||||
if a[p][c].is_nan() || a[p][c].abs() <= 1e-300 {
|
||||
return None;
|
||||
}
|
||||
a.swap(c, p);
|
||||
b.swap(c, p);
|
||||
for r in c + 1..4 {
|
||||
let f = a[r][c] / a[c][c];
|
||||
let (top, bottom) = a.split_at_mut(r);
|
||||
for (x, p) in bottom[0][c..].iter_mut().zip(&top[c][c..]) {
|
||||
*x -= f * p;
|
||||
}
|
||||
b[r] -= f * b[c];
|
||||
}
|
||||
}
|
||||
let mut x = [0.0; 4];
|
||||
for c in (0..4).rev() {
|
||||
let s: f64 = (c + 1..4).map(|k| a[c][k] * x[k]).sum();
|
||||
x[c] = (b[c] - s) / a[c][c];
|
||||
}
|
||||
x.iter().all(|v| v.is_finite()).then_some(x)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn declare_strip_len(file: &mut [u8], len: u32, declared: u32) {
|
||||
let big = file.starts_with(b"MM");
|
||||
@@ -186,4 +299,55 @@ pub(crate) mod tests {
|
||||
let w = white_from_data(&d, 14);
|
||||
assert!((13500.0..=13583.0).contains(&w), "{w}");
|
||||
}
|
||||
|
||||
#[cfg(feature = "rw2")]
|
||||
#[test]
|
||||
fn warp_from_forward_map_recovers_identity_and_rejects_folds() {
|
||||
let k = warp_from_forward_map(&|r| r, 1.0, 1e-9).unwrap();
|
||||
assert!((k[0] - 1.0).abs() < 1e-9 && k[1].abs() < 1e-9 && k[2].abs() < 1e-9 && k[3].abs() < 1e-9);
|
||||
let barrel = |r: f64| r * (1.0 + 0.05 * r * r); // mild barrel: source < output radius
|
||||
let t_max = 0.9;
|
||||
let k = warp_from_forward_map(&barrel, t_max, 1e-3).unwrap();
|
||||
assert!(k[0] < t_max && k[0] > 0.8 && k[1] < 0.0); // source radius at u = 1 is below t_max
|
||||
for i in 0..=20 {
|
||||
let u = i as f64 / 20.0;
|
||||
let s = u * (k[0] + u * u * (k[1] + u * u * (k[2] + u * u * k[3])));
|
||||
assert!((barrel(s) - u * t_max).abs() < 1e-5, "u {u}");
|
||||
}
|
||||
assert!(warp_from_forward_map(&|r| if r < 0.5 { r } else { 0.5 - (r - 0.5) }, 1.0, 1e-3).is_none()); // fold, never reaches t_max
|
||||
// a tiny dip (two scan points wide, 1e-4 deep) that still reaches t_max and fits well: only the monotonicity check rejects it
|
||||
let dip = |r: f64| {
|
||||
if r < 0.5 {
|
||||
r
|
||||
} else if r < 0.5015 {
|
||||
0.5 - 0.05 * (r - 0.5)
|
||||
} else {
|
||||
r - 0.001575
|
||||
}
|
||||
};
|
||||
assert!(warp_from_forward_map(&dip, 1.0, 1e-2).is_none());
|
||||
assert!(warp_from_forward_map(&|_| f64::NAN, 1.0, 1e-3).is_none());
|
||||
}
|
||||
|
||||
#[cfg(feature = "rw2")]
|
||||
#[test]
|
||||
fn fit_radial_warp_identity_and_gate() {
|
||||
let k = fit_radial_warp(&|u| u, 1e-9, 1.0).unwrap();
|
||||
assert!((k[0] - 1.0).abs() < 1e-9 && k[1].abs() < 1e-9 && k[2].abs() < 1e-9 && k[3].abs() < 1e-9, "{k:?}");
|
||||
assert!(fit_radial_warp(&|u| if u > 0.5 { f64::NAN } else { u }, 1.0, 1.0).is_none(), "a NaN sample");
|
||||
assert!(fit_radial_warp(&|u| if u > 0.5 { f64::INFINITY } else { u }, 1.0, 1.0).is_none(), "an infinite sample");
|
||||
assert!(fit_radial_warp(&|u: f64| u.sqrt(), 1.0, 1.0).is_some());
|
||||
assert!(fit_radial_warp(&|u: f64| u.sqrt(), 0.0, 1.0).is_none(), "not a polynomial: the gate rejects it");
|
||||
// a step at u = 1 alone: worst fit error 0.057 over all samples, 0.006 over u <= 0.9 (the fit is the same)
|
||||
let step = |u: f64| if u > 0.99 { 1.1 } else { u };
|
||||
assert!(fit_radial_warp(&step, 0.01, 1.0).is_none());
|
||||
assert_eq!(fit_radial_warp(&step, 0.01, 0.9), fit_radial_warp(&step, 1.0, 1.0), "gated only where shown");
|
||||
// the first sample at or beyond `gate_until` is gated too (the band up to it is shown): 0.023 up to
|
||||
// u = 62/64, 0.032 at 63/64
|
||||
assert!(fit_radial_warp(&step, 0.03, 62.0 / 64.0).is_some(), "a sample exactly at the bound");
|
||||
assert!(fit_radial_warp(&step, 0.03, 0.98).is_none(), "63/64 is the next sample beyond 0.98");
|
||||
// capped at 1, and a NaN gates every sample (fails closed)
|
||||
assert_eq!(fit_radial_warp(&step, 1.0, f64::INFINITY), fit_radial_warp(&step, 1.0, 1.0));
|
||||
assert!(fit_radial_warp(&step, 0.01, f64::NAN).is_none(), "a NaN gate");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,119 @@ fn cr2() -> Vec<u8> {
|
||||
TiffWriter::new(ByteOrder::Little, false).write(&[ifd0, blank(), blank(), ifd3]).unwrap()
|
||||
}
|
||||
|
||||
/// A Panasonic RW2: sensor size, bit depth and raw format in IFD0, 12-bit packed blocks.
|
||||
fn rw2() -> Vec<u8> {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(0x0002, Value::Short(vec![W as u16]));
|
||||
ifd.set(0x0003, Value::Short(vec![H as u16]));
|
||||
ifd.set(0x0004, Value::Short(vec![1]));
|
||||
ifd.set(0x0005, Value::Short(vec![2]));
|
||||
ifd.set(0x0006, Value::Short(vec![H as u16 - 1]));
|
||||
ifd.set(0x0007, Value::Short(vec![W as u16 - 2]));
|
||||
ifd.set(0x000a, Value::Short(vec![12]));
|
||||
ifd.set(0x002d, Value::Short(vec![5]));
|
||||
ifd.set(0x001c, Value::Short(vec![64]));
|
||||
ifd.set(0x001d, Value::Short(vec![64]));
|
||||
ifd.set(0x001e, Value::Short(vec![64]));
|
||||
ifd.set(t::MAKE, Value::Ascii("Panasonic".into()));
|
||||
let need = (W * H).div_ceil(10) as usize * 16;
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: H, strips: vec![(0..need).map(|i| (i * 29) as u8).collect()] });
|
||||
let mut b = TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).unwrap();
|
||||
b[..4].copy_from_slice(b"IIU\0");
|
||||
b
|
||||
}
|
||||
|
||||
/// A Panasonic RW2 of raw format `format` (or, with `format` 0, the oldest 16-bit-word kind): sensor size, borders,
|
||||
/// bit depth, black levels, an in-camera crop and `n` bytes of payload at the end, at `RawDataOffset`. Format 8 also
|
||||
/// gets two strips covering the payload and a code table.
|
||||
fn rw2_of(format: u16, bits: u16, n: usize) -> Vec<u8> {
|
||||
let u16s = |v: &[u16]| Value::Undefined(v.iter().flat_map(|x| x.to_le_bytes()).collect());
|
||||
let build = |off: u32| {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
for (tag, v) in [(2, W as u16), (3, H as u16), (4, 2), (5, 4), (6, H as u16), (7, W as u16), (9, 2), (0x0a, bits), (0x1c, 128), (0x1d, 128)] {
|
||||
ifd.set(tag, Value::Short(vec![v]));
|
||||
}
|
||||
for (tag, v) in [(0x2f, 2u16), (0x30, 6), (0x31, H as u16 - 2), (0x32, W as u16 - 2)] {
|
||||
ifd.set(tag, Value::Short(vec![v]));
|
||||
}
|
||||
if format == 0 {
|
||||
ifd.set(0x000b, Value::Short(vec![34828]));
|
||||
ifd.set(t::STRIP_OFFSETS, Value::Long(vec![off]));
|
||||
} else {
|
||||
ifd.set(0x002d, Value::Short(vec![format]));
|
||||
ifd.set(0x0118, Value::Long(vec![off]));
|
||||
}
|
||||
if format == 8 {
|
||||
let table = [
|
||||
(6, 62),
|
||||
(7, 126),
|
||||
(6, 61),
|
||||
(5, 28),
|
||||
(4, 12),
|
||||
(3, 4),
|
||||
(3, 2),
|
||||
(2, 0),
|
||||
(3, 3),
|
||||
(3, 5),
|
||||
(4, 13),
|
||||
(5, 29),
|
||||
(6, 60),
|
||||
(8, 254),
|
||||
(8, 255),
|
||||
(12, 4094),
|
||||
(12, 4095),
|
||||
];
|
||||
ifd.set(0x0040, u16s(&std::iter::once(17).chain(table.iter().flat_map(|&(l, c)| [l, c])).collect::<Vec<u16>>()));
|
||||
let half = (n / 2) as u32;
|
||||
let (a, b) = (off, off + half);
|
||||
ifd.set(0x0044, u16s(&[2, a as u16, (a >> 16) as u16, b as u16, (b >> 16) as u16]));
|
||||
ifd.set(0x0045, u16s(&[2, 0, 0, W as u16 / 2, 0]));
|
||||
ifd.set(0x0046, u16s(&[2, (half * 8) as u16, ((half * 8) >> 16) as u16, (half * 8) as u16, ((half * 8) >> 16) as u16]));
|
||||
ifd.set(0x0047, u16s(&[2, W as u16 / 2, W as u16 / 2]));
|
||||
ifd.set(0x0048, u16s(&[2, H as u16, H as u16]));
|
||||
}
|
||||
ifd.set(t::MAKE, Value::Ascii("Panasonic".into()));
|
||||
let mut b = TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).unwrap();
|
||||
b[..4].copy_from_slice(b"IIU\0");
|
||||
b
|
||||
};
|
||||
let off = build(0).len();
|
||||
let mut b = build(off as u32);
|
||||
if format == 8 {
|
||||
// a valid stream: the category-0 code `111110` over and over, each byte's bits from bit 0 up (every sample 0,
|
||||
// inside any bit depth)
|
||||
let bit = |k: usize| u8::from(k % 6 != 5);
|
||||
b.extend((0..n).map(|i| (0..8).fold(0u8, |a, j| a | bit(i * 8 + j) << j)));
|
||||
} else {
|
||||
b.extend((0..n).map(|i| (i * 89 % 253) as u8));
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// One RW2 per encoding with a sample decoder of its own (formats 4, 6 at 14 and 12 bits, 8, and 16-bit words).
|
||||
fn rw2_formats() -> Vec<Vec<u8>> {
|
||||
vec![
|
||||
rw2_of(4, 12, (W * H).div_ceil(14) as usize * 16),
|
||||
rw2_of(6, 14, (W * H).div_ceil(11) as usize * 16),
|
||||
rw2_of(6, 12, (W * H).div_ceil(14) as usize * 16),
|
||||
rw2_of(8, 14, (W * H) as usize * 2),
|
||||
rw2_of(0, 12, (W * H) as usize * 2),
|
||||
]
|
||||
}
|
||||
|
||||
/// The RW2 samples decode as they are, so the mutations reach every format's sample decoder.
|
||||
#[cfg(feature = "rw2")]
|
||||
#[test]
|
||||
fn rw2_samples_decode_unmutated() {
|
||||
for s in &rw2_formats() {
|
||||
let r = craft_raw::decode(s).unwrap();
|
||||
assert_eq!((r.width, r.height), (W as usize, H as usize));
|
||||
}
|
||||
}
|
||||
|
||||
fn samples() -> Vec<Vec<u8>> {
|
||||
vec![
|
||||
rw2(),
|
||||
cr2(),
|
||||
cfa_tiff("NIKON CORPORATION", 1, 12, ByteOrder::Big),
|
||||
cfa_tiff("NIKON CORPORATION", 34713, 14, ByteOrder::Big),
|
||||
@@ -69,6 +180,9 @@ fn samples() -> Vec<Vec<u8>> {
|
||||
cfa_tiff("SAMSUNG", 32769, 14, ByteOrder::Big),
|
||||
cfa_tiff("ACME", 1, 16, ByteOrder::Little),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(rw2_formats())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn exercise(bytes: &[u8]) {
|
||||
|
||||
Reference in New Issue
Block a user