#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:
vangel76andClaude Opus 5.5 committed 2026-10-11 15:44:00 +02:00
1 parent 4e2d0bc2d3
commit 28247afae7
16 files changed
+3286 -1

No files matched your search

Generated
+11
View File
@@ -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",
+4
View File
@@ -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
+23
View File
@@ -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
+88
View File
@@ -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.
+385
View File
@@ -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);
}
}
+255
View File
@@ -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");
}
}
+358
View File
@@ -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()
}
+395
View File
@@ -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,
}
}
+264
View File
@@ -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());
}
}
+254
View File
@@ -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 })
}
+334
View File
@@ -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;
}
+449
View File
@@ -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
}
+97
View File
@@ -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);
}
+366
View File
@@ -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())));
}
+2 -1
View File
@@ -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 |
+1
View File
@@ -49,6 +49,7 @@ const LAYERS: &[(&str, u8)] = &[
("mjpeg", 0),
("dnx", 0),
("apv", 0),
("dng", 0),
("av1", 0),
("aac", 0),
("opus", 0),