#1065: clean-room DNG / CinemaDNG decoder crate
Problem: camera RAW is at 0% (docs/codec-parity.md, #383). CinemaDNG, the one cinema RAW format with a public specification, could not be read at all. Fix: new L0 crate filmcraft-dng, written from the public DNG 1.4-1.7, TIFF 6.0 / TIFF-EP, ITU-T T.81 and CinemaDNG specifications only (no other decoder's source read; editions in the crate README): - TIFF container with SubIFDs, bounded (64 IFDs, 4 levels, cycles end the walk) and checked offsets; the main raw IFD is the largest NewSubFileType 0 CFA / LinearRaw image. - Raw data: 1-16-bit uncompressed (MSB-first packing) and lossless JPEG (process 14, predictors 1-7, 1-4 components, point transform, line restarts), strips and tiles, tiles decoded in parallel; allocations capped by size and by the data present. - Processing per the DNG model: ActiveArea, LinearizationTable, per-site black levels with deltas, white levels, as-shot white balance with neutral highlight clipping, Malvar-He-Cutler Bayer demosaic (bilinear for other patterns), DefaultCrop, ColorMatrix / CameraCalibration / ForwardMatrix with illuminant interpolation, Bradford to linear Rec. 709, BaselineExposure; output scene-linear RGBA floats. - CinemaDNG FrameRate, TimeCodes and ReelName tags. - A test-only writer (feature `writer`) synthesises DNGs. Tests: raw samples bit-exact for every depth / predictor / layout, processing against hand-computed values, hostile files (every truncation, 12k mutations, cyclic IFDs, huge sizes, bad offsets), and LibRaw's unprocessed_raw as an external oracle reading the same samples (skipped when not installed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e2d0bc2d3
commit
28247afae7
16 files changed
+3286
-1
No files matched your search
Generated
+11
@@ -1853,6 +1853,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-dng"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"filmcraft-dng",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-dnx"
|
||||
version = "0.6.0"
|
||||
@@ -1884,6 +1893,7 @@ dependencies = [
|
||||
"filmcraft-captions",
|
||||
"filmcraft-codecs",
|
||||
"filmcraft-color",
|
||||
"filmcraft-dng",
|
||||
"filmcraft-edit",
|
||||
"filmcraft-export",
|
||||
"filmcraft-format",
|
||||
@@ -2076,6 +2086,7 @@ name = "filmcraft-media"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"filmcraft-color",
|
||||
"filmcraft-dng",
|
||||
"filmcraft-frame",
|
||||
"filmcraft-time",
|
||||
"image",
|
||||
|
||||
@@ -30,6 +30,7 @@ filmcraft-vp9 = { path = "crates/vp9" }
|
||||
filmcraft-prores = { path = "crates/prores" }
|
||||
filmcraft-dnx = { path = "crates/dnx" }
|
||||
filmcraft-apv = { path = "crates/apv" }
|
||||
filmcraft-dng = { path = "crates/dng" }
|
||||
filmcraft-av1 = { path = "crates/av1" }
|
||||
filmcraft-aac = { path = "crates/aac" }
|
||||
filmcraft-opus = { path = "crates/opus" }
|
||||
@@ -109,6 +110,9 @@ opt-level = 3
|
||||
[profile.dev.package.filmcraft-apv]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.filmcraft-dng]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.filmcraft-av1]
|
||||
opt-level = 3
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "filmcraft-dng"
|
||||
description = "Clean-room DNG / CinemaDNG camera RAW decoder (TIFF container, lossless JPEG, DNG colour pipeline)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["threads"]
|
||||
threads = ["dep:rayon"]
|
||||
# A small DNG and lossless JPEG writer for synthesising test files (not used by the app).
|
||||
writer = []
|
||||
|
||||
[dependencies]
|
||||
rayon = { workspace = true, optional = true }
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
filmcraft-dng = { path = ".", features = ["writer"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,88 @@
|
||||
# filmcraft-dng
|
||||
|
||||
Clean-room DNG and CinemaDNG camera RAW decoder in pure Rust. Layer L0: depends only on
|
||||
`thiserror` and (optional, feature `threads`, default on) `rayon`. Builds for
|
||||
`wasm32-unknown-unknown`; no `unsafe`. Implemented from the public specifications below only; the
|
||||
source of no other raw decoder (dcraw, LibRaw, rawspeed, darktable, RawTherapee, FFmpeg, the Adobe
|
||||
DNG SDK…) was read. LibRaw's `unprocessed_raw` sample program is run as an external test oracle
|
||||
when it is installed, never linked.
|
||||
|
||||
## Specifications
|
||||
|
||||
| Document | Edition | Used for |
|
||||
|---|---|---|
|
||||
| Adobe Digital Negative (DNG) Specification | 1.4.0.0 (2012) – 1.7.1.0 (2023) | raw IFD selection (NewSubFileType 0), CFA / LinearRaw data, BitsPerSample packing, LinearizationTable, BlackLevel (+ RepeatDim, DeltaH / DeltaV), WhiteLevel, ActiveArea, DefaultCrop, the colour model of chapter 6 (ColorMatrix, CameraCalibration, AnalogBalance, ForwardMatrix, AsShotNeutral / AsShotWhiteXY, CalibrationIlluminant interpolation, BaselineExposure) |
|
||||
| TIFF Revision 6.0 | June 1992 | header, IFDs, field types, strips and tiles, PlanarConfiguration, row padding |
|
||||
| TIFF/EP, ISO 12234-2 | 2001 | CFARepeatPatternDim, CFAPattern |
|
||||
| TIFF Technical Note 1 (SubIFDs) | 1993 | the SubIFD tree |
|
||||
| ITU-T T.81 \| ISO/IEC 10918-1 | 09/1992 | lossless JPEG (process 14, SOF3): Annex B syntax, Annex C Huffman tables, F.2.2.3 decoding, Annex H prediction and point transform |
|
||||
| CinemaDNG Image Data Format Specification | 1.1.0.0 (2009) | frames as numbered DNG files, FrameRate, TimeCodes, ReelName tags |
|
||||
| SMPTE ST 12-1 | 2014 | time code BCD packing (via the CinemaDNG TimeCodes tag) |
|
||||
| CIPA DC-008 (Exif) | 2.3 | LightSource codes → illuminant temperatures |
|
||||
| Malvar, He, Cutler, "High-quality linear interpolation for demosaicing of Bayer-patterned color images", IEEE ICASSP | 2004 | the Bayer demosaic filters |
|
||||
| McCamy, "Correlated color temperature as an explicit function of chromaticity coordinates", Color Res. Appl. 17(2) | 1992 | white xy → temperature for the calibration interpolation |
|
||||
| IEC 61966-2-1 (sRGB) | 1999 | Rec. 709 / sRGB primaries and D65 white |
|
||||
|
||||
## API
|
||||
|
||||
```rust
|
||||
if filmcraft_dng::sniff(&bytes) {
|
||||
let dng = filmcraft_dng::Dng::parse(&bytes)?;
|
||||
let info = dng.info()?; // size after crop, bits, CFA or LinearRaw, frame rate, time code, camera, reel
|
||||
let raw = dng.raw()?; // the stored samples (u16), bit-exact
|
||||
let img = dng.develop()?; // linear Rec. 709 RGBA f32 (alpha 1), info.width × info.height
|
||||
}
|
||||
let img = filmcraft_dng::decode(&bytes)?; // parse + develop
|
||||
```
|
||||
|
||||
`writer` (feature `writer`, tests only) writes synthetic DNGs: uncompressed at any depth or
|
||||
lossless JPEG with any predictor, strips or tiles, either byte order, an optional preview IFD with
|
||||
the raw image in a SubIFD, and arbitrary extra tags.
|
||||
|
||||
## What it decodes
|
||||
|
||||
- **Container:** little and big-endian TIFF, IFD chains and SubIFDs (at most 64 IFDs, 4 levels,
|
||||
cycles end the walk). The main image is the largest NewSubFileType 0 IFD with a CFA or LinearRaw
|
||||
photometric interpretation.
|
||||
- **Raw data:** 1–16-bit unsigned samples, uncompressed (8 and 16-bit in file byte order, other
|
||||
depths packed most significant bit first) or lossless JPEG (all predictors 1–7, 1–4 components,
|
||||
point transform, restart intervals of whole lines), in strips or tiles; tiles decode in
|
||||
parallel.
|
||||
- **Processing:** ActiveArea, LinearizationTable, per-site black levels with row / column deltas,
|
||||
per-sample white levels, as-shot white balance (clipped highlights stay neutral), demosaic
|
||||
(Malvar–He–Cutler for 2 × 2 Bayer patterns of any phase, bilinear for other patterns up to
|
||||
8 × 8), DefaultCrop, the DNG colour model to XYZ D50 and Bradford to linear Rec. 709, and
|
||||
BaselineExposure. LinearRaw (3 samples) skips the demosaic; monochrome LinearRaw is grey.
|
||||
- **Output:** scene-linear Rec. 709 floats: a white the sensor clipped is 1.0 × 2^BaselineExposure;
|
||||
brighter values are kept (FilmCraft's compositor works in linear float). Negative results of the
|
||||
colour matrix are clipped to 0. Black levels scale per pixel by (white − black).
|
||||
- **White balance without a ForwardMatrix** follows the specification: the camera matrix takes the
|
||||
values to XYZ and a Bradford adaptation moves the as-shot white to D50. Decoders in the dcraw
|
||||
tradition (LibRaw included) scale the camera channels instead, so under a white far from D65
|
||||
their saturated colours differ from ours (greys agree).
|
||||
- **Speed:** a 4096 × 2160 12-bit frame develops in about 80–100 ms on 16 threads (raw decode
|
||||
~15 ms, the rest demosaic and colour).
|
||||
|
||||
## Not supported (errors name them)
|
||||
|
||||
Lossy JPEG and JPEG XL DNG, deflate / floating-point DNG (HDR merges), planar raw data, colour
|
||||
filters other than red / green / blue (CMY, RGBE), non-rectangular CFA layouts, opcode lists
|
||||
(warp, vignette, bad pixels: ignored), DefaultScale and BestQualityScale (non-square pixels are
|
||||
shown square), DNG 1.6 IlluminantData for illuminant 255 (calibration 1 is used), lossless JPEG
|
||||
restart intervals that split a line, and MXF-wrapped CinemaDNG.
|
||||
|
||||
## Tests
|
||||
|
||||
- `tests/roundtrip.rs`: container parsing in both byte orders with and without a preview IFD;
|
||||
uncompressed samples bit-exact at 8–16 bits, strips and tiles; lossless JPEG bit-exact for every
|
||||
predictor and component count, full-swing 16-bit data (every difference category, SSSS = 16),
|
||||
restarts and point transform; developed values against hand-computed ones (black / white,
|
||||
white balance, ForwardMatrix, BaselineExposure, ActiveArea, DefaultCrop, LinearRaw); CinemaDNG
|
||||
FrameRate / TimeCodes / ReelName; hostile files (every truncation, 12 000 mutations under
|
||||
`catch_unwind`, cyclic IFDs, huge sizes, bad offsets, zero denominators).
|
||||
- `tests/oracle_libraw.rs`: LibRaw's `unprocessed_raw` reads the same samples from our synthetic
|
||||
files (uncompressed 8–16-bit in both byte orders, lossless JPEG predictors 1–7 with one and two
|
||||
components). Skipped when it is not installed.
|
||||
- Unit tests: matrix maths, the published sRGB matrix, McCamy temperatures, calibration
|
||||
interpolation, sRGB / ForwardMatrix cameras mapping to themselves, demosaic on flat fields
|
||||
(every Bayer phase), ramps and edges, black / white / linearisation maths.
|
||||
@@ -0,0 +1,385 @@
|
||||
//! Camera colour to linear Rec. 709, following the DNG specification's "Mapping Camera Color Space
|
||||
//! to CIE XYZ Space" (DNG 1.4–1.7 chapter 6):
|
||||
//!
|
||||
//! - The two calibrations (ColorMatrix, CameraCalibration, ForwardMatrix 1 and 2) are blended
|
||||
//! linearly in inverse correlated colour temperature between their CalibrationIlluminants.
|
||||
//! - The white balance is AsShotNeutral (found as a white xy by the specification's iteration) or
|
||||
//! AsShotWhiteXY; without either, D65.
|
||||
//! - With ForwardMatrix: CameraToXYZ_D50 = FM · D · (AB · CC)⁻¹, D = diag((AB · CC)⁻¹ · neutral)⁻¹.
|
||||
//! Without: CameraToXYZ_D50 = CA · (AB · CC · CM)⁻¹, CA a linear Bradford adaptation from the
|
||||
//! white to D50.
|
||||
//! - XYZ (D50) goes to linear Rec. 709 / sRGB primaries (D65 white) through Bradford again.
|
||||
//!
|
||||
//! The correlated colour temperature of an xy white comes from McCamy's cubic approximation
|
||||
//! (C. S. McCamy, "Correlated color temperature as an explicit function of chromaticity
|
||||
//! coordinates", Color Research & Application 17(2), 1992); the illuminant temperatures are the
|
||||
//! CIE / EXIF 2.3 LightSource values.
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
pub type M3 = [[f64; 3]; 3];
|
||||
pub type V3 = [f64; 3];
|
||||
|
||||
pub const IDENTITY: M3 = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||||
|
||||
/// CIE D50 and D65 white chromaticities (the DNG specification's D50).
|
||||
pub const D50: (f64, f64) = (0.3457, 0.3585);
|
||||
pub const D65: (f64, f64) = (0.3127, 0.3290);
|
||||
|
||||
/// Linear Bradford cone response matrix.
|
||||
const BRADFORD: M3 = [[0.8951, 0.2664, -0.1614], [-0.7502, 1.7135, 0.0367], [0.0389, -0.0685, 1.0296]];
|
||||
|
||||
pub fn mul(a: &M3, b: &M3) -> M3 {
|
||||
let mut m = [[0.0; 3]; 3];
|
||||
for (i, row) in m.iter_mut().enumerate() {
|
||||
for (j, v) in row.iter_mut().enumerate() {
|
||||
*v = (0..3).map(|k| a[i][k] * b[k][j]).sum();
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
pub fn mulv(a: &M3, v: &V3) -> V3 {
|
||||
[0, 1, 2].map(|i| a[i][0] * v[0] + a[i][1] * v[1] + a[i][2] * v[2])
|
||||
}
|
||||
|
||||
pub fn diag(v: &V3) -> M3 {
|
||||
[[v[0], 0.0, 0.0], [0.0, v[1], 0.0], [0.0, 0.0, v[2]]]
|
||||
}
|
||||
|
||||
pub fn scale(a: &M3, s: f64) -> M3 {
|
||||
a.map(|r| r.map(|v| v * s))
|
||||
}
|
||||
|
||||
fn blend(a: &M3, b: &M3, w: f64) -> M3 {
|
||||
let mut m = [[0.0; 3]; 3];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
m[i][j] = w * a[i][j] + (1.0 - w) * b[i][j];
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// The inverse of `a`, or `None` when it is (nearly) singular.
|
||||
pub fn inverse(a: &M3) -> Option<M3> {
|
||||
let c = |r: usize, s: usize| a[(r + 1) % 3][(s + 1) % 3] * a[(r + 2) % 3][(s + 2) % 3] - a[(r + 1) % 3][(s + 2) % 3] * a[(r + 2) % 3][(s + 1) % 3];
|
||||
let det = a[0][0] * c(0, 0) + a[0][1] * c(0, 1) + a[0][2] * c(0, 2);
|
||||
let norm: f64 = a.iter().flatten().map(|v| v.abs()).fold(0.0, f64::max);
|
||||
if !det.is_finite() || det.abs() <= 1e-12 * norm.powi(3).max(1e-300) {
|
||||
return None;
|
||||
}
|
||||
let mut m = [[0.0; 3]; 3];
|
||||
for (i, row) in m.iter_mut().enumerate() {
|
||||
for (j, v) in row.iter_mut().enumerate() {
|
||||
*v = c(j, i) / det;
|
||||
}
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// XYZ (Y = 1) of a chromaticity.
|
||||
pub fn xy_to_xyz((x, y): (f64, f64)) -> V3 {
|
||||
let y = y.max(1e-6);
|
||||
[x / y, 1.0, (1.0 - x - y) / y]
|
||||
}
|
||||
|
||||
pub fn xyz_to_xy(v: &V3) -> Option<(f64, f64)> {
|
||||
let s = v[0] + v[1] + v[2];
|
||||
(s.is_finite() && s > 1e-12).then(|| (v[0] / s, v[1] / s))
|
||||
}
|
||||
|
||||
/// Correlated colour temperature (kelvin) of a chromaticity, McCamy (1992), limited to
|
||||
/// 1 000–50 000 K.
|
||||
pub fn cct((x, y): (f64, f64)) -> f64 {
|
||||
let d = 0.1858 - y;
|
||||
if d.abs() < 1e-9 {
|
||||
return 50_000.0;
|
||||
}
|
||||
let n = (x - 0.3320) / d;
|
||||
let t = 449.0 * n.powi(3) + 3525.0 * n.powi(2) + 6823.3 * n + 5520.33;
|
||||
if t.is_finite() { t.clamp(1_000.0, 50_000.0) } else { 50_000.0 }
|
||||
}
|
||||
|
||||
/// Colour temperature of an EXIF LightSource code used as a DNG CalibrationIlluminant.
|
||||
pub fn illuminant_kelvin(code: u64) -> Option<f64> {
|
||||
Some(match code {
|
||||
1 | 4 | 9 => 5500.0, // daylight, flash, fine weather
|
||||
2 | 14 => 4150.0, // fluorescent, cool white fluorescent (W 3800–4500 K)
|
||||
3 => 2850.0, // tungsten
|
||||
10 => 6500.0, // cloudy
|
||||
11 => 7500.0, // shade
|
||||
12 => 6430.0, // daylight fluorescent (D 5700–7100 K)
|
||||
13 => 5050.0, // day white fluorescent (N 4600–5500 K)
|
||||
15 => 3525.0, // white fluorescent (WW 3250–3800 K)
|
||||
16 => 2925.0, // warm white fluorescent (L 2600–3250 K)
|
||||
17 => 2856.0, // standard light A
|
||||
18 => 4874.0, // standard light B
|
||||
19 => 6774.0, // standard light C
|
||||
20 => 5503.0, // D55
|
||||
21 => 6504.0, // D65
|
||||
22 => 7504.0, // D75
|
||||
23 => 5003.0, // D50
|
||||
24 => 3200.0, // ISO studio tungsten
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Linear Bradford adaptation from one white (XYZ) to another.
|
||||
pub fn bradford(src: &V3, dst: &V3) -> Option<M3> {
|
||||
let s = mulv(&BRADFORD, src);
|
||||
let d = mulv(&BRADFORD, dst);
|
||||
if s.iter().any(|v| v.abs() < 1e-12) {
|
||||
return None;
|
||||
}
|
||||
let g = [d[0] / s[0], d[1] / s[1], d[2] / s[2]];
|
||||
Some(mul(&inverse(&BRADFORD)?, &mul(&diag(&g), &BRADFORD)))
|
||||
}
|
||||
|
||||
/// Linear RGB (Rec. 709 / sRGB primaries, D65 white) → XYZ.
|
||||
pub fn rec709_to_xyz() -> Option<M3> {
|
||||
let p = [(0.64, 0.33), (0.30, 0.60), (0.15, 0.06)].map(xy_to_xyz);
|
||||
let pm: M3 = [[p[0][0], p[1][0], p[2][0]], [p[0][1], p[1][1], p[2][1]], [p[0][2], p[1][2], p[2][2]]];
|
||||
let s = mulv(&inverse(&pm)?, &xy_to_xyz(D65));
|
||||
Some(mul(&pm, &diag(&s)))
|
||||
}
|
||||
|
||||
/// XYZ relative to D50 → linear Rec. 709.
|
||||
pub fn xyz_d50_to_rec709() -> Option<M3> {
|
||||
let to_d65 = bradford(&xy_to_xyz(D50), &xy_to_xyz(D65))?;
|
||||
Some(mul(&inverse(&rec709_to_xyz()?)?, &to_d65))
|
||||
}
|
||||
|
||||
/// The colour tags of a three-colour DNG.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Calibration {
|
||||
pub color_matrix: [Option<M3>; 2],
|
||||
pub camera_calibration: [Option<M3>; 2],
|
||||
pub forward_matrix: [Option<M3>; 2],
|
||||
/// EXIF LightSource codes of the two calibrations.
|
||||
pub illuminant: [Option<u64>; 2],
|
||||
pub analog_balance: Option<V3>,
|
||||
pub as_shot_neutral: Option<V3>,
|
||||
pub as_shot_white_xy: Option<(f64, f64)>,
|
||||
pub baseline_exposure: f64,
|
||||
}
|
||||
|
||||
/// The result of [`Calibration::solve`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Solved {
|
||||
/// Camera neutral: raw (normalised) camera values of a white surface. Dividing by it white
|
||||
/// balances the camera values.
|
||||
pub neutral: V3,
|
||||
/// White-balanced camera values → linear Rec. 709, including the baseline exposure. The white
|
||||
/// balanced white (1, 1, 1) goes to Y = 2^BaselineExposure.
|
||||
pub matrix: M3,
|
||||
/// The white point found, for information.
|
||||
pub white_xy: (f64, f64),
|
||||
}
|
||||
|
||||
impl Calibration {
|
||||
/// Weight of calibration 1 for a white of temperature `t` (1 when only one is usable).
|
||||
fn weight(&self, t: f64) -> f64 {
|
||||
let (Some(_), Some(_)) = (self.color_matrix[0], self.color_matrix[1]) else { return 1.0 };
|
||||
let (Some(t1), Some(t2)) = (self.illuminant[0].and_then(illuminant_kelvin), self.illuminant[1].and_then(illuminant_kelvin)) else {
|
||||
return 1.0;
|
||||
};
|
||||
if (t1 - t2).abs() < 1.0 {
|
||||
return 1.0;
|
||||
}
|
||||
// the formula needs t1 < t2; swap the roles otherwise
|
||||
let (lo, hi, lo_is_1) = if t1 < t2 { (t1, t2, true) } else { (t2, t1, false) };
|
||||
let w_lo = if t <= lo {
|
||||
1.0
|
||||
} else if t >= hi {
|
||||
0.0
|
||||
} else {
|
||||
(1.0 / t - 1.0 / hi) / (1.0 / lo - 1.0 / hi)
|
||||
};
|
||||
if lo_is_1 { w_lo } else { 1.0 - w_lo }
|
||||
}
|
||||
|
||||
fn pick(&self, m: &[Option<M3>; 2], w: f64, default: M3) -> M3 {
|
||||
match (m[0], m[1]) {
|
||||
(Some(a), Some(b)) => blend(&a, &b, w),
|
||||
(Some(a), None) => a,
|
||||
(None, Some(b)) if self.color_matrix[0].is_none() => b,
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
|
||||
/// XYZ → camera at weight `w`: AB · CC · CM.
|
||||
fn xyz_to_camera(&self, w: f64) -> Option<M3> {
|
||||
let cm = match self.color_matrix {
|
||||
[None, None] => return None,
|
||||
_ => self.pick(&self.color_matrix, w, IDENTITY),
|
||||
};
|
||||
let cc = self.pick(&self.camera_calibration, w, IDENTITY);
|
||||
let ab = diag(&self.analog_balance.unwrap_or([1.0; 3]));
|
||||
Some(mul(&ab, &mul(&cc, &cm)))
|
||||
}
|
||||
|
||||
/// Work out the white balance and the camera → linear Rec. 709 matrix.
|
||||
pub fn solve(&self) -> Result<Solved> {
|
||||
let out_matrix = xyz_d50_to_rec709().ok_or(Error::Malformed("colour maths"))?;
|
||||
let exposure = 2f64.powf(self.baseline_exposure.clamp(-16.0, 16.0));
|
||||
if self.color_matrix.iter().all(Option::is_none) {
|
||||
// no colour calibration: the camera values are taken as Rec. 709, white balanced
|
||||
let neutral = self.as_shot_neutral.unwrap_or([1.0; 3]);
|
||||
if neutral.iter().any(|v| !v.is_finite() || *v <= 1e-9) {
|
||||
return Err(Error::Malformed("camera neutral must be positive"));
|
||||
}
|
||||
return Ok(Solved { neutral, matrix: scale(&IDENTITY, exposure), white_xy: D65 });
|
||||
}
|
||||
let bad = || Error::Malformed("singular colour matrix");
|
||||
// the white point and the camera neutral
|
||||
let (white_xy, neutral) = if let Some(n) = self.as_shot_neutral {
|
||||
let mut xy = D50;
|
||||
for _ in 0..50 {
|
||||
let w = self.weight(cct(xy));
|
||||
let to_xyz = inverse(&self.xyz_to_camera(w).ok_or_else(bad)?).ok_or_else(bad)?;
|
||||
let next = xyz_to_xy(&mulv(&to_xyz, &n)).ok_or(Error::Malformed("AsShotNeutral is not a colour"))?;
|
||||
let done = (next.0 - xy.0).abs() + (next.1 - xy.1).abs() < 1e-9;
|
||||
xy = next;
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(xy, n)
|
||||
} else {
|
||||
let xy = self.as_shot_white_xy.unwrap_or(D65);
|
||||
let w = self.weight(cct(xy));
|
||||
let n = mulv(&self.xyz_to_camera(w).ok_or_else(bad)?, &xy_to_xyz(xy));
|
||||
(xy, n)
|
||||
};
|
||||
if neutral.iter().any(|v| !v.is_finite() || *v <= 1e-9) {
|
||||
return Err(Error::Malformed("camera neutral must be positive"));
|
||||
}
|
||||
let w = self.weight(cct(white_xy));
|
||||
let ab_cc = mul(&diag(&self.analog_balance.unwrap_or([1.0; 3])), &self.pick(&self.camera_calibration, w, IDENTITY));
|
||||
let camera_to_xyz_d50 = if self.forward_matrix.iter().any(Option::is_some) {
|
||||
let fm = self.pick(&self.forward_matrix, w, IDENTITY);
|
||||
let inv_ab_cc = inverse(&ab_cc).ok_or_else(bad)?;
|
||||
let reference = mulv(&inv_ab_cc, &neutral);
|
||||
if reference.iter().any(|v| v.abs() < 1e-12) {
|
||||
return Err(bad());
|
||||
}
|
||||
mul(&fm, &mul(&diag(&reference.map(|v| 1.0 / v)), &inv_ab_cc))
|
||||
} else {
|
||||
let to_xyz = inverse(&self.xyz_to_camera(w).ok_or_else(bad)?).ok_or_else(bad)?;
|
||||
let ca = bradford(&xy_to_xyz(white_xy), &xy_to_xyz(D50)).ok_or_else(bad)?;
|
||||
mul(&ca, &to_xyz)
|
||||
};
|
||||
// white-balanced camera values: camera = neutral · wb
|
||||
let wb_to_xyz = mul(&camera_to_xyz_d50, &diag(&neutral));
|
||||
let y = mulv(&wb_to_xyz, &[1.0; 3])[1];
|
||||
if !y.is_finite() || y <= 1e-9 {
|
||||
return Err(bad());
|
||||
}
|
||||
let matrix = scale(&mul(&out_matrix, &wb_to_xyz), exposure / y);
|
||||
if matrix.iter().flatten().any(|v| !v.is_finite()) {
|
||||
return Err(bad());
|
||||
}
|
||||
Ok(Solved { neutral, matrix, white_xy })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn close(a: &M3, b: &M3, tol: f64) -> bool {
|
||||
a.iter().flatten().zip(b.iter().flatten()).all(|(x, y)| (x - y).abs() <= tol)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rec709_matrix_matches_the_published_one() {
|
||||
// IEC 61966-2-1 (sRGB) linear RGB → XYZ, four decimals
|
||||
let m = rec709_to_xyz().unwrap();
|
||||
let want = [[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722], [0.0193, 0.1192, 0.9505]];
|
||||
assert!(close(&m, &want, 1e-3), "{m:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_round_trips_and_refuses_singular() {
|
||||
let a = [[2.0, 1.0, 0.5], [0.1, 3.0, 0.2], [0.3, 0.4, 1.5]];
|
||||
assert!(close(&mul(&a, &inverse(&a).unwrap()), &IDENTITY, 1e-12));
|
||||
assert!(inverse(&[[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [0.0, 0.0, 1.0]]).is_none());
|
||||
assert!(inverse(&[[f64::NAN; 3]; 3]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mccamy_temperatures() {
|
||||
assert!((cct(D65) - 6504.0).abs() < 15.0, "{}", cct(D65));
|
||||
assert!((cct(D50) - 5003.0).abs() < 15.0, "{}", cct(D50));
|
||||
assert!((cct((0.44757, 0.40745)) - 2856.0).abs() < 15.0, "illuminant A");
|
||||
assert_eq!(cct((0.3320, 0.1858)), 50_000.0, "no division by zero");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolation_weight_follows_inverse_temperature() {
|
||||
let c = Calibration { color_matrix: [Some(IDENTITY), Some(IDENTITY)], illuminant: [Some(17), Some(21)], ..Default::default() };
|
||||
assert_eq!(c.weight(2000.0), 1.0);
|
||||
assert_eq!(c.weight(9000.0), 0.0);
|
||||
let mid = c.weight(4000.0);
|
||||
let want = (1.0 / 4000.0 - 1.0 / 6504.0) / (1.0 / 2856.0 - 1.0 / 6504.0);
|
||||
assert!((mid - want).abs() < 1e-12);
|
||||
// swapped order: weight of calibration 1 (now D65)
|
||||
let s = Calibration { illuminant: [Some(21), Some(17)], ..c.clone() };
|
||||
assert!((s.weight(4000.0) - (1.0 - want)).abs() < 1e-12);
|
||||
// unknown illuminant: calibration 1 only
|
||||
let u = Calibration { illuminant: [Some(0), Some(21)], ..c };
|
||||
assert_eq!(u.weight(4000.0), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_srgb_camera_maps_to_itself() {
|
||||
// ColorMatrix = XYZ → linear sRGB, white D65: the camera is Rec. 709 already
|
||||
let cm = inverse(&rec709_to_xyz().unwrap()).unwrap();
|
||||
let c = Calibration { color_matrix: [Some(cm), None], illuminant: [Some(21), None], as_shot_neutral: Some([1.0; 3]), ..Default::default() };
|
||||
let s = c.solve().unwrap();
|
||||
assert!(close(&s.matrix, &IDENTITY, 1e-6), "{:?}", s.matrix);
|
||||
assert!((s.white_xy.0 - D65.0).abs() < 1e-4 && (s.white_xy.1 - D65.1).abs() < 1e-4, "{:?}", s.white_xy);
|
||||
// a baseline exposure of +1 doubles everything
|
||||
let e = Calibration { baseline_exposure: 1.0, ..c }.solve().unwrap();
|
||||
assert!(close(&e.matrix, &scale(&IDENTITY, 2.0), 1e-6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_matrix_white_balances_by_the_neutral() {
|
||||
// FM = linear Rec. 709 → XYZ D50: the white-balanced camera values come out unchanged
|
||||
let fm = mul(&bradford(&xy_to_xyz(D65), &xy_to_xyz(D50)).unwrap(), &rec709_to_xyz().unwrap());
|
||||
let cm = inverse(&rec709_to_xyz().unwrap()).unwrap();
|
||||
let c = Calibration {
|
||||
color_matrix: [Some(cm), None],
|
||||
forward_matrix: [Some(fm), None],
|
||||
illuminant: [Some(21), None],
|
||||
as_shot_neutral: Some([0.5, 1.0, 0.8]),
|
||||
..Default::default()
|
||||
};
|
||||
let s = c.solve().unwrap();
|
||||
assert_eq!(s.neutral, [0.5, 1.0, 0.8]);
|
||||
assert!(close(&s.matrix, &IDENTITY, 1e-6), "{:?}", s.matrix);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_xy_gives_the_neutral() {
|
||||
let cm = inverse(&rec709_to_xyz().unwrap()).unwrap();
|
||||
let c = Calibration { color_matrix: [Some(cm), None], illuminant: [Some(21), None], as_shot_white_xy: Some(D65), ..Default::default() };
|
||||
let s = c.solve().unwrap();
|
||||
for v in s.neutral {
|
||||
assert!((v - s.neutral[1]).abs() < 1e-3, "D65 is neutral for an sRGB camera: {:?}", s.neutral);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_calibrations_are_errors() {
|
||||
let zero = Calibration { color_matrix: [Some([[0.0; 3]; 3]), None], as_shot_neutral: Some([1.0; 3]), ..Default::default() };
|
||||
assert!(zero.solve().is_err());
|
||||
let cm = inverse(&rec709_to_xyz().unwrap()).unwrap();
|
||||
let neg = Calibration { color_matrix: [Some(cm), None], as_shot_neutral: Some([-1.0, 1.0, 1.0]), ..Default::default() };
|
||||
assert!(neg.solve().is_err());
|
||||
let none = Calibration { as_shot_neutral: Some([0.5, 1.0, 0.5]), ..Default::default() }.solve().unwrap();
|
||||
assert_eq!(none.matrix, IDENTITY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
//! Demosaicing: one colour sample per pixel (a colour filter array) to three.
|
||||
//!
|
||||
//! - A 2 × 2 Bayer pattern (one red, two green, one blue) uses the gradient-corrected linear
|
||||
//! interpolation of H. S. Malvar, L.-W. He and R. Cutler, "High-quality linear interpolation for
|
||||
//! demosaicing of Bayer-patterned color images", IEEE ICASSP 2004: bilinear interpolation plus a
|
||||
//! Laplacian correction from the pixel's own colour, as 5 × 5 filters.
|
||||
//! - Any other pattern (up to 8 × 8) uses bilinear interpolation: the mean of the nearest samples
|
||||
//! of each missing colour in the 3 × 3 neighbourhood, else the 5 × 5 one.
|
||||
//!
|
||||
//! The picture is extended by two pixels on every side by mirroring (without repeating the edge),
|
||||
//! which keeps a 2 × 2 pattern's phase.
|
||||
|
||||
#[cfg(feature = "threads")]
|
||||
use rayon::prelude::*;
|
||||
|
||||
/// A CFA pattern: `rows` × `cols` colour indices (0 red, 1 green, 2 blue), origin at the top left
|
||||
/// of the image.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Pattern {
|
||||
pub rows: usize,
|
||||
pub cols: usize,
|
||||
pub colors: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
pub fn color(&self, x: usize, y: usize) -> u8 {
|
||||
self.colors.get((y % self.rows) * self.cols + x % self.cols).copied().unwrap_or(1)
|
||||
}
|
||||
|
||||
/// Whether this is a 2 × 2 Bayer pattern.
|
||||
pub fn is_bayer(&self) -> bool {
|
||||
if (self.rows, self.cols) != (2, 2) {
|
||||
return false;
|
||||
}
|
||||
let [a, b, c, d] = self.colors[..] else { return false };
|
||||
let count = |k: u8| [a, b, c, d].iter().filter(|&&v| v == k).count();
|
||||
// the two greens sit on a diagonal
|
||||
count(0) == 1 && count(1) == 2 && count(2) == 1 && ((a == 1 && d == 1) || (b == 1 && c == 1))
|
||||
}
|
||||
}
|
||||
|
||||
const PAD: usize = 2;
|
||||
|
||||
/// Mirror index `i` (which may be up to PAD outside) into 0..n.
|
||||
fn mirror(i: isize, n: usize) -> usize {
|
||||
let n = n as isize;
|
||||
let mut i = i;
|
||||
if i < 0 {
|
||||
i = -i;
|
||||
}
|
||||
if i >= n {
|
||||
i = 2 * (n - 1) - i;
|
||||
}
|
||||
i.clamp(0, n - 1) as usize
|
||||
}
|
||||
|
||||
/// The plane extended by PAD mirrored pixels on each side.
|
||||
fn padded(plane: &[f32], w: usize, h: usize) -> (Vec<f32>, usize) {
|
||||
let pw = w + 2 * PAD;
|
||||
let ph = h + 2 * PAD;
|
||||
let mut out = vec![0f32; pw * ph];
|
||||
for y in 0..ph {
|
||||
let sy = mirror(y as isize - PAD as isize, h);
|
||||
for x in 0..pw {
|
||||
let sx = mirror(x as isize - PAD as isize, w);
|
||||
out[y * pw + x] = plane.get(sy * w + sx).copied().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
(out, pw)
|
||||
}
|
||||
|
||||
/// Demosaic `plane` (`w` × `h`, one sample per pixel) into interleaved RGB.
|
||||
pub fn demosaic(plane: &[f32], w: usize, h: usize, pattern: &Pattern) -> Vec<f32> {
|
||||
let mut out = vec![0f32; w * h * 3];
|
||||
if w == 0 || h == 0 || plane.len() < w * h {
|
||||
return out;
|
||||
}
|
||||
let (p, pw) = padded(plane, w, h);
|
||||
let bayer = pattern.is_bayer() && w >= 3 && h >= 3;
|
||||
let row = |y: usize, o: &mut [f32]| {
|
||||
if bayer {
|
||||
malvar_row(&p, pw, y, w, pattern, o);
|
||||
} else {
|
||||
bilinear_row(&p, pw, y, w, h, pattern, o);
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "threads")]
|
||||
out.par_chunks_mut(w * 3).enumerate().for_each(|(y, o)| row(y, o));
|
||||
#[cfg(not(feature = "threads"))]
|
||||
out.chunks_mut(w * 3).enumerate().for_each(|(y, o)| row(y, o));
|
||||
out
|
||||
}
|
||||
|
||||
fn malvar_row(p: &[f32], pw: usize, y: usize, w: usize, pat: &Pattern, out: &mut [f32]) {
|
||||
let py = y + PAD;
|
||||
for x in 0..w {
|
||||
let px = x + PAD;
|
||||
let at = |dx: isize, dy: isize| p[((py as isize + dy) as usize) * pw + (px as isize + dx) as usize];
|
||||
let c = at(0, 0);
|
||||
let cross1 = at(-1, 0) + at(1, 0) + at(0, -1) + at(0, 1);
|
||||
let cross2 = at(-2, 0) + at(2, 0) + at(0, -2) + at(0, 2);
|
||||
let diag = at(-1, -1) + at(1, -1) + at(-1, 1) + at(1, 1);
|
||||
let me = pat.color(x, y);
|
||||
let rgb = match me {
|
||||
1 => {
|
||||
let h1 = at(-1, 0) + at(1, 0);
|
||||
let h2 = at(-2, 0) + at(2, 0);
|
||||
let v1 = at(0, -1) + at(0, 1);
|
||||
let v2 = at(0, -2) + at(0, 2);
|
||||
// colour along the row and along the column
|
||||
let along_row = (5.0 * c + 4.0 * h1 - h2 - diag + 0.5 * v2) / 8.0;
|
||||
let along_col = (5.0 * c + 4.0 * v1 - v2 - diag + 0.5 * h2) / 8.0;
|
||||
if pat.color(x + 1, y) == 0 { [along_row, c, along_col] } else { [along_col, c, along_row] }
|
||||
}
|
||||
_ => {
|
||||
let g = (4.0 * c + 2.0 * cross1 - cross2) / 8.0;
|
||||
let other = (6.0 * c + 2.0 * diag - 1.5 * cross2) / 8.0;
|
||||
if me == 0 { [c, g, other] } else { [other, g, c] }
|
||||
}
|
||||
};
|
||||
if let Some(o) = out.get_mut(x * 3..x * 3 + 3) {
|
||||
o.copy_from_slice(&rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bilinear_row(p: &[f32], pw: usize, y: usize, w: usize, h: usize, pat: &Pattern, out: &mut [f32]) {
|
||||
for x in 0..w {
|
||||
let me = pat.color(x, y);
|
||||
let mut rgb = [0f32; 3];
|
||||
for (k, v) in rgb.iter_mut().enumerate() {
|
||||
if usize::from(me) == k {
|
||||
*v = p[(y + PAD) * pw + x + PAD];
|
||||
continue;
|
||||
}
|
||||
for r in 1..=PAD as isize {
|
||||
let (mut sum, mut n) = (0f32, 0u32);
|
||||
for dy in -r..=r {
|
||||
for dx in -r..=r {
|
||||
// the colour of a mirrored neighbour is the colour where it was mirrored from
|
||||
let sx = mirror(x as isize + dx, w);
|
||||
let sy = mirror(y as isize + dy, h);
|
||||
if usize::from(pat.color(sx, sy)) == k {
|
||||
sum += p[(sy + PAD) * pw + sx + PAD];
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
*v = sum / n as f32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(o) = out.get_mut(x * 3..x * 3 + 3) {
|
||||
o.copy_from_slice(&rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn rggb() -> Pattern {
|
||||
Pattern { rows: 2, cols: 2, colors: vec![0, 1, 1, 2] }
|
||||
}
|
||||
|
||||
/// A mosaic of an RGB picture given per pixel.
|
||||
fn mosaic(w: usize, h: usize, pat: &Pattern, rgb: impl Fn(usize, usize) -> [f32; 3]) -> Vec<f32> {
|
||||
(0..h).flat_map(|y| (0..w).map(move |x| (x, y))).map(|(x, y)| rgb(x, y)[usize::from(pat.color(x, y))]).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bayer_detection() {
|
||||
assert!(rggb().is_bayer());
|
||||
for c in [[1, 0, 2, 1], [1, 2, 0, 1], [2, 1, 1, 0]] {
|
||||
assert!(Pattern { rows: 2, cols: 2, colors: c.to_vec() }.is_bayer(), "{c:?}");
|
||||
}
|
||||
assert!(!Pattern { rows: 2, cols: 2, colors: vec![0, 1, 2, 1] }.is_bayer(), "greens side by side");
|
||||
assert!(!Pattern { rows: 2, cols: 2, colors: vec![0, 0, 1, 2] }.is_bayer());
|
||||
assert!(!Pattern { rows: 1, cols: 3, colors: vec![0, 1, 2] }.is_bayer());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_fields_stay_flat_for_every_bayer_phase_and_other_patterns() {
|
||||
let pats = [[0, 1, 1, 2], [1, 0, 2, 1], [1, 2, 0, 1], [2, 1, 1, 0]].map(|c| Pattern { rows: 2, cols: 2, colors: c.to_vec() });
|
||||
let stripes = Pattern { rows: 1, cols: 3, colors: vec![0, 1, 2] };
|
||||
for pat in pats.iter().chain([&stripes]) {
|
||||
let (w, h) = (9, 7);
|
||||
let m = mosaic(w, h, pat, |_, _| [0.25, 0.5, 0.75]);
|
||||
let out = demosaic(&m, w, h, pat);
|
||||
for (i, px) in out.chunks(3).enumerate() {
|
||||
for (k, want) in [0.25, 0.5, 0.75].iter().enumerate() {
|
||||
assert!((px[k] - want).abs() < 1e-6, "{pat:?} pixel {i} channel {k}: {}", px[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_samples_are_kept_and_linear_ramps_are_exact() {
|
||||
// a horizontal ramp is reconstructed exactly away from the border (all filters are
|
||||
// symmetric and sum to one, the correction terms sum to zero)
|
||||
let pat = rggb();
|
||||
let (w, h) = (16, 12);
|
||||
let f = |x: usize, _y: usize| {
|
||||
let v = x as f32 / 16.0;
|
||||
[v, 0.5 * v + 0.1, 0.2 + 0.25 * v]
|
||||
};
|
||||
let m = mosaic(w, h, &pat, f);
|
||||
let out = demosaic(&m, w, h, &pat);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let px = &out[(y * w + x) * 3..][..3];
|
||||
let own = usize::from(pat.color(x, y));
|
||||
assert_eq!(px[own], m[y * w + x], "own sample kept");
|
||||
if (2..w - 2).contains(&x) {
|
||||
let want = f(x, y);
|
||||
for k in 0..3 {
|
||||
assert!((px[k] - want[k]).abs() < 1e-5, "({x},{y}) ch {k}: {} vs {}", px[k], want[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_vertical_edge_does_not_smear_far() {
|
||||
// white left half, black right half: pixels two columns from the edge are untouched
|
||||
let pat = rggb();
|
||||
let (w, h) = (12, 8);
|
||||
let m = mosaic(w, h, &pat, |x, _| if x < 6 { [1.0; 3] } else { [0.0; 3] });
|
||||
let out = demosaic(&m, w, h, &pat);
|
||||
for y in 0..h {
|
||||
for x in (0..3).chain(9..12) {
|
||||
let want = if x < 6 { 1.0 } else { 0.0 };
|
||||
for k in 0..3 {
|
||||
assert!((out[(y * w + x) * 3 + k] - want).abs() < 1e-6, "({x},{y}) ch {k}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_and_empty_pictures_do_not_panic() {
|
||||
let pat = rggb();
|
||||
for (w, h) in [(0, 0), (1, 1), (2, 1), (1, 5), (2, 2), (3, 2)] {
|
||||
let m = vec![0.5; w * h];
|
||||
let out = demosaic(&m, w, h, &pat);
|
||||
assert_eq!(out.len(), w * h * 3);
|
||||
}
|
||||
assert_eq!(demosaic(&[0.5; 3], 4, 4, &pat).len(), 48, "short plane");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
//! Clean-room DNG and CinemaDNG decoder, implemented from the public specifications only (see the
|
||||
//! crate README for editions): the TIFF 6.0 / TIFF-EP container, uncompressed and lossless JPEG
|
||||
//! (ITU-T T.81 process 14) raw data, and the DNG processing model (linearisation, black and white
|
||||
//! levels, white balance, demosaic, default crop, colour matrices) to a scene-linear Rec. 709
|
||||
//! picture.
|
||||
//!
|
||||
//! - [`sniff`]: is this a DNG (a TIFF with a DNGVersion tag)?
|
||||
//! - [`Dng::parse`] → [`Dng::info`] (size, bit depth, CinemaDNG frame rate and time code),
|
||||
//! [`Dng::raw`] (the raw samples) and [`Dng::develop`] (linear RGBA floats).
|
||||
//! - [`decode`]: parse and develop in one call.
|
||||
//!
|
||||
//! Every number in the file is hostile: offsets, counts and sizes are checked, allocations are
|
||||
//! capped by [`raw::MAX_SAMPLES`] and by the amount of data present, and the IFD walk is bounded.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub mod color;
|
||||
pub mod demosaic;
|
||||
pub mod ljpeg;
|
||||
pub mod process;
|
||||
pub mod raw;
|
||||
pub mod tiff;
|
||||
#[cfg(any(test, feature = "writer"))]
|
||||
pub mod writer;
|
||||
|
||||
use color::{Calibration, M3, V3};
|
||||
use demosaic::Pattern;
|
||||
use process::Params;
|
||||
use raw::{PHOTOMETRIC_CFA, RawImage};
|
||||
use tiff::{Entry, Ifd, Tiff, tags};
|
||||
|
||||
/// DNG decoding errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("not a DNG file")]
|
||||
NotDng,
|
||||
#[error("truncated DNG data")]
|
||||
Truncated,
|
||||
#[error("raw image too large")]
|
||||
TooLarge,
|
||||
#[error("malformed DNG: {0}")]
|
||||
Malformed(&'static str),
|
||||
#[error("unsupported DNG: {0}")]
|
||||
Unsupported(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Whether `b` is a DNG: a TIFF file whose first IFD has a DNGVersion tag.
|
||||
pub fn sniff(b: &[u8]) -> bool {
|
||||
let Ok((t, first)) = Tiff::new(b) else { return false };
|
||||
t.read_ifd(first, 0).is_ok_and(|(ifd, _)| ifd.get(tags::DNG_VERSION).is_some())
|
||||
}
|
||||
|
||||
/// SMPTE ST 12-1 time code of a CinemaDNG frame.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Timecode {
|
||||
pub hours: u8,
|
||||
pub minutes: u8,
|
||||
pub seconds: u8,
|
||||
pub frames: u8,
|
||||
pub drop_frame: bool,
|
||||
}
|
||||
|
||||
impl Timecode {
|
||||
/// The first time code of a CinemaDNG TimeCodes tag (SMPTE ST 331 / 12-1 packing: BCD frames,
|
||||
/// seconds, minutes, hours in bytes 0–3, the drop-frame flag in bit 6 of byte 0).
|
||||
pub fn from_bytes(b: &[u8]) -> Option<Timecode> {
|
||||
let bcd = |v: u8, tens_mask: u8| {
|
||||
let (u, t) = (v & 0x0F, (v >> 4) & tens_mask);
|
||||
(u <= 9).then_some(t * 10 + u)
|
||||
};
|
||||
let [f, s, m, h, ..] = *b else { return None };
|
||||
let tc = Timecode { frames: bcd(f, 0x3)?, seconds: bcd(s, 0x7)?, minutes: bcd(m, 0x7)?, hours: bcd(h, 0x3)?, drop_frame: f & 0x40 != 0 };
|
||||
(tc.seconds < 60 && tc.minutes < 60 && tc.hours < 24).then_some(tc)
|
||||
}
|
||||
}
|
||||
|
||||
/// What a DNG holds, without decoding the pixels.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Info {
|
||||
/// Size of the developed picture (the default crop).
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
/// Size of the stored raw image.
|
||||
pub raw_width: usize,
|
||||
pub raw_height: usize,
|
||||
pub bits: u32,
|
||||
pub samples_per_pixel: usize,
|
||||
/// Colour filter array data (else LinearRaw).
|
||||
pub cfa: bool,
|
||||
/// "uncompressed" or "lossless JPEG".
|
||||
pub compression: &'static str,
|
||||
pub dng_version: [u8; 4],
|
||||
/// CinemaDNG FrameRate (numerator, denominator).
|
||||
pub frame_rate: Option<(i64, i64)>,
|
||||
/// CinemaDNG time code of this frame.
|
||||
pub timecode: Option<Timecode>,
|
||||
/// UniqueCameraModel, else Make and Model.
|
||||
pub camera: Option<String>,
|
||||
/// CinemaDNG ReelName.
|
||||
pub reel: Option<String>,
|
||||
}
|
||||
|
||||
/// A developed DNG: premultiplied, scene-linear Rec. 709 RGBA floats (alpha 1), top row first.
|
||||
/// A white surface the sensor saw at its clipping level is 1.0 times 2^BaselineExposure.
|
||||
pub struct Image {
|
||||
pub info: Info,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub rgba: Vec<f32>,
|
||||
}
|
||||
|
||||
/// A parsed DNG file.
|
||||
pub struct Dng<'a> {
|
||||
tiff: Tiff<'a>,
|
||||
ifds: Vec<Ifd>,
|
||||
/// Index of the main raw IFD in `ifds` (the first is IFD 0).
|
||||
raw: usize,
|
||||
}
|
||||
|
||||
impl<'a> Dng<'a> {
|
||||
/// Parse the container and find the main raw image.
|
||||
pub fn parse(bytes: &'a [u8]) -> Result<Dng<'a>> {
|
||||
let (tiff, first) = Tiff::new(bytes)?;
|
||||
let ifds = tiff.all_ifds(first)?;
|
||||
if ifds.first().and_then(|i| i.get(tags::DNG_VERSION)).is_none() {
|
||||
return Err(Error::NotDng);
|
||||
}
|
||||
let main = raw::find_raw_ifd(&tiff, &ifds)?;
|
||||
let raw = ifds.iter().position(|i| i.offset == main.offset).ok_or(Error::Malformed("raw IFD"))?;
|
||||
Ok(Dng { tiff, ifds, raw })
|
||||
}
|
||||
|
||||
fn ifd0(&self) -> &Ifd {
|
||||
static EMPTY: Ifd = Ifd { offset: 0, entries: Vec::new(), depth: 0 };
|
||||
self.ifds.first().unwrap_or(&EMPTY)
|
||||
}
|
||||
|
||||
fn raw_ifd(&self) -> &Ifd {
|
||||
self.ifds.get(self.raw).unwrap_or(self.ifd0())
|
||||
}
|
||||
|
||||
/// A raw-image tag: from the raw IFD, else IFD 0.
|
||||
fn raw_tag(&self, tag: u16) -> Option<&Entry> {
|
||||
self.raw_ifd().get(tag).or_else(|| self.ifd0().get(tag))
|
||||
}
|
||||
|
||||
/// A camera-wide tag: from IFD 0, else the raw IFD.
|
||||
fn main_tag(&self, tag: u16) -> Option<&Entry> {
|
||||
self.ifd0().get(tag).or_else(|| self.raw_ifd().get(tag))
|
||||
}
|
||||
|
||||
/// A numeric tag's values; a value outside the file (or a zero denominator) is an error, so a
|
||||
/// damaged file does not silently lose its white balance.
|
||||
fn floats(&self, e: Option<&Entry>) -> Result<Option<Vec<f64>>> {
|
||||
e.map(|e| self.tiff.floats(e)).transpose()
|
||||
}
|
||||
|
||||
/// A 3 × 3 matrix tag (`None` when absent or not 3 × 3, e.g. for other colour counts).
|
||||
fn matrix(&self, tag: u16) -> Result<Option<M3>> {
|
||||
let Some(v) = self.floats(self.main_tag(tag))? else { return Ok(None) };
|
||||
let Ok(v) = <[f64; 9]>::try_from(v.as_slice()) else { return Ok(None) };
|
||||
Ok(Some([[v[0], v[1], v[2]], [v[3], v[4], v[5]], [v[6], v[7], v[8]]]))
|
||||
}
|
||||
|
||||
fn vec3(&self, tag: u16) -> Result<Option<V3>> {
|
||||
Ok(self.floats(self.main_tag(tag))?.and_then(|v| <[f64; 3]>::try_from(v.as_slice()).ok()))
|
||||
}
|
||||
|
||||
fn text(&self, tag: u16) -> Option<String> {
|
||||
self.main_tag(tag).and_then(|e| self.tiff.ascii(e).ok()).filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// The CFA pattern in camera planes (`None` for LinearRaw).
|
||||
fn cfa(&self) -> Result<Option<Pattern>> {
|
||||
let l = raw::layout(&self.tiff, self.raw_ifd())?;
|
||||
if l.photometric != PHOTOMETRIC_CFA {
|
||||
return Ok(None);
|
||||
}
|
||||
let dims = self.raw_tag(tags::CFA_REPEAT_PATTERN_DIM).map(|e| self.tiff.uints(e)).transpose()?.unwrap_or_else(|| vec![2, 2]);
|
||||
let (rows, cols) = match dims[..] {
|
||||
[r, c, ..] => (r as usize, c as usize),
|
||||
_ => return Err(Error::Malformed("CFARepeatPatternDim")),
|
||||
};
|
||||
if !(1..=8).contains(&rows) || !(1..=8).contains(&cols) {
|
||||
return Err(Error::Unsupported(format!("{rows} × {cols} CFA pattern")));
|
||||
}
|
||||
let pattern = self.tiff.uints(self.raw_tag(tags::CFA_PATTERN).ok_or(Error::Malformed("no CFAPattern"))?)?;
|
||||
if pattern.len() < rows * cols {
|
||||
return Err(Error::Malformed("CFAPattern shorter than its repeat"));
|
||||
}
|
||||
let planes = match self.raw_tag(tags::CFA_PLANE_COLOR) {
|
||||
Some(e) => self.tiff.uints(e)?,
|
||||
None => vec![0, 1, 2],
|
||||
};
|
||||
let mut sorted = planes.clone();
|
||||
sorted.sort_unstable();
|
||||
if sorted != [0, 1, 2] {
|
||||
return Err(Error::Unsupported("CFA colours other than red, green and blue".into()));
|
||||
}
|
||||
if let Some(e) = self.raw_tag(tags::CFA_LAYOUT)
|
||||
&& self.tiff.uint(e)? != 1
|
||||
{
|
||||
return Err(Error::Unsupported("a non-rectangular CFA layout".into()));
|
||||
}
|
||||
let colors = pattern[..rows * cols]
|
||||
.iter()
|
||||
.map(|&v| planes.iter().position(|&p| p == v).map(|i| i as u8).ok_or(Error::Malformed("CFAPattern colour not in CFAPlaneColor")))
|
||||
.collect::<Result<Vec<u8>>>()?;
|
||||
Ok(Some(Pattern { rows, cols, colors }))
|
||||
}
|
||||
|
||||
/// The processing tags of the raw image.
|
||||
pub fn params(&self) -> Result<Params> {
|
||||
let uint_pair = |tag| -> Result<Option<(usize, usize)>> {
|
||||
match self.raw_tag(tag) {
|
||||
Some(e) => match self.tiff.uints(e)?[..] {
|
||||
[a, b, ..] => Ok(Some((a as usize, b as usize))),
|
||||
_ => Err(Error::Malformed("short field")),
|
||||
},
|
||||
None => Ok(None),
|
||||
}
|
||||
};
|
||||
let active_area = match self.raw_tag(tags::ACTIVE_AREA) {
|
||||
Some(e) => match self.tiff.uints(e)?[..] {
|
||||
[t, l, b, r, ..] => Some([t, l, b, r].map(|v| usize::try_from(v).unwrap_or(usize::MAX))),
|
||||
_ => return Err(Error::Malformed("ActiveArea")),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let linearization = match self.raw_tag(tags::LINEARIZATION_TABLE) {
|
||||
Some(e) => {
|
||||
let v = self.tiff.uints(e)?;
|
||||
if v.len() > 65_536 {
|
||||
return Err(Error::Malformed("LinearizationTable longer than 65536"));
|
||||
}
|
||||
Some(v.into_iter().map(|x| x.min(65_535) as u16).collect())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let pair = |tag| -> Result<Option<(f64, f64)>> {
|
||||
match self.raw_tag(tag) {
|
||||
Some(e) => match self.tiff.floats(e)?[..] {
|
||||
[a, b, ..] => Ok(Some((a, b))),
|
||||
_ => Err(Error::Malformed("short field")),
|
||||
},
|
||||
None => Ok(None),
|
||||
}
|
||||
};
|
||||
let list = |tag| -> Result<Vec<f64>> { self.raw_tag(tag).map(|e| self.tiff.floats(e)).transpose().map(Option::unwrap_or_default) };
|
||||
Ok(Params {
|
||||
active_area,
|
||||
linearization,
|
||||
black_repeat: uint_pair(tags::BLACK_LEVEL_REPEAT_DIM)?.unwrap_or((1, 1)),
|
||||
black: list(tags::BLACK_LEVEL)?,
|
||||
black_delta_h: list(tags::BLACK_LEVEL_DELTA_H)?,
|
||||
black_delta_v: list(tags::BLACK_LEVEL_DELTA_V)?,
|
||||
white: list(tags::WHITE_LEVEL)?,
|
||||
crop_origin: pair(tags::DEFAULT_CROP_ORIGIN)?,
|
||||
crop_size: pair(tags::DEFAULT_CROP_SIZE)?,
|
||||
cfa: self.cfa()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// The colour tags.
|
||||
pub fn calibration(&self) -> Result<Calibration> {
|
||||
let illum = |tag| self.main_tag(tag).map(|e| self.tiff.uint(e)).transpose();
|
||||
let white_xy = self.floats(self.main_tag(tags::AS_SHOT_WHITE_XY))?.and_then(|v| match v[..] {
|
||||
[x, y] => Some((x, y)),
|
||||
_ => None,
|
||||
});
|
||||
Ok(Calibration {
|
||||
color_matrix: [self.matrix(tags::COLOR_MATRIX_1)?, self.matrix(tags::COLOR_MATRIX_2)?],
|
||||
camera_calibration: [self.matrix(tags::CAMERA_CALIBRATION_1)?, self.matrix(tags::CAMERA_CALIBRATION_2)?],
|
||||
forward_matrix: [self.matrix(tags::FORWARD_MATRIX_1)?, self.matrix(tags::FORWARD_MATRIX_2)?],
|
||||
illuminant: [illum(tags::CALIBRATION_ILLUMINANT_1)?, illum(tags::CALIBRATION_ILLUMINANT_2)?],
|
||||
analog_balance: self.vec3(tags::ANALOG_BALANCE)?,
|
||||
as_shot_neutral: self.vec3(tags::AS_SHOT_NEUTRAL)?,
|
||||
as_shot_white_xy: white_xy,
|
||||
baseline_exposure: self.floats(self.main_tag(tags::BASELINE_EXPOSURE))?.and_then(|v| v.first().copied()).unwrap_or(0.0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Size, depth and CinemaDNG metadata.
|
||||
pub fn info(&self) -> Result<Info> {
|
||||
let l = raw::layout(&self.tiff, self.raw_ifd())?;
|
||||
let p = self.params()?;
|
||||
let (_, _, aw, ah) = process::active_rect(&p, l.width, l.height)?;
|
||||
let (_, _, width, height) = process::crop_rect(&p, aw, ah);
|
||||
let dng_version = self
|
||||
.main_tag(tags::DNG_VERSION)
|
||||
.and_then(|e| self.tiff.uints(e).ok())
|
||||
.and_then(|v| v.get(..4).map(|v| [0, 1, 2, 3].map(|i| v[i] as u8)))
|
||||
.unwrap_or_default();
|
||||
let frame_rate = self.main_tag(tags::FRAME_RATE).and_then(|e| {
|
||||
let b = self.tiff.value_bytes(e).ok()?;
|
||||
if !matches!(e.ty, tiff::Type::SRational | tiff::Type::Rational) {
|
||||
return None;
|
||||
}
|
||||
let n = i64::from(self.tiff.u32_of(b.get(0..4)?.try_into().ok()?) as i32);
|
||||
let d = i64::from(self.tiff.u32_of(b.get(4..8)?.try_into().ok()?) as i32);
|
||||
(n > 0 && d > 0 && n <= 1000 * d && n * 1000 >= d).then_some((n, d))
|
||||
});
|
||||
let timecode = self.main_tag(tags::TIME_CODES).and_then(|e| self.tiff.value_bytes(e).ok()).and_then(Timecode::from_bytes);
|
||||
let camera = self.text(tags::UNIQUE_CAMERA_MODEL).or_else(|| {
|
||||
let mm = [self.text(tags::MAKE), self.text(tags::MODEL)].into_iter().flatten().collect::<Vec<_>>().join(" ");
|
||||
(!mm.is_empty()).then_some(mm)
|
||||
});
|
||||
Ok(Info {
|
||||
width,
|
||||
height,
|
||||
raw_width: l.width,
|
||||
raw_height: l.height,
|
||||
bits: l.bits,
|
||||
samples_per_pixel: l.samples_per_pixel,
|
||||
cfa: p.cfa.is_some(),
|
||||
compression: if l.compression == 7 { "lossless JPEG" } else { "uncompressed" },
|
||||
dng_version,
|
||||
frame_rate,
|
||||
timecode,
|
||||
camera,
|
||||
reel: self.text(tags::REEL_NAME),
|
||||
})
|
||||
}
|
||||
|
||||
/// The raw samples of the main image, as stored (before any processing).
|
||||
pub fn raw(&self) -> Result<RawImage> {
|
||||
raw::read(&self.tiff, self.raw_ifd())
|
||||
}
|
||||
|
||||
/// Develop the main image to scene-linear Rec. 709.
|
||||
pub fn develop(&self) -> Result<Image> {
|
||||
let info = self.info()?;
|
||||
let p = self.params()?;
|
||||
let raw = self.raw()?;
|
||||
let calibration = self.calibration()?;
|
||||
let solved = if p.cfa.is_none() && raw.samples_per_pixel == 1 {
|
||||
// monochrome: no colour, only the exposure
|
||||
let e = 2f64.powf(calibration.baseline_exposure.clamp(-16.0, 16.0));
|
||||
color::Solved { neutral: [1.0; 3], matrix: color::scale(&color::IDENTITY, e), white_xy: color::D65 }
|
||||
} else {
|
||||
calibration.solve()?
|
||||
};
|
||||
let d = process::develop(&raw, &p, &solved.neutral, &solved.matrix)?;
|
||||
Ok(Image { info, width: d.width, height: d.height, rgba: d.rgba })
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse and develop a DNG file.
|
||||
pub fn decode(bytes: &[u8]) -> Result<Image> {
|
||||
Dng::parse(bytes)?.develop()
|
||||
}
|
||||
|
||||
/// Parse a DNG file's metadata.
|
||||
pub fn probe(bytes: &[u8]) -> Result<Info> {
|
||||
Dng::parse(bytes)?.info()
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
//! Lossless JPEG decoding: ITU-T T.81 | ISO/IEC 10918-1 Annex H (process 14, Huffman coded,
|
||||
//! "SOF3"), the compression DNG calls lossless JPEG (Compression = 7).
|
||||
//!
|
||||
//! - Frame header (B.2.2): 2–16-bit precision, 1–4 components, each sampled 1×1.
|
||||
//! - Huffman tables (B.2.4.2, Annex C) decoded with a 9-bit lookup and the F.2.2.3 code-length
|
||||
//! walk for longer codes.
|
||||
//! - Scan header (B.2.3): one scan holding every component, predictor `Ss` 1–7 (Table H.1) and
|
||||
//! point transform `Al`.
|
||||
//! - Prediction (H.1.2.1): the first line (and the first line after a restart) predicts from the
|
||||
//! left neighbour with 2^(P − Pt − 1) for its first sample; later lines predict their first
|
||||
//! sample from the one above and the rest with the selected predictor. Differences are added
|
||||
//! modulo 2^16 (H.1.2.2) and the SSSS = 16 category is 32768 with no extra bits.
|
||||
//! - Restart intervals (DRI, RSTn) that span whole lines.
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A decoded lossless JPEG image.
|
||||
#[derive(Debug)]
|
||||
pub struct Decoded {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub components: usize,
|
||||
pub precision: u8,
|
||||
pub predictor: u8,
|
||||
/// Samples, interleaved by component, line after line, shifted back up by the point transform.
|
||||
pub samples: Vec<u16>,
|
||||
}
|
||||
|
||||
const LOOKUP_BITS: u32 = 9;
|
||||
|
||||
/// A Huffman table for decoding (T.81 Annex C and F.2.2.3).
|
||||
#[derive(Clone)]
|
||||
struct Huffman {
|
||||
/// For codes of up to LOOKUP_BITS bits: (length, value) indexed by the next LOOKUP_BITS bits;
|
||||
/// length 0 means "longer code".
|
||||
fast: Vec<(u8, u8)>,
|
||||
maxcode: [i32; 17],
|
||||
valptr: [i32; 17],
|
||||
mincode: [i32; 17],
|
||||
values: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Huffman {
|
||||
fn new(bits: &[u8; 16], values: Vec<u8>) -> Result<Huffman> {
|
||||
// C.2: code sizes and codes
|
||||
let mut sizes = Vec::new();
|
||||
for (l, &n) in bits.iter().enumerate() {
|
||||
for _ in 0..n {
|
||||
sizes.push(l as u8 + 1);
|
||||
}
|
||||
}
|
||||
if sizes.len() != values.len() || sizes.is_empty() {
|
||||
return Err(Error::Malformed("lossless JPEG: Huffman table counts"));
|
||||
}
|
||||
let mut codes = Vec::with_capacity(sizes.len());
|
||||
let mut code: u32 = 0;
|
||||
let mut si = sizes[0];
|
||||
let mut k = 0;
|
||||
while k < sizes.len() {
|
||||
while k < sizes.len() && sizes[k] == si {
|
||||
codes.push(code);
|
||||
code += 1;
|
||||
k += 1;
|
||||
}
|
||||
// the codes of one length must fit in that length
|
||||
if code > (1u32 << si) {
|
||||
return Err(Error::Malformed("lossless JPEG: Huffman table over-subscribed"));
|
||||
}
|
||||
code <<= 1;
|
||||
si += 1;
|
||||
}
|
||||
// F.2.2.3 decoder tables
|
||||
let mut maxcode = [-1i32; 17];
|
||||
let mut valptr = [0i32; 17];
|
||||
let mut mincode = [0i32; 17];
|
||||
let mut j = 0usize;
|
||||
for l in 1..=16usize {
|
||||
let n = usize::from(bits[l - 1]);
|
||||
if n == 0 {
|
||||
maxcode[l] = -1;
|
||||
} else {
|
||||
valptr[l] = j as i32;
|
||||
mincode[l] = codes[j] as i32;
|
||||
j += n;
|
||||
maxcode[l] = codes[j - 1] as i32;
|
||||
}
|
||||
}
|
||||
let mut fast = vec![(0u8, 0u8); 1 << LOOKUP_BITS];
|
||||
for (i, (&c, &s)) in codes.iter().zip(&sizes).enumerate() {
|
||||
let s = u32::from(s);
|
||||
if s <= LOOKUP_BITS {
|
||||
let shift = LOOKUP_BITS - s;
|
||||
let base = (c << shift) as usize;
|
||||
for f in fast.iter_mut().skip(base).take(1 << shift) {
|
||||
*f = (s as u8, values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Huffman { fast, maxcode, valptr, mincode, values })
|
||||
}
|
||||
}
|
||||
|
||||
/// Entropy-coded data reader: removes stuffed zero bytes and stops at markers (feeding zeros).
|
||||
struct Bits<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
acc: u64,
|
||||
n: u32,
|
||||
/// The marker that ended the data (after 0xFF), if one was met.
|
||||
marker: Option<u8>,
|
||||
}
|
||||
|
||||
impl<'a> Bits<'a> {
|
||||
fn new(data: &'a [u8], pos: usize) -> Self {
|
||||
Bits { data, pos, acc: 0, n: 0, marker: None }
|
||||
}
|
||||
|
||||
fn fill(&mut self) {
|
||||
while self.n <= 56 {
|
||||
let mut byte = 0u8;
|
||||
if self.marker.is_none() {
|
||||
match self.data.get(self.pos) {
|
||||
Some(&0xFF) => match self.data.get(self.pos + 1) {
|
||||
Some(&0x00) => {
|
||||
byte = 0xFF;
|
||||
self.pos += 2;
|
||||
}
|
||||
Some(&m) => self.marker = Some(m),
|
||||
None => self.marker = Some(0xD9),
|
||||
},
|
||||
Some(&b) => {
|
||||
byte = b;
|
||||
self.pos += 1;
|
||||
}
|
||||
None => self.marker = Some(0xD9),
|
||||
}
|
||||
}
|
||||
self.acc |= u64::from(byte) << (56 - self.n);
|
||||
self.n += 8;
|
||||
}
|
||||
}
|
||||
|
||||
fn peek(&mut self, k: u32) -> u32 {
|
||||
if self.n < k {
|
||||
self.fill();
|
||||
}
|
||||
(self.acc >> (64 - k)) as u32
|
||||
}
|
||||
|
||||
fn skip(&mut self, k: u32) {
|
||||
self.acc <<= k;
|
||||
self.n -= k;
|
||||
}
|
||||
|
||||
fn bits(&mut self, k: u32) -> u32 {
|
||||
if k == 0 {
|
||||
return 0;
|
||||
}
|
||||
let v = self.peek(k);
|
||||
self.skip(k);
|
||||
v
|
||||
}
|
||||
|
||||
/// Decode one Huffman symbol (F.2.2.3).
|
||||
fn decode(&mut self, h: &Huffman) -> Result<u8> {
|
||||
let p = self.peek(LOOKUP_BITS) as usize;
|
||||
if let Some(&(len, v)) = h.fast.get(p)
|
||||
&& len > 0
|
||||
{
|
||||
self.skip(u32::from(len));
|
||||
return Ok(v);
|
||||
}
|
||||
let mut code = self.bits(1) as i32;
|
||||
for l in 1..=16usize {
|
||||
if code <= h.maxcode[l] {
|
||||
let i = h.valptr[l] + code - h.mincode[l];
|
||||
return h.values.get(i as usize).copied().ok_or(Error::Malformed("lossless JPEG: bad Huffman code"));
|
||||
}
|
||||
code = (code << 1) | self.bits(1) as i32;
|
||||
}
|
||||
Err(Error::Malformed("lossless JPEG: bad Huffman code"))
|
||||
}
|
||||
|
||||
/// Skip to the byte after the next RSTn marker, starting a new bit stream there.
|
||||
fn restart(&mut self) -> Result<()> {
|
||||
// the decoder may have looked ahead into fill bits; find the marker in the bytes
|
||||
let mut p = self.pos;
|
||||
loop {
|
||||
match (self.data.get(p), self.data.get(p + 1)) {
|
||||
(Some(0xFF), Some(&m)) if (0xD0..=0xD7).contains(&m) => {
|
||||
*self = Bits::new(self.data, p + 2);
|
||||
return Ok(());
|
||||
}
|
||||
(Some(_), Some(_)) => p += 1,
|
||||
_ => return Err(Error::Malformed("lossless JPEG: missing restart marker")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Component {
|
||||
id: u8,
|
||||
table: usize,
|
||||
}
|
||||
|
||||
/// Decode a lossless JPEG stream. `max_samples` caps the picture (width × height × components).
|
||||
pub fn decode(data: &[u8], max_samples: usize) -> Result<Decoded> {
|
||||
if data.get(0..2) != Some(&[0xFF, 0xD8]) {
|
||||
return Err(Error::Malformed("lossless JPEG: no SOI marker"));
|
||||
}
|
||||
let mut pos = 2usize;
|
||||
let mut tables: [Option<Huffman>; 4] = [None, None, None, None];
|
||||
let mut frame: Option<(u8, usize, usize, Vec<Component>)> = None;
|
||||
let mut restart_interval = 0usize;
|
||||
loop {
|
||||
// markers may be preceded by fill bytes 0xFF
|
||||
while data.get(pos) == Some(&0xFF) && data.get(pos + 1) == Some(&0xFF) {
|
||||
pos += 1;
|
||||
}
|
||||
let (Some(&0xFF), Some(&m)) = (data.get(pos), data.get(pos + 1)) else {
|
||||
return Err(Error::Malformed("lossless JPEG: expected a marker"));
|
||||
};
|
||||
pos += 2;
|
||||
if m == 0xD9 {
|
||||
return Err(Error::Malformed("lossless JPEG: no scan"));
|
||||
}
|
||||
if (0xD0..=0xD8).contains(&m) || m == 0x01 {
|
||||
continue; // markers without a segment
|
||||
}
|
||||
let len_b = data.get(pos..pos + 2).ok_or(Error::Truncated)?;
|
||||
let len = usize::from(u16::from_be_bytes([len_b[0], len_b[1]]));
|
||||
if len < 2 {
|
||||
return Err(Error::Malformed("lossless JPEG: segment length"));
|
||||
}
|
||||
let seg = data.get(pos + 2..pos + len).ok_or(Error::Truncated)?;
|
||||
match m {
|
||||
0xC3 => {
|
||||
let [p, y0, y1, x0, x1, nf, rest @ ..] = seg else {
|
||||
return Err(Error::Malformed("lossless JPEG: short frame header"));
|
||||
};
|
||||
let (precision, nf) = (*p, usize::from(*nf));
|
||||
let height = usize::from(u16::from_be_bytes([*y0, *y1]));
|
||||
let width = usize::from(u16::from_be_bytes([*x0, *x1]));
|
||||
if !(2..=16).contains(&precision) {
|
||||
return Err(Error::Unsupported(format!("lossless JPEG precision {precision}")));
|
||||
}
|
||||
if height == 0 {
|
||||
return Err(Error::Unsupported("lossless JPEG with the height in a DNL marker".into()));
|
||||
}
|
||||
if width == 0 || !(1..=4).contains(&nf) || rest.len() < nf * 3 {
|
||||
return Err(Error::Malformed("lossless JPEG: frame header"));
|
||||
}
|
||||
let mut comps = Vec::with_capacity(nf);
|
||||
for c in rest.as_chunks::<3>().0.iter().take(nf) {
|
||||
if c[1] != 0x11 {
|
||||
return Err(Error::Unsupported("subsampled lossless JPEG components".into()));
|
||||
}
|
||||
comps.push(Component { id: c[0], table: 0 });
|
||||
}
|
||||
frame = Some((precision, width, height, comps));
|
||||
}
|
||||
0xC4 => {
|
||||
let mut s = seg;
|
||||
while !s.is_empty() {
|
||||
let [tcth, counts @ ..] = s else { break };
|
||||
let bits: [u8; 16] = counts.get(..16).and_then(|b| b.try_into().ok()).ok_or(Error::Truncated)?;
|
||||
let n: usize = bits.iter().map(|&b| usize::from(b)).sum();
|
||||
let values = counts.get(16..16 + n).ok_or(Error::Truncated)?.to_vec();
|
||||
let th = usize::from(tcth & 0x0F);
|
||||
if tcth >> 4 != 0 || th > 3 {
|
||||
return Err(Error::Malformed("lossless JPEG: Huffman table class or id"));
|
||||
}
|
||||
if values.iter().any(|&v| v > 16) {
|
||||
return Err(Error::Malformed("lossless JPEG: difference category above 16"));
|
||||
}
|
||||
tables[th] = Some(Huffman::new(&bits, values)?);
|
||||
s = counts.get(16 + n..).unwrap_or(&[]);
|
||||
}
|
||||
}
|
||||
0xDD => {
|
||||
let r = seg.get(0..2).ok_or(Error::Truncated)?;
|
||||
restart_interval = usize::from(u16::from_be_bytes([r[0], r[1]]));
|
||||
}
|
||||
0xDA => {
|
||||
let Some((precision, width, height, mut comps)) = frame.take() else {
|
||||
return Err(Error::Malformed("lossless JPEG: scan before the frame header"));
|
||||
};
|
||||
let ns = usize::from(*seg.first().ok_or(Error::Truncated)?);
|
||||
if ns != comps.len() {
|
||||
return Err(Error::Unsupported("lossless JPEG with more than one scan".into()));
|
||||
}
|
||||
for k in 0..ns {
|
||||
let c = seg.get(1 + 2 * k..3 + 2 * k).ok_or(Error::Truncated)?;
|
||||
let comp = comps.iter_mut().find(|x| x.id == c[0]).ok_or(Error::Malformed("lossless JPEG: unknown scan component"))?;
|
||||
comp.table = usize::from(c[1] >> 4);
|
||||
if comp.table > 3 {
|
||||
return Err(Error::Malformed("lossless JPEG: Huffman table id"));
|
||||
}
|
||||
}
|
||||
let tail = seg.get(1 + 2 * ns..4 + 2 * ns).ok_or(Error::Truncated)?;
|
||||
let (predictor, pt) = (tail[0], tail[2] & 0x0F);
|
||||
if !(1..=7).contains(&predictor) {
|
||||
return Err(Error::Unsupported(format!("lossless JPEG predictor {predictor}")));
|
||||
}
|
||||
if pt >= precision {
|
||||
return Err(Error::Malformed("lossless JPEG: point transform"));
|
||||
}
|
||||
let n = width.checked_mul(height).and_then(|v| v.checked_mul(comps.len())).ok_or(Error::TooLarge)?;
|
||||
if n > max_samples {
|
||||
return Err(Error::TooLarge);
|
||||
}
|
||||
let mut hufs = Vec::with_capacity(comps.len());
|
||||
for c in &comps {
|
||||
hufs.push(tables.get(c.table).and_then(|t| t.as_ref()).ok_or(Error::Malformed("lossless JPEG: missing Huffman table"))?);
|
||||
}
|
||||
let samples = scan(data, pos + len, &hufs, width, height, precision, predictor, pt, restart_interval)?;
|
||||
return Ok(Decoded { width, height, components: comps.len(), precision, predictor, samples });
|
||||
}
|
||||
0xC0..=0xC2 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => {
|
||||
return Err(Error::Unsupported("lossy (DCT) JPEG in a DNG".into()));
|
||||
}
|
||||
_ => {} // APPn, COM, DQT … are skipped
|
||||
}
|
||||
pos += len;
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the entropy-coded data of one scan (H.1.2).
|
||||
fn scan(data: &[u8], start: usize, hufs: &[&Huffman], width: usize, height: usize, precision: u8, predictor: u8, pt: u8, restart: usize) -> Result<Vec<u16>> {
|
||||
let nc = hufs.len();
|
||||
let row_len = width * nc;
|
||||
if restart != 0 && !restart.is_multiple_of(width) {
|
||||
return Err(Error::Unsupported("lossless JPEG restart interval that splits a line".into()));
|
||||
}
|
||||
let lines_per_interval = if restart == 0 { usize::MAX } else { restart / width };
|
||||
let mut out = vec![0u16; row_len * height];
|
||||
let mut bits = Bits::new(data, start);
|
||||
let initial = 1i32 << (precision - pt - 1);
|
||||
let mut line_in_interval = 0usize;
|
||||
for y in 0..height {
|
||||
if line_in_interval == lines_per_interval {
|
||||
bits.restart()?;
|
||||
line_in_interval = 0;
|
||||
}
|
||||
let first_line = line_in_interval == 0;
|
||||
let (above, cur) = if y == 0 {
|
||||
(None, &mut out[..row_len])
|
||||
} else {
|
||||
let (a, c) = out.split_at_mut(y * row_len);
|
||||
(Some(&a[(y - 1) * row_len..]), &mut c[..row_len])
|
||||
};
|
||||
for x in 0..width {
|
||||
for (c, h) in hufs.iter().enumerate() {
|
||||
let i = x * nc + c;
|
||||
let ssss = u32::from(bits.decode(h)?);
|
||||
let diff = match ssss {
|
||||
0 => 0,
|
||||
16 => 32768,
|
||||
s => {
|
||||
let v = bits.bits(s) as i32;
|
||||
if v < (1 << (s - 1)) { v - (1 << s) + 1 } else { v }
|
||||
}
|
||||
};
|
||||
let px = match above {
|
||||
_ if first_line && x == 0 => initial,
|
||||
_ if first_line => i32::from(cur[i - nc]),
|
||||
Some(a) if x == 0 => i32::from(a[i]),
|
||||
Some(a) => predict(predictor, i32::from(cur[i - nc]), i32::from(a[i]), i32::from(a[i - nc])),
|
||||
None => initial,
|
||||
};
|
||||
cur[i] = ((px + diff) & 0xFFFF) as u16;
|
||||
}
|
||||
}
|
||||
line_in_interval += 1;
|
||||
}
|
||||
if pt > 0 {
|
||||
for s in &mut out {
|
||||
*s <<= pt;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Table H.1 predictors (`ra` left, `rb` above, `rc` above left).
|
||||
pub fn predict(p: u8, ra: i32, rb: i32, rc: i32) -> i32 {
|
||||
match p {
|
||||
1 => ra,
|
||||
2 => rb,
|
||||
3 => rc,
|
||||
4 => ra + rb - rc,
|
||||
5 => ra + ((rb - rc) >> 1),
|
||||
6 => rb + ((ra - rc) >> 1),
|
||||
_ => (ra + rb) >> 1,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
//! Raw samples to a linear Rec. 709 picture, in the order of the DNG specification's chapter 5
|
||||
//! ("Mapping Raw Values to Linear Reference Values") and chapter 6 (colour):
|
||||
//!
|
||||
//! 1. ActiveArea: only the active (unmasked) sensor area is used; black levels, the CFA pattern
|
||||
//! and the default crop are relative to its top left corner.
|
||||
//! 2. LinearizationTable maps each sample.
|
||||
//! 3. The black level (BlackLevel over its BlackLevelRepeatDim pattern, plus BlackLevelDeltaH for
|
||||
//! the column and BlackLevelDeltaV for the row) is subtracted and the range up to WhiteLevel
|
||||
//! scaled to 0–1 (clipped there).
|
||||
//! 4. White balance: each sample is divided by the camera neutral of its colour and clipped at
|
||||
//! 1, so highlights the sensor clipped stay neutral.
|
||||
//! 5. A CFA is demosaiced ([`crate::demosaic`]).
|
||||
//! 6. DefaultCropOrigin / DefaultCropSize cut the picture.
|
||||
//! 7. The colour matrix of [`crate::color`] takes the white-balanced camera values to linear
|
||||
//! Rec. 709 (including BaselineExposure); negative results are clipped to 0.
|
||||
|
||||
#[cfg(feature = "threads")]
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::color::{M3, V3};
|
||||
use crate::demosaic::{Pattern, demosaic};
|
||||
use crate::raw::RawImage;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// The processing tags of a raw image.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Params {
|
||||
/// top, left, bottom, right
|
||||
pub active_area: Option<[usize; 4]>,
|
||||
pub linearization: Option<Vec<u16>>,
|
||||
/// rows, columns of the BlackLevel pattern
|
||||
pub black_repeat: (usize, usize),
|
||||
/// rows × columns × samples per pixel values
|
||||
pub black: Vec<f64>,
|
||||
pub black_delta_h: Vec<f64>,
|
||||
pub black_delta_v: Vec<f64>,
|
||||
/// per sample
|
||||
pub white: Vec<f64>,
|
||||
pub crop_origin: Option<(f64, f64)>,
|
||||
pub crop_size: Option<(f64, f64)>,
|
||||
/// CFA pattern in camera planes (`None`: LinearRaw).
|
||||
pub cfa: Option<Pattern>,
|
||||
}
|
||||
|
||||
/// A rectangle: x, y, width, height.
|
||||
type Rect = (usize, usize, usize, usize);
|
||||
|
||||
/// The active area of a `w` × `h` raw image.
|
||||
pub fn active_rect(p: &Params, w: usize, h: usize) -> Result<Rect> {
|
||||
match p.active_area {
|
||||
None => Ok((0, 0, w, h)),
|
||||
Some([top, left, bottom, right]) => {
|
||||
if top >= bottom || left >= right || bottom > h || right > w {
|
||||
return Err(Error::Malformed("ActiveArea outside the image"));
|
||||
}
|
||||
Ok((left, top, right - left, bottom - top))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The default crop inside an active area of `aw` × `ah`.
|
||||
pub fn crop_rect(p: &Params, aw: usize, ah: usize) -> Rect {
|
||||
let (ox, oy) = p.crop_origin.unwrap_or((0.0, 0.0));
|
||||
let (sw, sh) = p.crop_size.unwrap_or((aw as f64, ah as f64));
|
||||
let to = |v: f64, max: usize| if v.is_finite() && v > 0.0 { (v.min(max as f64)) as usize } else { 0 };
|
||||
let x = to(ox.floor(), aw.saturating_sub(1));
|
||||
let y = to(oy.floor(), ah.saturating_sub(1));
|
||||
let w = to(sw.round(), aw - x).max(1);
|
||||
let h = to(sh.round(), ah - y).max(1);
|
||||
(x, y, w, h)
|
||||
}
|
||||
|
||||
/// Normalised, white-balanced samples of the active area (steps 1–4), one plane per sample.
|
||||
fn normalise(raw: &RawImage, p: &Params, (ax, ay, aw, ah): Rect, neutral: &V3, planes: usize) -> Result<Vec<f32>> {
|
||||
let spp = raw.samples_per_pixel;
|
||||
let (br, bc) = (p.black_repeat.0.max(1), p.black_repeat.1.max(1));
|
||||
if br > 64 || bc > 64 {
|
||||
return Err(Error::Unsupported("a BlackLevelRepeatDim larger than 64".into()));
|
||||
}
|
||||
let need = br * bc * spp;
|
||||
let black: Vec<f64> = match p.black.len() {
|
||||
0 => vec![0.0; need],
|
||||
1 => vec![p.black[0]; need],
|
||||
n if n >= need => p.black[..need].to_vec(),
|
||||
_ => return Err(Error::Malformed("BlackLevel has too few values")),
|
||||
};
|
||||
let max_code = ((1u32 << raw.bits) - 1) as f64;
|
||||
let white: Vec<f64> = (0..spp).map(|s| p.white.get(s).or(p.white.first()).copied().unwrap_or(max_code)).collect();
|
||||
let lin = p.linearization.as_deref().filter(|t| !t.is_empty());
|
||||
let inv_neutral: Vec<f32> = (0..planes).map(|c| (1.0 / neutral.get(c).copied().unwrap_or(1.0)) as f32).collect();
|
||||
let cfa = p.cfa.as_ref();
|
||||
let mut out = vec![0f32; aw * ah * spp];
|
||||
let row = |y: usize, o: &mut [f32]| {
|
||||
let dv = p.black_delta_v.get(y).copied().unwrap_or(0.0);
|
||||
let src_row = (ay + y) * raw.width * spp;
|
||||
for x in 0..aw {
|
||||
let dh = p.black_delta_h.get(x).copied().unwrap_or(0.0);
|
||||
for s in 0..spp {
|
||||
let code = raw.data.get(src_row + (ax + x) * spp + s).copied().unwrap_or(0);
|
||||
let v = match lin {
|
||||
Some(t) => f64::from(t.get(usize::from(code)).or(t.last()).copied().unwrap_or(0)),
|
||||
None => f64::from(code),
|
||||
};
|
||||
let b = black[((y % br) * bc + x % bc) * spp + s] + dh + dv;
|
||||
let range = white[s] - b;
|
||||
let n = if range > 0.0 { ((v - b) / range).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let plane = match cfa {
|
||||
Some(c) => usize::from(c.color(x, y)),
|
||||
None => s,
|
||||
};
|
||||
let wb = inv_neutral.get(plane).copied().unwrap_or(1.0);
|
||||
if let Some(o) = o.get_mut(x * spp + s) {
|
||||
*o = (n as f32 * wb).min(1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "threads")]
|
||||
out.par_chunks_mut((aw * spp).max(1)).enumerate().for_each(|(y, o)| row(y, o));
|
||||
#[cfg(not(feature = "threads"))]
|
||||
out.chunks_mut((aw * spp).max(1)).enumerate().for_each(|(y, o)| row(y, o));
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A developed picture: premultiplied linear Rec. 709 RGBA floats (alpha 1).
|
||||
pub struct Developed {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub rgba: Vec<f32>,
|
||||
}
|
||||
|
||||
/// Develop `raw` (steps 1–7). `neutral` and `matrix` come from [`crate::color::Calibration::solve`].
|
||||
pub fn develop(raw: &RawImage, p: &Params, neutral: &V3, matrix: &M3) -> Result<Developed> {
|
||||
let spp = raw.samples_per_pixel;
|
||||
if p.cfa.is_some() && spp != 1 {
|
||||
return Err(Error::Malformed("a CFA image with more than one sample per pixel"));
|
||||
}
|
||||
if p.cfa.is_none() && !matches!(spp, 1 | 3) {
|
||||
return Err(Error::Unsupported(format!("LinearRaw with {spp} samples per pixel")));
|
||||
}
|
||||
let active = active_rect(p, raw.width, raw.height)?;
|
||||
let (_, _, aw, ah) = active;
|
||||
let planes = if p.cfa.is_some() { 3 } else { spp };
|
||||
let norm = normalise(raw, p, active, neutral, planes)?;
|
||||
// three samples per pixel
|
||||
let rgb = match (&p.cfa, spp) {
|
||||
(Some(pat), _) => demosaic(&norm, aw, ah, pat),
|
||||
(None, 3) => norm,
|
||||
_ => norm.iter().flat_map(|&v| [v, v, v]).collect(),
|
||||
};
|
||||
let (cx, cy, cw, ch) = crop_rect(p, aw, ah);
|
||||
let m: [[f32; 3]; 3] = matrix.map(|r| r.map(|v| v as f32));
|
||||
let mono = p.cfa.is_none() && spp == 1;
|
||||
let mut rgba = vec![0f32; cw * ch * 4];
|
||||
let row = |y: usize, o: &mut [f32]| {
|
||||
let src = (cy + y) * aw + cx;
|
||||
for x in 0..cw {
|
||||
let Some(c) = rgb.get((src + x) * 3..(src + x) * 3 + 3) else { continue };
|
||||
let px = if mono {
|
||||
// grey: the matrix's luminance gain only
|
||||
let g = c[0] * (m[1][0] + m[1][1] + m[1][2]);
|
||||
[g, g, g]
|
||||
} else {
|
||||
[0, 1, 2].map(|i| m[i][0] * c[0] + m[i][1] * c[1] + m[i][2] * c[2])
|
||||
};
|
||||
if let Some(d) = o.get_mut(x * 4..x * 4 + 4) {
|
||||
d.copy_from_slice(&[px[0].max(0.0), px[1].max(0.0), px[2].max(0.0), 1.0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "threads")]
|
||||
rgba.par_chunks_mut(cw * 4).enumerate().for_each(|(y, o)| row(y, o));
|
||||
#[cfg(not(feature = "threads"))]
|
||||
rgba.chunks_mut(cw * 4).enumerate().for_each(|(y, o)| row(y, o));
|
||||
Ok(Developed { width: cw, height: ch, rgba })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::color::IDENTITY;
|
||||
|
||||
fn raw(w: usize, h: usize, spp: usize, data: Vec<u16>) -> RawImage {
|
||||
RawImage { width: w, height: h, samples_per_pixel: spp, bits: 12, data }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_white_linearisation_and_white_balance() {
|
||||
// LinearRaw 2 × 1: black 100, white 1100; neutral (0.5, 1, 0.25)
|
||||
let r = raw(2, 1, 3, vec![600, 600, 600, 100, 1100, 2000]);
|
||||
let p = Params { black: vec![100.0], white: vec![1100.0], ..Default::default() };
|
||||
let d = develop(&r, &p, &[0.5, 1.0, 0.25], &IDENTITY).unwrap();
|
||||
assert_eq!((d.width, d.height), (2, 1));
|
||||
// 0.5 / 0.5 = 1, 0.5 / 1, 0.5 / 0.25 = 2 → clipped to 1
|
||||
assert_eq!(&d.rgba[..4], &[1.0, 0.5, 1.0, 1.0]);
|
||||
// below black → 0, at white → 1, above white → 1
|
||||
assert_eq!(&d.rgba[4..], &[0.0, 1.0, 1.0, 1.0]);
|
||||
// a linearisation table doubling the codes
|
||||
let t: Vec<u16> = (0..4096u32).map(|v| (v * 2).min(65_535) as u16).collect();
|
||||
let p = Params { linearization: Some(t), black: vec![200.0], white: vec![2200.0], ..p };
|
||||
let d = develop(&r, &p, &[1.0; 3], &IDENTITY).unwrap();
|
||||
assert_eq!(&d.rgba[..3], &[0.5, 0.5, 0.5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_cfa_black_levels_and_deltas() {
|
||||
// 2 × 2 CFA, black repeat 2 × 2 with a different black per site, plus row/column deltas
|
||||
let pat = Pattern { rows: 2, cols: 2, colors: vec![0, 1, 1, 2] };
|
||||
let p = Params {
|
||||
black_repeat: (2, 2),
|
||||
black: vec![10.0, 20.0, 30.0, 40.0],
|
||||
black_delta_h: vec![0.0, 5.0],
|
||||
black_delta_v: vec![0.0, 1.0],
|
||||
white: vec![1010.0],
|
||||
cfa: Some(pat),
|
||||
..Default::default()
|
||||
};
|
||||
let r = raw(2, 2, 1, vec![510, 525, 531, 546]);
|
||||
let n = normalise(&r, &p, (0, 0, 2, 2), &[1.0; 3], 3).unwrap();
|
||||
// (v - black) / (white - black) for black 10, 25, 31, 46
|
||||
let want = [500.0 / 1000.0, 500.0 / 985.0, 500.0 / 979.0, 500.0 / 964.0];
|
||||
for (a, b) in n.iter().zip(want) {
|
||||
assert!((a - b).abs() < 1e-6, "{a} vs {b}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_area_and_default_crop() {
|
||||
// 6 × 4 LinearRaw grey ramp; active area cuts a 1-pixel border, crop takes 2 × 1 at (1, 1)
|
||||
let data: Vec<u16> = (0..24u16).flat_map(|i| [i * 10; 3]).collect();
|
||||
let r = raw(6, 4, 3, data);
|
||||
let p =
|
||||
Params { active_area: Some([1, 1, 3, 5]), white: vec![4095.0], crop_origin: Some((1.0, 1.0)), crop_size: Some((2.0, 1.0)), ..Default::default() };
|
||||
let d = develop(&r, &p, &[1.0; 3], &IDENTITY).unwrap();
|
||||
assert_eq!((d.width, d.height), (2, 1));
|
||||
// active (1,1)–(5,3); crop origin (1,1) inside it → raw (2, 2) and (3, 2): samples 14, 15
|
||||
assert!((d.rgba[0] - 140.0 / 4095.0).abs() < 1e-6);
|
||||
assert!((d.rgba[4] - 150.0 / 4095.0).abs() < 1e-6);
|
||||
// hostile active areas and crops
|
||||
let bad = Params { active_area: Some([3, 0, 1, 6]), ..Default::default() };
|
||||
assert!(develop(&r, &bad, &[1.0; 3], &IDENTITY).is_err());
|
||||
let bad = Params { active_area: Some([0, 0, 4, 7]), ..Default::default() };
|
||||
assert!(develop(&r, &bad, &[1.0; 3], &IDENTITY).is_err());
|
||||
let huge = Params { crop_origin: Some((1e12, -5.0)), crop_size: Some((f64::NAN, 1e9)), white: vec![4095.0], ..Default::default() };
|
||||
let d = develop(&r, &huge, &[1.0; 3], &IDENTITY).unwrap();
|
||||
assert!(d.width >= 1 && d.width <= 6 && d.height >= 1 && d.height <= 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matrix_and_clipping_of_negatives() {
|
||||
let r = raw(1, 1, 3, vec![4095, 0, 0]);
|
||||
let p = Params { white: vec![4095.0], ..Default::default() };
|
||||
let m = [[1.0, 0.0, 0.0], [-0.5, 1.0, 0.0], [0.0, 0.0, 2.0]];
|
||||
let d = develop(&r, &p, &[1.0; 3], &m).unwrap();
|
||||
assert_eq!(&d.rgba, &[1.0, 0.0, 0.0, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_black_level_is_an_error() {
|
||||
let r = raw(2, 2, 1, vec![0; 4]);
|
||||
let p = Params { black_repeat: (2, 2), black: vec![1.0, 2.0], cfa: Some(Pattern { rows: 2, cols: 2, colors: vec![0, 1, 1, 2] }), ..Default::default() };
|
||||
assert!(develop(&r, &p, &[1.0; 3], &IDENTITY).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
//! The raw image data of a DNG: finding the main raw IFD, and reading its strips or tiles
|
||||
//! (uncompressed or lossless JPEG) into one array of 16-bit samples.
|
||||
|
||||
#[cfg(feature = "threads")]
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::tiff::{Ifd, Tiff, tags};
|
||||
use crate::{Error, Result, ljpeg};
|
||||
|
||||
/// Largest raw image decoded, in samples (width × height × samples per pixel).
|
||||
pub const MAX_SAMPLES: usize = 1 << 27;
|
||||
|
||||
/// Photometric interpretations of DNG raw data.
|
||||
pub const PHOTOMETRIC_CFA: u64 = 32803;
|
||||
pub const PHOTOMETRIC_LINEAR_RAW: u64 = 34892;
|
||||
|
||||
/// The layout of a raw IFD's image data.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RawLayout {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub samples_per_pixel: usize,
|
||||
pub bits: u32,
|
||||
/// 1 uncompressed, 7 lossless JPEG.
|
||||
pub compression: u64,
|
||||
pub photometric: u64,
|
||||
}
|
||||
|
||||
/// Raw samples: `width` × `height` pixels of `samples_per_pixel` interleaved 16-bit samples.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RawImage {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub samples_per_pixel: usize,
|
||||
pub bits: u32,
|
||||
pub data: Vec<u16>,
|
||||
}
|
||||
|
||||
fn uint_or(t: &Tiff, ifd: &Ifd, tag: u16, default: u64) -> Result<u64> {
|
||||
match ifd.get(tag) {
|
||||
Some(e) => t.uint(e),
|
||||
None => Ok(default),
|
||||
}
|
||||
}
|
||||
|
||||
/// The main raw image IFD: the largest full-resolution (NewSubFileType 0) IFD whose photometric
|
||||
/// interpretation is CFA or LinearRaw.
|
||||
pub fn find_raw_ifd<'i>(t: &Tiff, ifds: &'i [Ifd]) -> Result<&'i Ifd> {
|
||||
let mut best: Option<(&Ifd, u64)> = None;
|
||||
for ifd in ifds {
|
||||
let Ok(kind) = uint_or(t, ifd, tags::NEW_SUBFILE_TYPE, 0) else { continue };
|
||||
let Ok(photometric) = uint_or(t, ifd, tags::PHOTOMETRIC, 0) else { continue };
|
||||
if kind != 0 || !matches!(photometric, PHOTOMETRIC_CFA | PHOTOMETRIC_LINEAR_RAW) {
|
||||
continue;
|
||||
}
|
||||
let w = uint_or(t, ifd, tags::IMAGE_WIDTH, 0).unwrap_or(0);
|
||||
let h = uint_or(t, ifd, tags::IMAGE_LENGTH, 0).unwrap_or(0);
|
||||
let area = w.saturating_mul(h);
|
||||
if best.is_none_or(|(_, a)| area > a) {
|
||||
best = Some((ifd, area));
|
||||
}
|
||||
}
|
||||
best.map(|(i, _)| i).ok_or(Error::Unsupported("no raw image in this DNG".into()))
|
||||
}
|
||||
|
||||
/// Read and check the layout tags of a raw IFD.
|
||||
pub fn layout(t: &Tiff, ifd: &Ifd) -> Result<RawLayout> {
|
||||
let width = uint_or(t, ifd, tags::IMAGE_WIDTH, 0)?;
|
||||
let height = uint_or(t, ifd, tags::IMAGE_LENGTH, 0)?;
|
||||
let spp = uint_or(t, ifd, tags::SAMPLES_PER_PIXEL, 1)?;
|
||||
let photometric = uint_or(t, ifd, tags::PHOTOMETRIC, 0)?;
|
||||
let compression = uint_or(t, ifd, tags::COMPRESSION, 1)?;
|
||||
let bits = match ifd.get(tags::BITS_PER_SAMPLE) {
|
||||
Some(e) => {
|
||||
let b = t.uints(e)?;
|
||||
let first = *b.first().ok_or(Error::Malformed("empty BitsPerSample"))?;
|
||||
if b.iter().any(|&x| x != first) {
|
||||
return Err(Error::Unsupported("different bit depths per sample".into()));
|
||||
}
|
||||
first
|
||||
}
|
||||
None => 1,
|
||||
};
|
||||
if width == 0 || height == 0 || width > 65_535 || height > 65_535 {
|
||||
return Err(Error::Malformed("raw image size"));
|
||||
}
|
||||
if !(1..=4).contains(&spp) {
|
||||
return Err(Error::Unsupported(format!("{spp} samples per pixel")));
|
||||
}
|
||||
if photometric == PHOTOMETRIC_CFA && spp != 1 {
|
||||
return Err(Error::Malformed("a CFA image with more than one sample per pixel"));
|
||||
}
|
||||
if !(1..=16).contains(&bits) {
|
||||
return Err(Error::Unsupported(format!("{bits}-bit raw samples")));
|
||||
}
|
||||
if uint_or(t, ifd, tags::SAMPLE_FORMAT, 1)? != 1 {
|
||||
return Err(Error::Unsupported("floating-point raw data".into()));
|
||||
}
|
||||
if spp > 1 && uint_or(t, ifd, tags::PLANAR_CONFIGURATION, 1)? != 1 {
|
||||
return Err(Error::Unsupported("planar raw data".into()));
|
||||
}
|
||||
match compression {
|
||||
1 => {
|
||||
if uint_or(t, ifd, tags::PREDICTOR, 1)? != 1 {
|
||||
return Err(Error::Unsupported("a TIFF predictor on uncompressed raw data".into()));
|
||||
}
|
||||
}
|
||||
7 => {}
|
||||
8 => return Err(Error::Unsupported("deflate-compressed DNG".into())),
|
||||
34892 => return Err(Error::Unsupported("lossy JPEG DNG".into())),
|
||||
52546 => return Err(Error::Unsupported("JPEG XL DNG".into())),
|
||||
c => return Err(Error::Unsupported(format!("DNG compression {c}"))),
|
||||
}
|
||||
Ok(RawLayout { width: width as usize, height: height as usize, samples_per_pixel: spp as usize, bits: bits as u32, compression, photometric })
|
||||
}
|
||||
|
||||
/// One strip or tile: where it sits in the image and its bytes.
|
||||
struct Chunk<'a> {
|
||||
x: usize,
|
||||
y: usize,
|
||||
/// Pixel size of the chunk as coded (tiles are whole even at the image edge).
|
||||
w: usize,
|
||||
h: usize,
|
||||
bytes: &'a [u8],
|
||||
}
|
||||
|
||||
fn chunks<'a>(t: &Tiff<'a>, ifd: &Ifd, l: &RawLayout) -> Result<Vec<Chunk<'a>>> {
|
||||
let (offs, counts, tw, th) = if let (Some(o), Some(c)) = (ifd.get(tags::TILE_OFFSETS), ifd.get(tags::TILE_BYTE_COUNTS)) {
|
||||
let tw = uint_or(t, ifd, tags::TILE_WIDTH, 0)? as usize;
|
||||
let th = uint_or(t, ifd, tags::TILE_LENGTH, 0)? as usize;
|
||||
if tw == 0 || th == 0 || tw > 65_535 || th > 65_535 {
|
||||
return Err(Error::Malformed("tile size"));
|
||||
}
|
||||
(t.uints(o)?, t.uints(c)?, tw, th)
|
||||
} else if let (Some(o), Some(c)) = (ifd.get(tags::STRIP_OFFSETS), ifd.get(tags::STRIP_BYTE_COUNTS)) {
|
||||
let rows = (uint_or(t, ifd, tags::ROWS_PER_STRIP, l.height as u64)? as usize).clamp(1, l.height);
|
||||
(t.uints(o)?, t.uints(c)?, l.width, rows)
|
||||
} else {
|
||||
return Err(Error::Malformed("no strips or tiles"));
|
||||
};
|
||||
let across = l.width.div_ceil(tw);
|
||||
let down = l.height.div_ceil(th);
|
||||
let n = across * down;
|
||||
if offs.len() < n || counts.len() < n {
|
||||
return Err(Error::Malformed("fewer strips or tiles than the image needs"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let (x, y) = ((i % across) * tw, (i / across) * th);
|
||||
let start = usize::try_from(offs[i]).map_err(|_| Error::Truncated)?;
|
||||
let len = usize::try_from(counts[i]).map_err(|_| Error::Truncated)?;
|
||||
let end = start.checked_add(len).ok_or(Error::Truncated)?;
|
||||
let bytes = t.data.get(start..end).ok_or(Error::Truncated)?;
|
||||
// strips end at the image; tiles are coded whole
|
||||
let h = if ifd.get(tags::TILE_OFFSETS).is_some() { th } else { th.min(l.height - y) };
|
||||
out.push(Chunk { x, y, w: tw, h, bytes });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Unpack `w` × `h` × `spp` uncompressed samples of `bits` bits: 8 and 16-bit samples as bytes /
|
||||
/// words in the file's byte order, other depths packed most significant bit first with each row
|
||||
/// starting on a byte boundary (TIFF 6.0 §7, DNG BitsPerSample).
|
||||
pub fn unpack(bytes: &[u8], w: usize, h: usize, spp: usize, bits: u32, big_endian: bool) -> Result<Vec<u16>> {
|
||||
let per_row = w * spp;
|
||||
let row_bytes = (per_row * bits as usize).div_ceil(8);
|
||||
let need = row_bytes.checked_mul(h).ok_or(Error::TooLarge)?;
|
||||
if bytes.len() < need {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
let mut out = Vec::with_capacity(per_row * h);
|
||||
for row in bytes.chunks_exact(row_bytes.max(1)).take(h) {
|
||||
match bits {
|
||||
8 => out.extend(row.iter().map(|&b| u16::from(b))),
|
||||
16 => out.extend(row.as_chunks::<2>().0.iter().map(|&b| if big_endian { u16::from_be_bytes(b) } else { u16::from_le_bytes(b) })),
|
||||
_ => {
|
||||
let mut acc: u32 = 0;
|
||||
let mut n: u32 = 0;
|
||||
let mut it = row.iter();
|
||||
for _ in 0..per_row {
|
||||
while n < bits {
|
||||
acc = (acc << 8) | u32::from(*it.next().ok_or(Error::Truncated)?);
|
||||
n += 8;
|
||||
}
|
||||
n -= bits;
|
||||
out.push(((acc >> n) & ((1 << bits) - 1)) as u16);
|
||||
acc &= (1 << n) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Decode one chunk into `w` × `h` × `spp` samples.
|
||||
fn decode_chunk(c: &Chunk, l: &RawLayout, big_endian: bool) -> Result<Vec<u16>> {
|
||||
let need = c.w * c.h * l.samples_per_pixel;
|
||||
match l.compression {
|
||||
1 => unpack(c.bytes, c.w, c.h, l.samples_per_pixel, l.bits, big_endian),
|
||||
_ => {
|
||||
// the JPEG's samples fill the chunk in raster order (its own width × components may
|
||||
// differ from the chunk's, e.g. two components of half the width)
|
||||
let d = ljpeg::decode(c.bytes, need.saturating_add(65_536 * 4))?;
|
||||
if d.samples.len() < need {
|
||||
return Err(Error::Malformed("lossless JPEG smaller than its tile or strip"));
|
||||
}
|
||||
let mut s = d.samples;
|
||||
s.truncate(need);
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the raw image of `ifd`.
|
||||
pub fn read(t: &Tiff, ifd: &Ifd) -> Result<RawImage> {
|
||||
let l = layout(t, ifd)?;
|
||||
let spp = l.samples_per_pixel;
|
||||
let total = l.width.checked_mul(l.height).and_then(|v| v.checked_mul(spp)).ok_or(Error::TooLarge)?;
|
||||
if total > MAX_SAMPLES {
|
||||
return Err(Error::TooLarge);
|
||||
}
|
||||
let chunks = chunks(t, ifd, &l)?;
|
||||
// every sample takes at least one bit of file (a Huffman code is at least one bit long)
|
||||
let coded: usize = chunks.iter().map(|c| c.bytes.len()).fold(0usize, usize::saturating_add);
|
||||
if total / 8 > coded {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
for c in &chunks {
|
||||
let n = c.w.checked_mul(c.h).and_then(|v| v.checked_mul(spp)).ok_or(Error::TooLarge)?;
|
||||
if n > MAX_SAMPLES {
|
||||
return Err(Error::TooLarge);
|
||||
}
|
||||
if n / 8 > c.bytes.len() {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "threads")]
|
||||
let decoded: Vec<Result<Vec<u16>>> = chunks.par_iter().map(|c| decode_chunk(c, &l, t.big_endian)).collect();
|
||||
#[cfg(not(feature = "threads"))]
|
||||
let decoded: Vec<Result<Vec<u16>>> = chunks.iter().map(|c| decode_chunk(c, &l, t.big_endian)).collect();
|
||||
let mut data = vec![0u16; total];
|
||||
let row_len = l.width * spp;
|
||||
for (c, d) in chunks.iter().zip(decoded) {
|
||||
let d = d?;
|
||||
let cw = c.w.min(l.width - c.x);
|
||||
let ch = c.h.min(l.height - c.y);
|
||||
for r in 0..ch {
|
||||
let src = d.get(r * c.w * spp..(r * c.w + cw) * spp).ok_or(Error::Truncated)?;
|
||||
let at = (c.y + r) * row_len + c.x * spp;
|
||||
data.get_mut(at..at + cw * spp).ok_or(Error::Truncated)?.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
Ok(RawImage { width: l.width, height: l.height, samples_per_pixel: spp, bits: l.bits, data })
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
//! The TIFF container (TIFF 6.0 §2, as DNG uses it): byte order, image file directories (IFDs),
|
||||
//! their entries and values, and the SubIFD tree.
|
||||
//!
|
||||
//! Every offset and count comes from the file and is checked before use: an entry whose value lies
|
||||
//! outside the file is an error when it is read, the walk visits at most [`MAX_IFDS`] directories,
|
||||
//! never the same one twice (cyclic chains end), and goes at most [`MAX_DEPTH`] SubIFD levels deep.
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Most IFDs one file may have (main chain and SubIFDs together).
|
||||
pub const MAX_IFDS: usize = 64;
|
||||
/// Deepest SubIFD nesting followed.
|
||||
pub const MAX_DEPTH: usize = 4;
|
||||
|
||||
/// TIFF field types (TIFF 6.0 §2, plus type 13 IFD from TIFF Technical Note 1).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Byte,
|
||||
Ascii,
|
||||
Short,
|
||||
Long,
|
||||
Rational,
|
||||
SByte,
|
||||
Undefined,
|
||||
SShort,
|
||||
SLong,
|
||||
SRational,
|
||||
Float,
|
||||
Double,
|
||||
Ifd,
|
||||
}
|
||||
|
||||
impl Type {
|
||||
fn from_code(c: u16) -> Option<Type> {
|
||||
Some(match c {
|
||||
1 => Type::Byte,
|
||||
2 => Type::Ascii,
|
||||
3 => Type::Short,
|
||||
4 => Type::Long,
|
||||
5 => Type::Rational,
|
||||
6 => Type::SByte,
|
||||
7 => Type::Undefined,
|
||||
8 => Type::SShort,
|
||||
9 => Type::SLong,
|
||||
10 => Type::SRational,
|
||||
11 => Type::Float,
|
||||
12 => Type::Double,
|
||||
13 => Type::Ifd,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Bytes per value.
|
||||
pub fn size(self) -> usize {
|
||||
match self {
|
||||
Type::Byte | Type::Ascii | Type::SByte | Type::Undefined => 1,
|
||||
Type::Short | Type::SShort => 2,
|
||||
Type::Long | Type::SLong | Type::Float | Type::Ifd => 4,
|
||||
Type::Rational | Type::SRational | Type::Double => 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One directory entry.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Entry {
|
||||
pub tag: u16,
|
||||
pub ty: Type,
|
||||
pub count: u32,
|
||||
/// Where the value bytes start: inside the entry when they fit in four bytes, else the offset
|
||||
/// the entry holds (not yet checked against the file length).
|
||||
value_at: u64,
|
||||
}
|
||||
|
||||
/// One image file directory.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Ifd {
|
||||
/// File offset of the directory.
|
||||
pub offset: u64,
|
||||
pub entries: Vec<Entry>,
|
||||
/// Nesting: 0 for the main chain, 1 for its SubIFDs, …
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
impl Ifd {
|
||||
pub fn get(&self, tag: u16) -> Option<&Entry> {
|
||||
self.entries.iter().find(|e| e.tag == tag)
|
||||
}
|
||||
}
|
||||
|
||||
/// A TIFF file in memory.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Tiff<'a> {
|
||||
pub data: &'a [u8],
|
||||
pub big_endian: bool,
|
||||
}
|
||||
|
||||
impl<'a> Tiff<'a> {
|
||||
/// Read the header. Classic TIFF only (BigTIFF, version 43, is not used by DNG).
|
||||
pub fn new(data: &'a [u8]) -> Result<(Tiff<'a>, u64)> {
|
||||
let big_endian = match data.get(0..4) {
|
||||
Some(b"II*\0") => false,
|
||||
Some(b"MM\0*") => true,
|
||||
_ => return Err(Error::NotDng),
|
||||
};
|
||||
let t = Tiff { data, big_endian };
|
||||
let first = u64::from(t.u32_at(4)?);
|
||||
Ok((t, first))
|
||||
}
|
||||
|
||||
fn bytes_at(&self, off: u64, len: usize) -> Result<&'a [u8]> {
|
||||
let start = usize::try_from(off).map_err(|_| Error::Malformed("offset outside the file"))?;
|
||||
let end = start.checked_add(len).ok_or(Error::Malformed("offset outside the file"))?;
|
||||
self.data.get(start..end).ok_or(Error::Malformed("offset outside the file"))
|
||||
}
|
||||
|
||||
pub fn u16_of(&self, b: [u8; 2]) -> u16 {
|
||||
if self.big_endian { u16::from_be_bytes(b) } else { u16::from_le_bytes(b) }
|
||||
}
|
||||
|
||||
pub fn u32_of(&self, b: [u8; 4]) -> u32 {
|
||||
if self.big_endian { u32::from_be_bytes(b) } else { u32::from_le_bytes(b) }
|
||||
}
|
||||
|
||||
fn u16_at(&self, off: u64) -> Result<u16> {
|
||||
let b = self.bytes_at(off, 2)?;
|
||||
Ok(self.u16_of([b[0], b[1]]))
|
||||
}
|
||||
|
||||
fn u32_at(&self, off: u64) -> Result<u32> {
|
||||
let b = self.bytes_at(off, 4)?;
|
||||
Ok(self.u32_of([b[0], b[1], b[2], b[3]]))
|
||||
}
|
||||
|
||||
/// Read the IFD at `off`: its entries and the offset of the next IFD (0 at the end of a chain).
|
||||
pub fn read_ifd(&self, off: u64, depth: usize) -> Result<(Ifd, u64)> {
|
||||
let n = self.u16_at(off)?;
|
||||
let mut entries = Vec::with_capacity(usize::from(n));
|
||||
for i in 0..u64::from(n) {
|
||||
let at = off + 2 + i * 12;
|
||||
let e = self.bytes_at(at, 12)?;
|
||||
let tag = self.u16_of([e[0], e[1]]);
|
||||
let code = self.u16_of([e[2], e[3]]);
|
||||
let count = self.u32_of([e[4], e[5], e[6], e[7]]);
|
||||
// unknown field types are skipped (TIFF 6.0: readers ignore them)
|
||||
let Some(ty) = Type::from_code(code) else { continue };
|
||||
let len = u64::from(count).saturating_mul(ty.size() as u64);
|
||||
let value_at = if len <= 4 { at + 8 } else { u64::from(self.u32_of([e[8], e[9], e[10], e[11]])) };
|
||||
entries.push(Entry { tag, ty, count, value_at });
|
||||
}
|
||||
let next = u64::from(self.u32_at(off + 2 + u64::from(n) * 12)?);
|
||||
Ok((Ifd { offset: off, entries, depth }, next))
|
||||
}
|
||||
|
||||
/// Every IFD of the file: the main chain from `first` and, depth first, the SubIFDs (tag 330) of
|
||||
/// each. Bounded by [`MAX_IFDS`] and [`MAX_DEPTH`]; a directory reached twice ends that branch.
|
||||
pub fn all_ifds(&self, first: u64) -> Result<Vec<Ifd>> {
|
||||
let mut out: Vec<Ifd> = Vec::new();
|
||||
let mut seen: Vec<u64> = Vec::new();
|
||||
let mut stack: Vec<(u64, usize)> = vec![(first, 0)];
|
||||
while let Some((off, depth)) = stack.pop() {
|
||||
let mut next = off;
|
||||
while next != 0 {
|
||||
if seen.contains(&next) {
|
||||
break;
|
||||
}
|
||||
if out.len() >= MAX_IFDS {
|
||||
return Err(Error::Malformed("too many IFDs"));
|
||||
}
|
||||
seen.push(next);
|
||||
let (ifd, after) = match self.read_ifd(next, depth) {
|
||||
Ok(x) => x,
|
||||
// a broken directory ends the chain; the first one must be readable
|
||||
Err(e) if out.is_empty() => return Err(e),
|
||||
Err(_) => break,
|
||||
};
|
||||
if depth < MAX_DEPTH
|
||||
&& let Some(sub) = ifd.get(tags::SUB_IFDS)
|
||||
&& let Ok(offs) = self.uints(sub)
|
||||
{
|
||||
for o in offs.into_iter().rev() {
|
||||
stack.push((o, depth + 1));
|
||||
}
|
||||
}
|
||||
out.push(ifd);
|
||||
next = after;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The value bytes of an entry.
|
||||
pub fn value_bytes(&self, e: &Entry) -> Result<&'a [u8]> {
|
||||
let len = (e.count as usize).checked_mul(e.ty.size()).ok_or(Error::Malformed("entry too large"))?;
|
||||
self.bytes_at(e.value_at, len)
|
||||
}
|
||||
|
||||
/// The `i`-th value of a numeric entry as f64 (rationals divided; a zero denominator is an
|
||||
/// error).
|
||||
fn number(&self, e: &Entry, b: &[u8], i: usize) -> Result<f64> {
|
||||
let s = e.ty.size();
|
||||
let v = b.get(i * s..i * s + s).ok_or(Error::Malformed("entry too short"))?;
|
||||
let u16v = |o: usize| self.u16_of([v[o], v[o + 1]]);
|
||||
let u32v = |o: usize| self.u32_of([v[o], v[o + 1], v[o + 2], v[o + 3]]);
|
||||
Ok(match e.ty {
|
||||
Type::Byte | Type::Undefined => f64::from(v[0]),
|
||||
Type::SByte => f64::from(v[0] as i8),
|
||||
Type::Short => f64::from(u16v(0)),
|
||||
Type::SShort => f64::from(u16v(0) as i16),
|
||||
Type::Long | Type::Ifd => f64::from(u32v(0)),
|
||||
Type::SLong => f64::from(u32v(0) as i32),
|
||||
Type::Rational => {
|
||||
let d = u32v(4);
|
||||
if d == 0 {
|
||||
return Err(Error::Malformed("rational with a zero denominator"));
|
||||
}
|
||||
f64::from(u32v(0)) / f64::from(d)
|
||||
}
|
||||
Type::SRational => {
|
||||
let d = u32v(4) as i32;
|
||||
if d == 0 {
|
||||
return Err(Error::Malformed("rational with a zero denominator"));
|
||||
}
|
||||
f64::from(u32v(0) as i32) / f64::from(d)
|
||||
}
|
||||
Type::Float => f64::from(f32::from_bits(u32v(0))),
|
||||
Type::Double => {
|
||||
let mut a = [0u8; 8];
|
||||
a.copy_from_slice(&v[..8]);
|
||||
if self.big_endian { f64::from_be_bytes(a) } else { f64::from_le_bytes(a) }
|
||||
}
|
||||
Type::Ascii => return Err(Error::Malformed("text where a number belongs")),
|
||||
})
|
||||
}
|
||||
|
||||
/// All values of a numeric entry as f64. Non-finite values are an error.
|
||||
pub fn floats(&self, e: &Entry) -> Result<Vec<f64>> {
|
||||
let b = self.value_bytes(e)?;
|
||||
let mut out = Vec::with_capacity(e.count as usize);
|
||||
for i in 0..e.count as usize {
|
||||
let v = self.number(e, b, i)?;
|
||||
if !v.is_finite() {
|
||||
return Err(Error::Malformed("non-finite number"));
|
||||
}
|
||||
out.push(v);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// All values of an unsigned integer entry (BYTE, SHORT, LONG, IFD).
|
||||
pub fn uints(&self, e: &Entry) -> Result<Vec<u64>> {
|
||||
if !matches!(e.ty, Type::Byte | Type::Undefined | Type::Short | Type::Long | Type::Ifd) {
|
||||
return Err(Error::Malformed("expected an unsigned integer field"));
|
||||
}
|
||||
let b = self.value_bytes(e)?;
|
||||
let s = e.ty.size();
|
||||
Ok(b.chunks_exact(s)
|
||||
.map(|v| match s {
|
||||
1 => u64::from(v[0]),
|
||||
2 => u64::from(self.u16_of([v[0], v[1]])),
|
||||
_ => u64::from(self.u32_of([v[0], v[1], v[2], v[3]])),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// The first value of an unsigned integer entry.
|
||||
pub fn uint(&self, e: &Entry) -> Result<u64> {
|
||||
self.uints(e)?.first().copied().ok_or(Error::Malformed("empty field"))
|
||||
}
|
||||
|
||||
/// An ASCII entry up to its first NUL, lossily decoded.
|
||||
pub fn ascii(&self, e: &Entry) -> Result<String> {
|
||||
let b = self.value_bytes(e)?;
|
||||
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||||
Ok(String::from_utf8_lossy(&b[..end]).trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Tag numbers (TIFF 6.0, TIFF/EP, DNG 1.7, CinemaDNG).
|
||||
pub mod tags {
|
||||
pub const NEW_SUBFILE_TYPE: u16 = 254;
|
||||
pub const IMAGE_WIDTH: u16 = 256;
|
||||
pub const IMAGE_LENGTH: u16 = 257;
|
||||
pub const BITS_PER_SAMPLE: u16 = 258;
|
||||
pub const COMPRESSION: u16 = 259;
|
||||
pub const PHOTOMETRIC: u16 = 262;
|
||||
pub const MAKE: u16 = 271;
|
||||
pub const MODEL: u16 = 272;
|
||||
pub const STRIP_OFFSETS: u16 = 273;
|
||||
pub const SAMPLES_PER_PIXEL: u16 = 277;
|
||||
pub const ROWS_PER_STRIP: u16 = 278;
|
||||
pub const STRIP_BYTE_COUNTS: u16 = 279;
|
||||
pub const PLANAR_CONFIGURATION: u16 = 284;
|
||||
pub const PREDICTOR: u16 = 317;
|
||||
pub const TILE_WIDTH: u16 = 322;
|
||||
pub const TILE_LENGTH: u16 = 323;
|
||||
pub const TILE_OFFSETS: u16 = 324;
|
||||
pub const TILE_BYTE_COUNTS: u16 = 325;
|
||||
pub const SUB_IFDS: u16 = 330;
|
||||
pub const SAMPLE_FORMAT: u16 = 339;
|
||||
pub const CFA_REPEAT_PATTERN_DIM: u16 = 33421;
|
||||
pub const CFA_PATTERN: u16 = 33422;
|
||||
pub const DNG_VERSION: u16 = 50706;
|
||||
pub const UNIQUE_CAMERA_MODEL: u16 = 50708;
|
||||
pub const CFA_PLANE_COLOR: u16 = 50710;
|
||||
pub const CFA_LAYOUT: u16 = 50711;
|
||||
pub const LINEARIZATION_TABLE: u16 = 50712;
|
||||
pub const BLACK_LEVEL_REPEAT_DIM: u16 = 50713;
|
||||
pub const BLACK_LEVEL: u16 = 50714;
|
||||
pub const BLACK_LEVEL_DELTA_H: u16 = 50715;
|
||||
pub const BLACK_LEVEL_DELTA_V: u16 = 50716;
|
||||
pub const WHITE_LEVEL: u16 = 50717;
|
||||
pub const DEFAULT_CROP_ORIGIN: u16 = 50719;
|
||||
pub const DEFAULT_CROP_SIZE: u16 = 50720;
|
||||
pub const COLOR_MATRIX_1: u16 = 50721;
|
||||
pub const COLOR_MATRIX_2: u16 = 50722;
|
||||
pub const CAMERA_CALIBRATION_1: u16 = 50723;
|
||||
pub const CAMERA_CALIBRATION_2: u16 = 50724;
|
||||
pub const ANALOG_BALANCE: u16 = 50727;
|
||||
pub const AS_SHOT_NEUTRAL: u16 = 50728;
|
||||
pub const AS_SHOT_WHITE_XY: u16 = 50729;
|
||||
pub const BASELINE_EXPOSURE: u16 = 50730;
|
||||
pub const CALIBRATION_ILLUMINANT_1: u16 = 50778;
|
||||
pub const CALIBRATION_ILLUMINANT_2: u16 = 50779;
|
||||
pub const ACTIVE_AREA: u16 = 50829;
|
||||
pub const FORWARD_MATRIX_1: u16 = 50964;
|
||||
pub const FORWARD_MATRIX_2: u16 = 50965;
|
||||
/// CinemaDNG: SMPTE ST 12-1 time codes, 8 bytes each.
|
||||
pub const TIME_CODES: u16 = 51043;
|
||||
/// CinemaDNG: frames per second (SRATIONAL).
|
||||
pub const FRAME_RATE: u16 = 51044;
|
||||
/// CinemaDNG: the camera's reel name.
|
||||
pub const REEL_NAME: u16 = 51081;
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
//! A small DNG writer for synthesising test files (feature `writer`; not used by the app): a
|
||||
//! TIFF container with the raw image in IFD 0 or in a SubIFD behind an 8-bit preview, strips or
|
||||
//! tiles, uncompressed samples of any depth or lossless JPEG (T.81 process 14) with any predictor,
|
||||
//! and whatever extra tags the caller adds.
|
||||
|
||||
use crate::ljpeg::predict;
|
||||
use crate::tiff::tags;
|
||||
|
||||
/// A tag value.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Value {
|
||||
Byte(Vec<u8>),
|
||||
Ascii(String),
|
||||
Short(Vec<u16>),
|
||||
Long(Vec<u32>),
|
||||
Rational(Vec<(u32, u32)>),
|
||||
SRational(Vec<(i32, i32)>),
|
||||
Undefined(Vec<u8>),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
fn code(&self) -> u16 {
|
||||
match self {
|
||||
Value::Byte(_) => 1,
|
||||
Value::Ascii(_) => 2,
|
||||
Value::Short(_) => 3,
|
||||
Value::Long(_) => 4,
|
||||
Value::Rational(_) => 5,
|
||||
Value::Undefined(_) => 7,
|
||||
Value::SRational(_) => 10,
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&self) -> usize {
|
||||
match self {
|
||||
Value::Byte(v) | Value::Undefined(v) => v.len(),
|
||||
Value::Ascii(s) => s.len() + 1,
|
||||
Value::Short(v) => v.len(),
|
||||
Value::Long(v) => v.len(),
|
||||
Value::Rational(v) => v.len(),
|
||||
Value::SRational(v) => v.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes(&self, be: bool) -> Vec<u8> {
|
||||
let w16 = |v: u16| if be { v.to_be_bytes() } else { v.to_le_bytes() };
|
||||
let w32 = |v: u32| if be { v.to_be_bytes() } else { v.to_le_bytes() };
|
||||
match self {
|
||||
Value::Byte(v) | Value::Undefined(v) => v.clone(),
|
||||
Value::Ascii(s) => s.bytes().chain([0]).collect(),
|
||||
Value::Short(v) => v.iter().flat_map(|&x| w16(x)).collect(),
|
||||
Value::Long(v) => v.iter().flat_map(|&x| w32(x)).collect(),
|
||||
Value::Rational(v) => v.iter().flat_map(|&(n, d)| w32(n).into_iter().chain(w32(d))).collect(),
|
||||
Value::SRational(v) => v.iter().flat_map(|&(n, d)| w32(n as u32).into_iter().chain(w32(d as u32))).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Signed rationals with denominator 10000 from floats (colour matrices and the like).
|
||||
pub fn srationals(v: &[f64]) -> Value {
|
||||
Value::SRational(v.iter().map(|&x| ((x * 10_000.0).round() as i32, 10_000)).collect())
|
||||
}
|
||||
|
||||
/// Unsigned rationals with denominator 10000.
|
||||
pub fn rationals(v: &[f64]) -> Value {
|
||||
Value::Rational(v.iter().map(|&x| ((x * 10_000.0).round().max(0.0) as u32, 10_000)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// How the raw data is stored.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Compression {
|
||||
None,
|
||||
/// Lossless JPEG: `predictor` 1–7; each tile / strip is coded as a JPEG of `components`
|
||||
/// interleaved components (its width × samples per pixel divided by `components`), with a
|
||||
/// restart marker every `restart_lines` lines (0: none) and point transform `pt`.
|
||||
LosslessJpeg {
|
||||
predictor: u8,
|
||||
components: usize,
|
||||
restart_lines: usize,
|
||||
pt: u8,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Layout {
|
||||
Strips { rows: usize },
|
||||
Tiles { width: usize, height: usize },
|
||||
}
|
||||
|
||||
/// What to write.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Options {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub samples_per_pixel: usize,
|
||||
pub bits: u32,
|
||||
pub big_endian: bool,
|
||||
pub compression: Compression,
|
||||
pub layout: Layout,
|
||||
/// CFA repeat (rows, columns) and pattern colour codes; `None` writes LinearRaw.
|
||||
pub cfa: Option<(usize, usize, Vec<u8>)>,
|
||||
/// Put an 8-bit preview in IFD 0 and the raw image in a SubIFD (as cameras do).
|
||||
pub preview: bool,
|
||||
/// Extra tags of IFD 0 (colour, CinemaDNG…).
|
||||
pub main_tags: Vec<(u16, Value)>,
|
||||
/// Extra tags of the raw IFD (black and white levels, crop…).
|
||||
pub raw_tags: Vec<(u16, Value)>,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Options {
|
||||
width: 8,
|
||||
height: 8,
|
||||
samples_per_pixel: 1,
|
||||
bits: 16,
|
||||
big_endian: false,
|
||||
compression: Compression::None,
|
||||
layout: Layout::Strips { rows: 8 },
|
||||
cfa: Some((2, 2, vec![0, 1, 1, 2])),
|
||||
preview: false,
|
||||
main_tags: Vec::new(),
|
||||
raw_tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pack `rows` rows of `per_row` samples of `bits` bits (8/16-bit in byte order `be`, other
|
||||
/// depths most significant bit first, each row starting on a byte boundary).
|
||||
pub fn pack(samples: &[u16], per_row: usize, bits: u32, be: bool) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
for row in samples.chunks(per_row.max(1)) {
|
||||
match bits {
|
||||
8 => out.extend(row.iter().map(|&v| v as u8)),
|
||||
16 => out.extend(row.iter().flat_map(|&v| if be { v.to_be_bytes() } else { v.to_le_bytes() })),
|
||||
_ => {
|
||||
let (mut acc, mut n) = (0u32, 0u32);
|
||||
for &v in row {
|
||||
acc = (acc << bits) | (u32::from(v) & ((1 << bits) - 1));
|
||||
n += bits;
|
||||
while n >= 8 {
|
||||
n -= 8;
|
||||
out.push((acc >> n) as u8);
|
||||
}
|
||||
acc &= (1 << n) - 1;
|
||||
}
|
||||
if n > 0 {
|
||||
out.push((acc << (8 - n)) as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Entropy-coded bit output with 0xFF stuffing.
|
||||
struct BitOut {
|
||||
out: Vec<u8>,
|
||||
acc: u32,
|
||||
n: u32,
|
||||
}
|
||||
|
||||
impl BitOut {
|
||||
fn put(&mut self, v: u32, len: u32) {
|
||||
for i in (0..len).rev() {
|
||||
self.acc = (self.acc << 1) | ((v >> i) & 1);
|
||||
self.n += 1;
|
||||
if self.n == 8 {
|
||||
let b = self.acc as u8;
|
||||
self.out.push(b);
|
||||
if b == 0xFF {
|
||||
self.out.push(0);
|
||||
}
|
||||
self.acc = 0;
|
||||
self.n = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pad the last byte with 1 bits.
|
||||
fn flush(&mut self) {
|
||||
while self.n != 0 {
|
||||
self.put(1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode `samples` (`width` × `height` × `comps`, interleaved) as lossless JPEG.
|
||||
pub fn lj92(samples: &[u16], width: usize, height: usize, comps: usize, precision: u8, predictor: u8, restart_lines: usize, pt: u8) -> Vec<u8> {
|
||||
let comps = comps.clamp(1, 4);
|
||||
let row = width * comps;
|
||||
let initial = 1i32 << (precision - pt - 1);
|
||||
// differences, line by line
|
||||
let mut cats: Vec<Vec<u32>> = vec![vec![0; 17]; comps];
|
||||
let mut coded: Vec<(usize, u32, u32, i32)> = Vec::with_capacity(samples.len()); // (comp, ssss, extra, line)
|
||||
let s = |i: usize| i32::from(samples.get(i).copied().unwrap_or(0) >> pt);
|
||||
for y in 0..height {
|
||||
let first_line = restart_lines == 0 && y == 0 || restart_lines != 0 && y % restart_lines == 0;
|
||||
for x in 0..width {
|
||||
for c in 0..comps {
|
||||
let i = y * row + x * comps + c;
|
||||
let px = if first_line && x == 0 {
|
||||
initial
|
||||
} else if first_line {
|
||||
s(i - comps)
|
||||
} else if x == 0 {
|
||||
s(i - row)
|
||||
} else {
|
||||
predict(predictor, s(i - comps), s(i - row), s(i - row - comps))
|
||||
};
|
||||
let d16 = (s(i) - px).rem_euclid(65_536);
|
||||
let (ssss, extra) = if d16 == 32_768 {
|
||||
(16, 0)
|
||||
} else {
|
||||
let d = if d16 >= 32_768 { d16 - 65_536 } else { d16 };
|
||||
let ssss = 32 - d.unsigned_abs().leading_zeros();
|
||||
let extra = if d >= 0 { d as u32 } else { (d + (1 << ssss) - 1) as u32 };
|
||||
(ssss, extra)
|
||||
};
|
||||
cats[c][ssss as usize] += 1;
|
||||
coded.push((c, ssss, extra, y as i32));
|
||||
}
|
||||
}
|
||||
}
|
||||
// per component: the used categories by frequency get 2-bit then 6-bit codes
|
||||
let mut tables = Vec::new();
|
||||
for counts in &cats {
|
||||
let mut syms: Vec<u8> = (0..17u8).filter(|&k| counts[usize::from(k)] > 0).collect();
|
||||
syms.sort_by_key(|&k| std::cmp::Reverse(counts[usize::from(k)]));
|
||||
if syms.is_empty() {
|
||||
syms.push(0);
|
||||
}
|
||||
let mut code_of = [(0u32, 0u32); 17];
|
||||
for (i, &k) in syms.iter().enumerate() {
|
||||
code_of[usize::from(k)] = if i < 3 { (i as u32, 2) } else { (0b110000 + (i as u32 - 3), 6) };
|
||||
}
|
||||
let mut bits = [0u8; 16];
|
||||
bits[1] = syms.len().min(3) as u8;
|
||||
bits[5] = syms.len().saturating_sub(3) as u8;
|
||||
tables.push((bits, syms, code_of));
|
||||
}
|
||||
let mut out = vec![0xFF, 0xD8];
|
||||
// SOF3
|
||||
let mut sof = vec![precision, (height >> 8) as u8, height as u8, (width >> 8) as u8, width as u8, comps as u8];
|
||||
for c in 0..comps {
|
||||
sof.extend([c as u8, 0x11, 0]);
|
||||
}
|
||||
segment(&mut out, 0xC3, &sof);
|
||||
let mut dht = Vec::new();
|
||||
for (c, (bits, syms, _)) in tables.iter().enumerate() {
|
||||
dht.push(c as u8);
|
||||
dht.extend(bits);
|
||||
dht.extend(syms);
|
||||
}
|
||||
segment(&mut out, 0xC4, &dht);
|
||||
if restart_lines != 0 {
|
||||
let ri = (restart_lines * width) as u16;
|
||||
segment(&mut out, 0xDD, &ri.to_be_bytes());
|
||||
}
|
||||
let mut sos = vec![comps as u8];
|
||||
for c in 0..comps {
|
||||
sos.extend([c as u8, (c as u8) << 4]);
|
||||
}
|
||||
sos.extend([predictor, 0, pt]);
|
||||
segment(&mut out, 0xDA, &sos);
|
||||
let mut bo = BitOut { out, acc: 0, n: 0 };
|
||||
let mut line = 0;
|
||||
let mut rst = 0u8;
|
||||
for (c, ssss, extra, y) in coded {
|
||||
if restart_lines != 0 && y != line && (y as usize).is_multiple_of(restart_lines) {
|
||||
bo.flush();
|
||||
bo.out.extend([0xFF, 0xD0 + rst]);
|
||||
rst = (rst + 1) % 8;
|
||||
}
|
||||
line = y;
|
||||
let (code, len) = tables[c].2[ssss as usize];
|
||||
bo.put(code, len);
|
||||
if ssss != 16 {
|
||||
bo.put(extra, ssss);
|
||||
}
|
||||
}
|
||||
bo.flush();
|
||||
let mut out = bo.out;
|
||||
out.extend([0xFF, 0xD9]);
|
||||
out
|
||||
}
|
||||
|
||||
fn segment(out: &mut Vec<u8>, marker: u8, body: &[u8]) {
|
||||
out.extend([0xFF, marker]);
|
||||
out.extend(((body.len() + 2) as u16).to_be_bytes());
|
||||
out.extend(body);
|
||||
}
|
||||
|
||||
/// Append an IFD at the end of `buf` (word aligned) and return its offset.
|
||||
fn put_ifd(buf: &mut Vec<u8>, mut entries: Vec<(u16, Value)>, be: bool) -> u32 {
|
||||
if buf.len() % 2 == 1 {
|
||||
buf.push(0);
|
||||
}
|
||||
entries.sort_by_key(|e| e.0);
|
||||
let at = buf.len();
|
||||
let n = entries.len();
|
||||
let w16 = |v: u16| if be { v.to_be_bytes() } else { v.to_le_bytes() };
|
||||
let w32 = |v: u32| if be { v.to_be_bytes() } else { v.to_le_bytes() };
|
||||
let mut ext_at = at + 2 + 12 * n + 4;
|
||||
let mut head = Vec::new();
|
||||
let mut ext = Vec::new();
|
||||
head.extend(w16(n as u16));
|
||||
for (tag, v) in &entries {
|
||||
let b = v.bytes(be);
|
||||
head.extend(w16(*tag));
|
||||
head.extend(w16(v.code()));
|
||||
head.extend(w32(v.count() as u32));
|
||||
if b.len() <= 4 {
|
||||
let mut inline = b.clone();
|
||||
inline.resize(4, 0);
|
||||
head.extend(inline);
|
||||
} else {
|
||||
head.extend(w32(ext_at as u32));
|
||||
ext.extend(&b);
|
||||
if b.len() % 2 == 1 {
|
||||
ext.push(0);
|
||||
}
|
||||
ext_at += b.len().div_ceil(2) * 2;
|
||||
}
|
||||
}
|
||||
head.extend(w32(0));
|
||||
buf.extend(head);
|
||||
buf.extend(ext);
|
||||
at as u32
|
||||
}
|
||||
|
||||
/// Encode the strips or tiles of `samples`, appending them to `buf`; returns (offsets, counts).
|
||||
fn put_chunks(buf: &mut Vec<u8>, samples: &[u16], o: &Options) -> (Vec<u32>, Vec<u32>) {
|
||||
let spp = o.samples_per_pixel;
|
||||
let (cw, ch) = match o.layout {
|
||||
Layout::Strips { rows } => (o.width, rows.max(1)),
|
||||
Layout::Tiles { width, height } => (width.max(1), height.max(1)),
|
||||
};
|
||||
let tiles = matches!(o.layout, Layout::Tiles { .. });
|
||||
let (mut offs, mut counts) = (Vec::new(), Vec::new());
|
||||
for ty in (0..o.height).step_by(ch) {
|
||||
for tx in (0..o.width).step_by(cw) {
|
||||
let rows = if tiles { ch } else { ch.min(o.height - ty) };
|
||||
// the chunk's samples; tiles past the image edge repeat the edge pixel
|
||||
let mut chunk = Vec::with_capacity(cw * rows * spp);
|
||||
for r in 0..rows {
|
||||
let y = (ty + r).min(o.height - 1);
|
||||
for c in 0..cw {
|
||||
let x = (tx + c).min(o.width - 1);
|
||||
for s in 0..spp {
|
||||
chunk.push(samples.get((y * o.width + x) * spp + s).copied().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
let data = match o.compression {
|
||||
Compression::None => pack(&chunk, cw * spp, o.bits, o.big_endian),
|
||||
Compression::LosslessJpeg { predictor, components, restart_lines, pt } => {
|
||||
let comps = components.max(1);
|
||||
lj92(&chunk, cw * spp / comps, rows, comps, o.bits as u8, predictor, restart_lines, pt)
|
||||
}
|
||||
};
|
||||
if buf.len() % 2 == 1 {
|
||||
buf.push(0);
|
||||
}
|
||||
offs.push(buf.len() as u32);
|
||||
counts.push(data.len() as u32);
|
||||
buf.extend(data);
|
||||
}
|
||||
}
|
||||
(offs, counts)
|
||||
}
|
||||
|
||||
/// Write a DNG holding `samples` (`width` × `height` × `samples_per_pixel`, interleaved).
|
||||
pub fn write(samples: &[u16], o: &Options) -> Vec<u8> {
|
||||
let be = o.big_endian;
|
||||
let mut buf = if be { b"MM\0*\0\0\0\0".to_vec() } else { b"II*\0\0\0\0\0".to_vec() };
|
||||
let (offs, counts) = put_chunks(&mut buf, samples, o);
|
||||
let mut raw = vec![
|
||||
(tags::NEW_SUBFILE_TYPE, Value::Long(vec![0])),
|
||||
(tags::IMAGE_WIDTH, Value::Long(vec![o.width as u32])),
|
||||
(tags::IMAGE_LENGTH, Value::Long(vec![o.height as u32])),
|
||||
(tags::BITS_PER_SAMPLE, Value::Short(vec![o.bits as u16; o.samples_per_pixel])),
|
||||
(tags::COMPRESSION, Value::Short(vec![if o.compression == Compression::None { 1 } else { 7 }])),
|
||||
(tags::SAMPLES_PER_PIXEL, Value::Short(vec![o.samples_per_pixel as u16])),
|
||||
(tags::PLANAR_CONFIGURATION, Value::Short(vec![1])),
|
||||
];
|
||||
match o.layout {
|
||||
Layout::Strips { rows } => {
|
||||
raw.push((tags::ROWS_PER_STRIP, Value::Long(vec![rows as u32])));
|
||||
raw.push((tags::STRIP_OFFSETS, Value::Long(offs)));
|
||||
raw.push((tags::STRIP_BYTE_COUNTS, Value::Long(counts)));
|
||||
}
|
||||
Layout::Tiles { width, height } => {
|
||||
raw.push((tags::TILE_WIDTH, Value::Long(vec![width as u32])));
|
||||
raw.push((tags::TILE_LENGTH, Value::Long(vec![height as u32])));
|
||||
raw.push((tags::TILE_OFFSETS, Value::Long(offs)));
|
||||
raw.push((tags::TILE_BYTE_COUNTS, Value::Long(counts)));
|
||||
}
|
||||
}
|
||||
match &o.cfa {
|
||||
Some((r, c, p)) => {
|
||||
raw.push((tags::PHOTOMETRIC, Value::Short(vec![32803])));
|
||||
raw.push((tags::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![*r as u16, *c as u16])));
|
||||
raw.push((tags::CFA_PATTERN, Value::Byte(p.clone())));
|
||||
}
|
||||
None => raw.push((tags::PHOTOMETRIC, Value::Short(vec![34892]))),
|
||||
}
|
||||
// later entries replace earlier ones with the same tag
|
||||
let merge = |base: &mut Vec<(u16, Value)>, extra: &[(u16, Value)]| {
|
||||
for (t, v) in extra {
|
||||
base.retain(|e| e.0 != *t);
|
||||
base.push((*t, v.clone()));
|
||||
}
|
||||
};
|
||||
merge(&mut raw, &o.raw_tags);
|
||||
let mut main = vec![(tags::DNG_VERSION, Value::Byte(vec![1, 4, 0, 0])), (tags::UNIQUE_CAMERA_MODEL, Value::Ascii("FilmCraft Synthetic".into()))];
|
||||
let ifd0 = if o.preview {
|
||||
let raw_at = put_ifd(&mut buf, raw, be);
|
||||
// a 2 × 2 RGB preview
|
||||
if buf.len() % 2 == 1 {
|
||||
buf.push(0);
|
||||
}
|
||||
let px_at = buf.len() as u32;
|
||||
buf.extend([128u8; 12]);
|
||||
let mut p = vec![
|
||||
(tags::NEW_SUBFILE_TYPE, Value::Long(vec![1])),
|
||||
(tags::IMAGE_WIDTH, Value::Long(vec![2])),
|
||||
(tags::IMAGE_LENGTH, Value::Long(vec![2])),
|
||||
(tags::BITS_PER_SAMPLE, Value::Short(vec![8, 8, 8])),
|
||||
(tags::COMPRESSION, Value::Short(vec![1])),
|
||||
(tags::PHOTOMETRIC, Value::Short(vec![2])),
|
||||
(tags::STRIP_OFFSETS, Value::Long(vec![px_at])),
|
||||
(tags::SAMPLES_PER_PIXEL, Value::Short(vec![3])),
|
||||
(tags::ROWS_PER_STRIP, Value::Long(vec![2])),
|
||||
(tags::STRIP_BYTE_COUNTS, Value::Long(vec![12])),
|
||||
(tags::SUB_IFDS, Value::Long(vec![raw_at])),
|
||||
];
|
||||
p.append(&mut main);
|
||||
merge(&mut p, &o.main_tags);
|
||||
put_ifd(&mut buf, p, be)
|
||||
} else {
|
||||
raw.append(&mut main);
|
||||
merge(&mut raw, &o.main_tags);
|
||||
put_ifd(&mut buf, raw, be)
|
||||
};
|
||||
let off = if be { ifd0.to_be_bytes() } else { ifd0.to_le_bytes() };
|
||||
buf[4..8].copy_from_slice(&off);
|
||||
buf
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! External oracle: LibRaw's `unprocessed_raw` sample program (an independent DNG reader) must
|
||||
//! read the raw samples of our synthetic DNGs exactly as written and as we decode them: packed
|
||||
//! uncompressed data at every depth in both byte orders, and lossless JPEG with every predictor.
|
||||
//! Skipped when `unprocessed_raw` is not installed (or `FILMCRAFT_UNPROCESSED_RAW` names it).
|
||||
//! LibRaw is only run, never linked or read.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use filmcraft_dng::Dng;
|
||||
use filmcraft_dng::writer::{Compression, Layout, Options, write};
|
||||
|
||||
fn tool() -> Option<String> {
|
||||
let t = std::env::var("FILMCRAFT_UNPROCESSED_RAW").unwrap_or_else(|_| "unprocessed_raw".into());
|
||||
Command::new(&t).arg("-q").output().ok().map(|_| t)
|
||||
}
|
||||
|
||||
fn picture(w: usize, h: usize, bits: u32, seed: u32) -> Vec<u16> {
|
||||
let mut s = seed.wrapping_mul(2_654_435_761).wrapping_add(7);
|
||||
let max = (1u32 << bits) - 1;
|
||||
(0..w * h)
|
||||
.map(|i| {
|
||||
s ^= s << 13;
|
||||
s ^= s >> 17;
|
||||
s ^= s << 5;
|
||||
let (x, y) = (i % w, i / w);
|
||||
(((x * 97 + y * 41) as u32 % (max + 1)) ^ (s & 15)).min(max) as u16
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The 16-bit samples of a binary PGM.
|
||||
fn pgm(b: &[u8]) -> Option<(usize, usize, Vec<u16>)> {
|
||||
let mut fields = Vec::new();
|
||||
let mut i = 0;
|
||||
while fields.len() < 4 {
|
||||
while b.get(i)?.is_ascii_whitespace() {
|
||||
i += 1;
|
||||
}
|
||||
let start = i;
|
||||
while !b.get(i)?.is_ascii_whitespace() {
|
||||
i += 1;
|
||||
}
|
||||
fields.push(std::str::from_utf8(&b[start..i]).ok()?.to_string());
|
||||
}
|
||||
let (w, h): (usize, usize) = (fields[1].parse().ok()?, fields[2].parse().ok()?);
|
||||
let data = b.get(i + 1..)?;
|
||||
Some((w, h, data.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes(*c)).collect()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libraw_reads_our_raw_samples() {
|
||||
let Some(tool) = tool() else {
|
||||
eprintln!("SKIPPED: unprocessed_raw (LibRaw samples) not installed");
|
||||
return;
|
||||
};
|
||||
let dir: PathBuf = std::env::temp_dir().join(format!("filmcraft-dng-oracle-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let (w, h) = (40, 26);
|
||||
let mut cases: Vec<(String, Options, u32)> = Vec::new();
|
||||
for bits in [8, 10, 12, 14, 16] {
|
||||
for be in [false, true] {
|
||||
cases.push((
|
||||
format!("u{bits}{}", if be { "be" } else { "le" }),
|
||||
Options { width: w, height: h, bits, big_endian: be, layout: Layout::Strips { rows: 7 }, ..Default::default() },
|
||||
bits,
|
||||
));
|
||||
}
|
||||
}
|
||||
for predictor in 1..=7 {
|
||||
for components in [1, 2] {
|
||||
let o = Options {
|
||||
width: w,
|
||||
height: h,
|
||||
bits: 14,
|
||||
layout: Layout::Tiles { width: 16, height: 16 },
|
||||
compression: Compression::LosslessJpeg { predictor, components, restart_lines: 0, pt: 0 },
|
||||
..Default::default()
|
||||
};
|
||||
cases.push((format!("lj{predictor}c{components}"), o, 14));
|
||||
}
|
||||
}
|
||||
for (i, (name, o, bits)) in cases.iter().enumerate() {
|
||||
let px = picture(w, h, *bits, i as u32);
|
||||
let f = write(&px, o);
|
||||
assert_eq!(Dng::parse(&f).unwrap().raw().unwrap().data, px, "{name}: ours");
|
||||
let path = dir.join(format!("{name}.dng"));
|
||||
std::fs::write(&path, &f).unwrap();
|
||||
let out = Command::new(&tool).arg("-q").arg(&path).output().unwrap();
|
||||
assert!(out.status.success(), "{name}: {}", String::from_utf8_lossy(&out.stderr));
|
||||
let (pw, ph, theirs) = pgm(&std::fs::read(dir.join(format!("{name}.dng.pgm"))).unwrap()).unwrap();
|
||||
assert_eq!((pw, ph), (w, h), "{name}");
|
||||
assert_eq!(theirs, px, "{name}: LibRaw");
|
||||
eprintln!("{name}: LibRaw agrees");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
//! Round trips through the test writer: container parsing, bit-exact raw decoding (uncompressed at
|
||||
//! every depth, lossless JPEG with every predictor), the processing maths against hand-computed
|
||||
//! values, CinemaDNG metadata, and hostile files.
|
||||
|
||||
use filmcraft_dng::color::{D50, D65, bradford, inverse, mul, rec709_to_xyz, xy_to_xyz};
|
||||
use filmcraft_dng::tiff::tags;
|
||||
use filmcraft_dng::writer::{Compression, Layout, Options, Value, lj92, write};
|
||||
use filmcraft_dng::{Dng, Error, Timecode, decode, ljpeg, probe, sniff};
|
||||
|
||||
/// A deterministic pseudo-random sequence.
|
||||
fn noise(n: usize, bits: u32, seed: u32) -> Vec<u16> {
|
||||
let mut s = seed.wrapping_mul(2_654_435_761).wrapping_add(1);
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
s ^= s << 13;
|
||||
s ^= s >> 17;
|
||||
s ^= s << 5;
|
||||
(s & ((1u32 << bits) - 1)) as u16
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A smooth picture with some noise (like a real sensor), `bits` deep.
|
||||
fn picture(w: usize, h: usize, spp: usize, bits: u32, seed: u32) -> Vec<u16> {
|
||||
let max = (1u32 << bits) - 1;
|
||||
let n = noise(w * h * spp, 4.min(bits), seed);
|
||||
(0..w * h * spp)
|
||||
.map(|i| {
|
||||
let (x, y) = ((i / spp) % w, (i / spp) / w);
|
||||
let v = (x * 37 + y * 23 + (i % spp) * 101) as u32 % (max + 1);
|
||||
(v ^ u32::from(n[i])).min(max) as u16
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sniff_and_container() {
|
||||
let px = picture(16, 8, 1, 12, 1);
|
||||
for be in [false, true] {
|
||||
for preview in [false, true] {
|
||||
let o = Options { width: 16, height: 8, bits: 12, big_endian: be, preview, ..Default::default() };
|
||||
let f = write(&px, &o);
|
||||
assert!(sniff(&f), "be {be} preview {preview}");
|
||||
let d = Dng::parse(&f).unwrap();
|
||||
let info = d.info().unwrap();
|
||||
assert_eq!((info.raw_width, info.raw_height, info.width, info.height, info.bits), (16, 8, 16, 8, 12));
|
||||
assert!(info.cfa);
|
||||
assert_eq!(info.dng_version, [1, 4, 0, 0]);
|
||||
assert_eq!(info.camera.as_deref(), Some("FilmCraft Synthetic"));
|
||||
assert_eq!(d.raw().unwrap().data, px, "be {be} preview {preview}");
|
||||
}
|
||||
}
|
||||
// a plain TIFF is not a DNG
|
||||
let mut plain = write(&px, &Options::default());
|
||||
// remove the DNGVersion tag by renaming it to an unknown tag number
|
||||
let pos = find_tag(&plain, tags::DNG_VERSION).unwrap();
|
||||
plain[pos..pos + 2].copy_from_slice(&40000u16.to_le_bytes());
|
||||
assert!(!sniff(&plain));
|
||||
assert_eq!(Dng::parse(&plain).err(), Some(Error::NotDng));
|
||||
assert!(!sniff(b"\x89PNG\r\n\x1a\n"));
|
||||
assert!(!sniff(b""));
|
||||
}
|
||||
|
||||
/// Offset of the directory entry for `tag` in a little-endian file's first IFD.
|
||||
fn find_tag(f: &[u8], tag: u16) -> Option<usize> {
|
||||
let ifd = u32::from_le_bytes(f[4..8].try_into().unwrap()) as usize;
|
||||
let n = u16::from_le_bytes(f[ifd..ifd + 2].try_into().unwrap()) as usize;
|
||||
(0..n).map(|i| ifd + 2 + 12 * i).find(|&e| u16::from_le_bytes(f[e..e + 2].try_into().unwrap()) == tag)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uncompressed_every_depth_strips_and_tiles() {
|
||||
for bits in [8, 9, 10, 12, 14, 16] {
|
||||
for (spp, cfa) in [(1, true), (3, false)] {
|
||||
for layout in [Layout::Strips { rows: 3 }, Layout::Strips { rows: 100 }, Layout::Tiles { width: 16, height: 8 }] {
|
||||
for be in [false, true] {
|
||||
let (w, h) = (21, 13);
|
||||
let px = picture(w, h, spp, bits, bits + spp as u32);
|
||||
let o = Options {
|
||||
width: w,
|
||||
height: h,
|
||||
samples_per_pixel: spp,
|
||||
bits,
|
||||
big_endian: be,
|
||||
layout,
|
||||
cfa: cfa.then(|| (2, 2, vec![0, 1, 1, 2])),
|
||||
..Default::default()
|
||||
};
|
||||
let raw = Dng::parse(&write(&px, &o)).unwrap().raw().unwrap();
|
||||
assert_eq!(raw.data, px, "bits {bits} spp {spp} {layout:?} be {be}");
|
||||
assert_eq!(raw.bits, bits);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossless_jpeg_every_predictor_bit_exact() {
|
||||
for predictor in 1..=7u8 {
|
||||
for bits in [8u32, 12, 14, 16] {
|
||||
for (components, layout) in
|
||||
[(1, Layout::Strips { rows: 5 }), (2, Layout::Tiles { width: 16, height: 16 }), (4, Layout::Tiles { width: 8, height: 4 })]
|
||||
{
|
||||
let (w, h) = (36, 22);
|
||||
let px = picture(w, h, 1, bits, u32::from(predictor) * 7 + bits);
|
||||
let o = Options {
|
||||
width: w,
|
||||
height: h,
|
||||
bits,
|
||||
layout,
|
||||
compression: Compression::LosslessJpeg { predictor, components, restart_lines: 0, pt: 0 },
|
||||
..Default::default()
|
||||
};
|
||||
let f = write(&px, &o);
|
||||
assert_eq!(probe(&f).unwrap().compression, "lossless JPEG");
|
||||
let raw = Dng::parse(&f).unwrap().raw().unwrap();
|
||||
assert_eq!(raw.data, px, "predictor {predictor} bits {bits} components {components}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossless_jpeg_extremes_restarts_and_point_transform() {
|
||||
// full-swing 16-bit noise hits every difference category including SSSS = 16
|
||||
let (w, h) = (17, 9);
|
||||
let mut px = noise(w * h * 2, 16, 3);
|
||||
px[0] = 0;
|
||||
px[2] = 32_768; // 0 after the 2^15 initial prediction is a difference of 32768 (SSSS 16)
|
||||
for predictor in 1..=7 {
|
||||
let j = lj92(&px, w, h, 2, 16, predictor, 0, 0);
|
||||
let d = ljpeg::decode(&j, 1 << 20).unwrap();
|
||||
assert_eq!((d.width, d.height, d.components, d.precision, d.predictor), (w, h, 2, 16, predictor));
|
||||
assert_eq!(d.samples, px, "predictor {predictor}");
|
||||
// restart markers every 2 and every 3 lines
|
||||
for r in [2, 3] {
|
||||
let j = lj92(&px, w, h, 2, 16, predictor, r, 0);
|
||||
assert!(j.windows(2).any(|b| b == [0xFF, 0xD0]), "has RST0");
|
||||
assert_eq!(ljpeg::decode(&j, 1 << 20).unwrap().samples, px, "predictor {predictor} restart {r}");
|
||||
}
|
||||
}
|
||||
// point transform: the low bits are dropped and the rest shifted back
|
||||
let px = picture(12, 6, 1, 14, 9);
|
||||
let j = lj92(&px, 12, 6, 1, 14, 4, 0, 2);
|
||||
let want: Vec<u16> = px.iter().map(|v| v >> 2 << 2).collect();
|
||||
assert_eq!(ljpeg::decode(&j, 1 << 20).unwrap().samples, want);
|
||||
// the sample cap
|
||||
assert_eq!(ljpeg::decode(&lj92(&px, 12, 6, 1, 14, 1, 0, 0), 71).err(), Some(Error::TooLarge));
|
||||
}
|
||||
|
||||
/// The colour tags of a camera whose white-balanced values are Rec. 709 already.
|
||||
fn rec709_camera_tags(neutral: [f64; 3]) -> Vec<(u16, Value)> {
|
||||
let cm = inverse(&rec709_to_xyz().unwrap()).unwrap();
|
||||
let fm = mul(&bradford(&xy_to_xyz(D65), &xy_to_xyz(D50)).unwrap(), &rec709_to_xyz().unwrap());
|
||||
let flat = |m: [[f64; 3]; 3]| m.iter().flatten().copied().collect::<Vec<f64>>();
|
||||
vec![
|
||||
(tags::COLOR_MATRIX_1, Value::srationals(&flat(cm))),
|
||||
(tags::FORWARD_MATRIX_1, Value::srationals(&flat(fm))),
|
||||
(tags::CALIBRATION_ILLUMINANT_1, Value::Short(vec![21])),
|
||||
(tags::AS_SHOT_NEUTRAL, Value::rationals(&neutral)),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn develop_matches_hand_computed_values() {
|
||||
// 12-bit RGGB, black 64, white 4064 (range 4000); four colour quadrants seen through a camera
|
||||
// with neutral (0.5, 1, 0.8). White-balanced values come out as Rec. 709 (the ForwardMatrix
|
||||
// is Rec. 709 → XYZ D50), so a quadrant of colour c reads back c.
|
||||
let (w, h) = (32, 24);
|
||||
let neutral: [f64; 3] = [0.5, 1.0, 0.8];
|
||||
let quad = |x: usize, y: usize| -> [f64; 3] {
|
||||
match (x < w / 2, y < h / 2) {
|
||||
(true, true) => [0.5, 0.5, 0.5],
|
||||
(false, true) => [0.8, 0.2, 0.1],
|
||||
(true, false) => [0.05, 0.6, 0.3],
|
||||
(false, false) => [0.25, 0.25, 0.9],
|
||||
}
|
||||
};
|
||||
let pat = [0u8, 1, 1, 2];
|
||||
let px: Vec<u16> = (0..w * h)
|
||||
.map(|i| {
|
||||
let (x, y) = (i % w, i / w);
|
||||
let c = usize::from(pat[(y % 2) * 2 + x % 2]);
|
||||
let camera = quad(x, y)[c] * neutral[c];
|
||||
(64.0 + camera * 4000.0).round() as u16
|
||||
})
|
||||
.collect();
|
||||
let mut main = rec709_camera_tags(neutral);
|
||||
main.push((tags::BASELINE_EXPOSURE, Value::SRational(vec![(0, 1)])));
|
||||
let raw_tags = vec![(tags::BLACK_LEVEL, Value::Short(vec![64])), (tags::WHITE_LEVEL, Value::Short(vec![4064]))];
|
||||
for compression in [Compression::None, Compression::LosslessJpeg { predictor: 6, components: 2, restart_lines: 0, pt: 0 }] {
|
||||
let o = Options { width: w, height: h, bits: 12, compression, main_tags: main.clone(), raw_tags: raw_tags.clone(), ..Default::default() };
|
||||
let img = decode(&write(&px, &o)).unwrap();
|
||||
assert_eq!((img.width, img.height), (w, h));
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
// away from the quadrant edges (the demosaic reaches two pixels)
|
||||
if (x as isize - w as isize / 2).abs() < 3 || (y as isize - h as isize / 2).abs() < 3 {
|
||||
continue;
|
||||
}
|
||||
let got = &img.rgba[(y * w + x) * 4..][..4];
|
||||
let want = quad(x, y);
|
||||
for k in 0..3 {
|
||||
assert!((got[k] - want[k] as f32).abs() < 2e-3, "({x},{y}) ch {k}: {} vs {}", got[k], want[k]);
|
||||
}
|
||||
assert_eq!(got[3], 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
// BaselineExposure +1: twice as bright
|
||||
let mut main1 = rec709_camera_tags(neutral);
|
||||
main1.push((tags::BASELINE_EXPOSURE, Value::SRational(vec![(1, 1)])));
|
||||
let o = Options { width: w, height: h, bits: 12, main_tags: main1, raw_tags, ..Default::default() };
|
||||
let img = decode(&write(&px, &o)).unwrap();
|
||||
assert!((img.rgba[(4 * w + 4) * 4] - 1.0).abs() < 4e-3, "{}", img.rgba[(4 * w + 4) * 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_area_default_crop_and_linear_raw() {
|
||||
// LinearRaw 3 samples, 10 × 6, active area cuts one pixel off each side, crop 4 × 2 at (2, 1)
|
||||
let (w, h) = (10, 6);
|
||||
let px: Vec<u16> = (0..w * h).flat_map(|i| [i as u16 * 100, 50, 7]).collect();
|
||||
let o = Options {
|
||||
width: w,
|
||||
height: h,
|
||||
samples_per_pixel: 3,
|
||||
bits: 16,
|
||||
cfa: None,
|
||||
raw_tags: vec![
|
||||
(tags::ACTIVE_AREA, Value::Long(vec![1, 1, 5, 9])),
|
||||
(tags::DEFAULT_CROP_ORIGIN, Value::Long(vec![2, 1])),
|
||||
(tags::DEFAULT_CROP_SIZE, Value::Rational(vec![(8, 2), (2, 1)])),
|
||||
(tags::WHITE_LEVEL, Value::Long(vec![65_535])),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let f = write(&px, &o);
|
||||
let info = probe(&f).unwrap();
|
||||
assert_eq!((info.width, info.height, info.cfa, info.samples_per_pixel), (4, 2, false, 3));
|
||||
let img = decode(&f).unwrap();
|
||||
assert_eq!((img.width, img.height), (4, 2));
|
||||
// output (0, 0) is raw (1 + 2, 1 + 1) = sample 23
|
||||
assert!((img.rgba[0] - 2300.0 / 65_535.0).abs() < 1e-6);
|
||||
assert!((img.rgba[1] - 50.0 / 65_535.0).abs() < 1e-6);
|
||||
// output (3, 1) is raw (6, 3) = sample 36
|
||||
assert!((img.rgba[(4 + 3) * 4] - 3600.0 / 65_535.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cinemadng_frame_rate_time_code_and_reel() {
|
||||
let px = picture(8, 8, 1, 12, 2);
|
||||
let o = Options {
|
||||
bits: 12,
|
||||
main_tags: vec![
|
||||
(tags::FRAME_RATE, Value::SRational(vec![(24_000, 1001)])),
|
||||
// 01:02:03:04, drop frame
|
||||
(tags::TIME_CODES, Value::Byte(vec![0x44, 0x03, 0x02, 0x01, 0, 0, 0, 0])),
|
||||
(tags::REEL_NAME, Value::Ascii("A001".into())),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let info = probe(&write(&px, &o)).unwrap();
|
||||
assert_eq!(info.frame_rate, Some((24_000, 1001)));
|
||||
assert_eq!(info.timecode, Some(Timecode { hours: 1, minutes: 2, seconds: 3, frames: 4, drop_frame: true }));
|
||||
assert_eq!(info.reel.as_deref(), Some("A001"));
|
||||
// nonsense rates and time codes are ignored
|
||||
let o = Options {
|
||||
bits: 12,
|
||||
main_tags: vec![(tags::FRAME_RATE, Value::SRational(vec![(-25, 1)])), (tags::TIME_CODES, Value::Byte(vec![0x0A, 0x99, 0, 0, 0, 0, 0, 0]))],
|
||||
..Default::default()
|
||||
};
|
||||
let info = probe(&write(&px, &o)).unwrap();
|
||||
assert_eq!((info.frame_rate, info.timecode), (None, None));
|
||||
}
|
||||
|
||||
/// Every hostile variant must give an error or a picture, never a panic.
|
||||
fn survives(f: &[u8]) -> bool {
|
||||
std::panic::catch_unwind(|| {
|
||||
let _ = probe(f);
|
||||
let _ = decode(f);
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_files_are_errors_not_panics() {
|
||||
let px = picture(24, 16, 1, 12, 5);
|
||||
let files: Vec<Vec<u8>> = [
|
||||
Options { width: 24, height: 16, bits: 12, ..Default::default() },
|
||||
Options { width: 24, height: 16, bits: 12, preview: true, layout: Layout::Tiles { width: 16, height: 16 }, ..Default::default() },
|
||||
Options {
|
||||
width: 24,
|
||||
height: 16,
|
||||
bits: 12,
|
||||
compression: Compression::LosslessJpeg { predictor: 4, components: 2, restart_lines: 2, pt: 0 },
|
||||
main_tags: rec709_camera_tags([0.5, 1.0, 0.7]),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
.iter()
|
||||
.map(|o| write(&px, o))
|
||||
.collect();
|
||||
for f in &files {
|
||||
assert!(decode(f).is_ok());
|
||||
// every truncation
|
||||
for n in 0..f.len() {
|
||||
assert!(survives(&f[..n]), "truncated to {n}");
|
||||
if n < f.len() / 2 {
|
||||
assert!(decode(&f[..n]).is_err(), "truncated to {n}");
|
||||
}
|
||||
}
|
||||
// bit flips and corrupt bytes everywhere
|
||||
let mut s = 12345u32;
|
||||
for i in 0..4000 {
|
||||
let mut g = f.clone();
|
||||
for _ in 0..1 + i % 4 {
|
||||
s ^= s << 13;
|
||||
s ^= s >> 17;
|
||||
s ^= s << 5;
|
||||
let at = s as usize % g.len();
|
||||
g[at] = if i % 3 == 0 { 0xFF } else { g[at] ^ (1 << (s >> 29)) };
|
||||
}
|
||||
assert!(survives(&g), "mutation {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyclic_ifds_huge_sizes_and_bad_offsets() {
|
||||
let px = picture(8, 8, 1, 16, 4);
|
||||
let f = write(&px, &Options { preview: true, ..Default::default() });
|
||||
let ifd0 = u32::from_le_bytes(f[4..8].try_into().unwrap()) as usize;
|
||||
let n = u16::from_le_bytes(f[ifd0..ifd0 + 2].try_into().unwrap()) as usize;
|
||||
// IFD 0's next pointer to itself
|
||||
let mut g = f.clone();
|
||||
g[ifd0 + 2 + 12 * n..ifd0 + 6 + 12 * n].copy_from_slice(&(ifd0 as u32).to_le_bytes());
|
||||
assert!(decode(&g).is_ok(), "a self-loop ends the chain");
|
||||
// the SubIFD pointing back at IFD 0
|
||||
let mut g = f.clone();
|
||||
let sub = find_tag(&g, tags::SUB_IFDS).unwrap();
|
||||
g[sub + 8..sub + 12].copy_from_slice(&(ifd0 as u32).to_le_bytes());
|
||||
assert!(matches!(decode(&g), Err(Error::Unsupported(_))), "no raw image left");
|
||||
// huge dimensions with a tiny file
|
||||
for (wtag, v) in [(tags::IMAGE_WIDTH, 65_535u32), (tags::IMAGE_LENGTH, 65_535), (tags::IMAGE_WIDTH, 0), (tags::IMAGE_WIDTH, u32::MAX)] {
|
||||
let mut g = write(&px, &Options::default());
|
||||
let e = find_tag(&g, wtag).unwrap();
|
||||
g[e + 8..e + 12].copy_from_slice(&v.to_le_bytes());
|
||||
assert!(decode(&g).is_err(), "{wtag} = {v}");
|
||||
}
|
||||
// a strip offset past the end of the file
|
||||
let mut g = write(&px, &Options::default());
|
||||
let e = find_tag(&g, tags::STRIP_OFFSETS).unwrap();
|
||||
g[e + 8..e + 12].copy_from_slice(&0xFFFF_FF00u32.to_le_bytes());
|
||||
assert_eq!(decode(&g).err(), Some(Error::Truncated));
|
||||
// an IFD offset past the end
|
||||
let mut g = write(&px, &Options::default());
|
||||
g[4..8].copy_from_slice(&0x7FFF_FFFFu32.to_le_bytes());
|
||||
assert!(decode(&g).is_err());
|
||||
// a rational with a zero denominator in a raw tag
|
||||
let o = Options { raw_tags: vec![(tags::DEFAULT_CROP_SIZE, Value::Rational(vec![(8, 0), (8, 1)]))], ..Default::default() };
|
||||
assert!(matches!(decode(&write(&px, &o)), Err(Error::Malformed(_))));
|
||||
// unsupported compressions and depths are named
|
||||
let o = Options { raw_tags: vec![(tags::COMPRESSION, Value::Short(vec![34892]))], ..Default::default() };
|
||||
assert_eq!(decode(&write(&px, &o)).err(), Some(Error::Unsupported("lossy JPEG DNG".into())));
|
||||
}
|
||||
@@ -29,7 +29,7 @@ Design principles:
|
||||
L2 edit · codecs · interchange · captions · speech · tts · tts-text
|
||||
L1 frame · media · project · audio-dsp · text
|
||||
L0 foundation: time · geom · color · bitstream · testkit (dev-dependency only)
|
||||
codecs/containers: isobmff · matroska · mxf · cfb · mpegts · ogg · h264 · h264enc · hevc · vp9 · av1 · mpeg2v · prores · dnx · apv · aac · ac3 · opus
|
||||
codecs/containers: isobmff · matroska · mxf · cfb · mpegts · ogg · h264 · h264enc · hevc · vp9 · av1 · mpeg2v · prores · dnx · apv · dng · aac · ac3 · opus
|
||||
```
|
||||
|
||||
Crates are named `filmcraft-<dir>` (`crates/time` is `filmcraft-time`). The apps are `filmcraft`
|
||||
@@ -52,6 +52,7 @@ and `filmcraft-cli`.
|
||||
| `prores` | L0 | ProRes decoder and encoder |
|
||||
| `dnx` | L0 | DNxHD / DNxHR (SMPTE ST 2019-1 VC-3) decoder and DNxHR encoder |
|
||||
| `apv` | L0 | Advanced Professional Video (IETF RFC 9924) decoder and encoder ([README](../crates/apv/README.md)) |
|
||||
| `dng` | L0 | DNG / CinemaDNG camera RAW decoder: TIFF container, uncompressed and lossless JPEG raw data, DNG processing to linear Rec. 709 ([README](../crates/dng/README.md)) |
|
||||
| `av1` | L0 | AV1 decoder (Main profile; bit-exact with libdav1d; see its README for the stage table) |
|
||||
| `mpeg2v` | L0 | MPEG-2 video (Main / 4:2:2 profile; frame and field pictures, dual prime) and MPEG-1 video decoder ([README](../crates/mpeg2v/README.md)) |
|
||||
| `aac` | L0 | AAC-LC decoder and encoder |
|
||||
|
||||
@@ -49,6 +49,7 @@ const LAYERS: &[(&str, u8)] = &[
|
||||
("mjpeg", 0),
|
||||
("dnx", 0),
|
||||
("apv", 0),
|
||||
("dng", 0),
|
||||
("av1", 0),
|
||||
("aac", 0),
|
||||
("opus", 0),
|
||||
|
||||
Reference in new issue
Block a user