mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
craft-raw: Nikon NEF and NRW (#965)
* craft-raw: the file-format layer of camera raw files, with DNG and CRW A new crate for #587 and #778: bytes in, the sensor data out as a neutral RawImage (samples, CFA, levels, active area, crop, orientation, colour tags, opcode lists, profile tables), and back for DNG. One Cargo feature per format: dng (read and write), jxl (JPEG XL DNG), crw (through craft-crw). The base is LightCraft's crates/raw at 5ff567a4: dng, dngwrite, tiffraw, unpack, ljpeg, jxl, semantic, preview, the parsing half of opcodes, profile and gaintable, the model and probe. Demosaicing and everything after it stay with the develop pipeline. Ported from PhotoCraft's and FilmCraft's DNG readers, each with a test: the default crop's per-tag defaults, an unusable crop falling back to the active area, non-finite white levels, the white default of a linearized image, the AsShotWhiteXY range, 8-bit-only lossy JPEG, lossy tiles that must cover their part of the image, and the CinemaDNG frame tags. * craft-raw: Hasselblad 3FR and FFF (feature hasselblad) RawFormat::CfaTiff decodes: the DNG-layout reader plus what these files add. Uncompressed data or Hasselblad's lossless JPEG (selection value 8), RGGB when no CFAPattern is stated, an FFF's colour matrix from the 3FR header it embeds, and the 8-bit RGB preview handed on as a baseline TIFF. Moved from LightCraft's vendor/hasselblad.rs at 5ff567a4 with its tests. Ported from PhotoCraft's reader: four BlackLevel values without a BlackLevelRepeatDim are one per CFA site in a file that is not a DNG. * craft-raw: Minolta MRW, Epson ERF, Mamiya MEF and Kodak DCS520C Four formats only LightCraft decodes, one feature each (mrw, erf, mef, kodak), moved from its vendor modules at 5ff567a4 with their tests; the MRW block container comes from lightcraft-meta. src/shared.rs holds the level helpers the maker decoders share. ERF and MEF state no white balance: the estimate LightCraft made from the preview is develop-side work, so wb_multipliers is None for them here. * craft-raw: shared scan budget for tagged previews, tile-sized lossy JPEG budget, bound on Deflate tile padding Ported from storytold/lightcraft#3695. Candidate preview ranges are deduplicated and charged to one byte budget and one header-walk budget; a lossy JPEG decoded for a tile may hold no more samples than the tile rounded up to whole MCUs (and never more than Limits::max_lossy_tile_samples); a Deflate chunk whose nominal size is mostly padding outside the image is refused before it is inflated. * craft-raw: lossless JPEG sample bound per referenced byte; previews larger than the file are passed over Ported from storytold/lightcraft#3910 (the parts whose code is in craft-raw today). check_image refuses a lossless JPEG image that declares more than 32 samples per referenced byte, counted once per chunk that points at the bytes so identical tiles stored once still decode. The Hasselblad RGB preview search passes over an image whose pixel bytes exceed the file and still takes a smaller complete one. * craft-raw: test for the Deflate padding bound * craft-raw: Pentax PEF and Samsung SRW Moved from LightCraft crates/raw (vendor/pef.rs, vendor/srw.rs) at 5ff567a4. The Huffman difference decoder and bit reader that PEF shares with the Nikon, Panasonic and Sony codings become the huffman module. * craft-raw: Canon CR2, with sRAW and mRAW Moved from LightCraft crates/raw (vendor/cr2.rs, vendor/cr2/sraw.rs) at 5ff567a4. The white-balance presets a CR2 lists come back as data in ColorData::kelvin_presets (the ordering from LightCraft's kelvin_presets.rs; the temperature and tint arithmetic stays with the developer). * craft-raw: an sRAW preview larger than the file is refused before its buffer is reserved Ported from storytold/lightcraft#3910 (cr2::sraw::read_preview). * WIP craft-raw: Nikon NEF/NRW (does not compile yet: limits threading, develop() calls in tests) * craft-raw: Nikon NEF/NRW, with PhotoCraft's maker-note compression routing and 12-bit D4 white level --------- Co-authored-by: radialmonster <radial@gmail.com>
This commit is contained in:
1 parent
2b35d24324
commit
69d812dd7a
10 files changed
+2692
-5
No files matched your search
@@ -93,7 +93,7 @@ crates/raw (craft-raw) is moved from LightCraft, with behaviour ported from two
|
||||
- LightCraft, crates/raw (src/lib.rs, dng.rs, dngwrite.rs, tiffraw.rs, unpack.rs, ljpeg.rs, jxl.rs,
|
||||
opcodes.rs, profile.rs, gaintable.rs, semantic.rs, preview.rs, vendor/crw.rs, vendor/hasselblad.rs,
|
||||
vendor.rs (shared level helpers), vendor/mrw.rs, vendor/erf.rs, vendor/mef.rs, vendor/kodak.rs, vendor/pef.rs,
|
||||
vendor/srw.rs, vendor/cr2.rs, vendor/cr2/sraw.rs, kelvin_presets.rs (the preset ordering), tests/it/vendor_robust.rs and their tests) and crates/meta/src/mrw.rs (the MRW block container),
|
||||
vendor/srw.rs, vendor/cr2.rs, vendor/cr2/sraw.rs, vendor/nef.rs, vendor/nefc.rs, vendor/nef_lens.rs, docs/nikon-lens-corrections.md, kelvin_presets.rs (the preset ordering), tests/it/vendor_robust.rs and their tests) and crates/meta/src/mrw.rs (the MRW block container),
|
||||
revision 5ff567a4a2e067bef1b6bce152c11e7f727ccbaf.
|
||||
https://github.com/storytold/lightcraft
|
||||
Copyright (c) 2026 ArtCraft Team and the LightCraft contributors.
|
||||
|
||||
@@ -134,8 +134,7 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
|
||||
what PhotoCraft's and FilmCraft's readers added: container parsing and decompression to a neutral
|
||||
raw image (samples, CFA, black and white levels, active area, crop, orientation, colour tags,
|
||||
opcode lists, embedded previews), one Cargo feature per format. DNG reading and writing,
|
||||
Hasselblad 3FR/FFF, Minolta MRW, Epson ERF, Mamiya MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2 and Canon CRW (through
|
||||
Hasselblad 3FR/FFF, Minolta MRW, Epson ERF, Mamiya MEF, Kodak DCS, Pentax PEF, Samsung SRW and Canon CRW (through
|
||||
Hasselblad 3FR/FFF, Minolta MRW, Epson ERF, Mamiya MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2, Nikon NEF/NRW and Canon CRW (through
|
||||
`craft-crw`) so far; demosaicing and everything after stay with the develop
|
||||
pipeline.
|
||||
- [`craft-tga`](crates/tga/src/lib.rs): Truevision Targa, from FilmCraft (reader) and PhotoCraft
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "craft-raw"
|
||||
description = "Camera raw files, the file-format layer: container parsing, decompression and the sensor data out as a neutral raw image with its levels, crop, orientation, colour tags and embedded previews. DNG (read and write), Hasselblad 3FR/FFF, MRW, ERF, MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2 and Canon CRW so far."
|
||||
description = "Camera raw files, the file-format layer: container parsing, decompression and the sensor data out as a neutral raw image with its levels, crop, orientation, colour tags and embedded previews. DNG (read and write), Hasselblad 3FR/FFF, MRW, ERF, MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2, Nikon NEF/NRW and Canon CRW so far."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -11,7 +11,7 @@ repository.workspace = true
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["dng", "jxl", "crw", "hasselblad", "mrw", "erf", "mef", "kodak", "pef", "srw", "cr2"]
|
||||
default = ["dng", "jxl", "crw", "hasselblad", "mrw", "erf", "mef", "kodak", "pef", "srw", "cr2", "nef"]
|
||||
## Adobe DNG, reading and writing: uncompressed, lossless JPEG, lossy JPEG, Deflate.
|
||||
dng = ["dep:miniz_oxide", "dep:zune-jpeg", "dep:zune-core"]
|
||||
## JPEG XL compressed DNGs (DNG 1.7, compression 52546) via jxl-oxide (pure Rust, MIT OR Apache-2.0).
|
||||
@@ -32,6 +32,8 @@ pef = []
|
||||
srw = []
|
||||
## Canon CR2: sliced lossless JPEG, and sRAW / mRAW (subsampled YCbCr) as linear RGB.
|
||||
cr2 = []
|
||||
## Nikon NEF and NRW: uncompressed, packed, Nikon's Huffman coding (lossless and lossy), small raw.
|
||||
nef = []
|
||||
## Canon CRW (CIFF) through craft-crw, the one CRW decoder.
|
||||
crw = ["dep:craft-crw", "dep:chrono"]
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ One module and one Cargo feature per maker or container family. All are on by de
|
||||
| `jxl` | JPEG XL DNG (DNG 1.7, compression 52546) | Lossless and lossy tiles through `jxl-oxide`; implies `dng` |
|
||||
| `crw` | Canon CRW (CIFF) | Decoded by [`craft-crw`](../crw/README.md), the one CRW decoder, and returned through this crate's types |
|
||||
| `cr2` | Canon CR2 | The lossless-JPEG frame of IFD 3 de-sliced (`0xc640`); the layout from `CR2CFAPattern` (`0xc5e0`); image area from `SensorInfo`; black measured on the masked columns; white from the saturation plateau; as-shot white balance from `ColorData` or `WBInfo`; the white-balance presets at their temperatures (`ColorData::kelvin_presets`). sRAW / mRAW (subsampled YCbCr, 4:2:2 and 4:2:0) come out as linear white-balanced RGB with three samples per pixel, the chroma gains measured against the file's own 16-bit RGB image in IFD 2 |
|
||||
| `nef` | Nikon NEF / NRW | Uncompressed (16-bit words, 12-bit packed, Coolpix word packing), the packed layouts labelled compression 34713, Nikon's Huffman coding (lossless and lossy types, 12 and 14 bits, "lossy after split") with the `0x0096` linearisation table, small raw (YCbCr 4:2:2) as linear white-balanced RGB; black from `0x003d` or the NRW block, as-shot white balance from `0x000c`, `CropArea` or the measured image area of bodies that record none, masked trailing columns left out of the active area, the D1X pixel aspect, and the in-file distortion correction as a `WarpRectilinear` opcode ([derivation](docs/nikon-lens-corrections.md)). High Efficiency (HE / HE★) is refused by name; the previews still read |
|
||||
| `mrw` | Minolta / Konica Minolta MRW | The `\0MRM` block container (`PRD`, `WBG`, `TTW`), 12-bit packed and 16-bit word mosaics, as-shot white balance from `WBG`, white from the saturation plateau |
|
||||
| `erf` | Epson ERF (R-D1, R-D1s, R-D1x) | 12-bit samples in 16-byte blocks, black levels from the maker note, the measured picture origin. No white balance (the file states none) |
|
||||
| `mef` | Mamiya MEF (ZD) | Uncompressed 12-bit packed; default crop from the RGB preview's measured window. No white balance (the file states none) |
|
||||
@@ -132,6 +133,15 @@ Left as LightCraft has it, with the reason:
|
||||
camera's white balance in (`wb_multipliers` is `[1, 1, 1]`) and measures the chroma scale per file against the
|
||||
IFD 2 image, on 31 files of 16 models including the 2008 to 2010 bodies whose chroma scale differs. That reading
|
||||
is kept; a developer that wants camera RGB for version 15 can divide by the same word.
|
||||
- Nikon NEF / NRW is LightCraft's reader with two things from PhotoCraft's:
|
||||
- The maker note's compression mode (`0x0093`, or the `u16` at byte 10 of `0x0051`) is read first. 6 and 10
|
||||
send the strip to the packed reader even when a `0x0096` table is present, and allow 14-bit rows without
|
||||
padding; 13 and 14 are High Efficiency. LightCraft chose the packed reader only when there was no table and
|
||||
recognised High Efficiency by the JPEG XS markers; both rules remain for files without the mode.
|
||||
- A 12-bit Nikon D4 file takes the measured 14-bit saturation level shifted like its samples (3880).
|
||||
LightCraft measured it from the data.
|
||||
LightCraft's built-in white-balance preset table for bodies that record none is not file data and stays with
|
||||
the developer. `CropArea`, the D90 / D5100 / D800E areas and the 14-bit-unit black level are the same in both.
|
||||
- Lossy JPEG is accepted for CFA data (one grey component) as in PhotoCraft; FilmCraft refuses it
|
||||
because DNG 1.4 allows the coding for LinearRaw only.
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# Nikon embedded distortion corrections
|
||||
|
||||
Nikon writes its own distortion correction into NEFs from its mirrorless bodies, in the raw SubIFD tag `0xc7d5`
|
||||
(NikonNEFInfo). LightCraft reads it for the NIKON Z 6_2 (Z 6II), the NIKON Z 8 and the NIKON Z f and turns it into an
|
||||
`OpcodeList3` `WarpRectilinear`, so it goes through the same profile correction control, optics path, orientation
|
||||
handling and DNG export as the other embedded corrections. Other bodies are left alone for now even if they have the
|
||||
same tag, because I've only checked files from these three.
|
||||
|
||||
This is distortion only. The vignetting block (`0x0006`) and what looks like lateral CA (`0x0007`) aren't applied
|
||||
yet. Like the Sony, Fujifilm and Panasonic corrections it's on by default and can be turned off under Lens
|
||||
Corrections.
|
||||
|
||||
## Where the data comes from
|
||||
|
||||
The [ExifTool Nikon tag names](https://exiftool.org/TagNames/Nikon.html) give the names of NikonNEFInfo and its
|
||||
DistortionInfo entry (`0x0005`): DistortionCorrection and RadialDistortionCoefficient1 to 3. Nothing else was taken
|
||||
from other software. The rest was worked out by comparing LightCraft's uncorrected render of each NEF with the
|
||||
full-size JPEG the camera embeds in it. That JPEG is always distortion-corrected with Z lenses ("On (Required)"), and
|
||||
with other lenses when Auto distortion control is on. The comparison matched textured patches between the two images
|
||||
(normalised cross-correlation on the green channel at quarter size).
|
||||
|
||||
NikonNEFInfo starts with `"Nikon\0"` and a 4-byte version, followed by a small TIFF stream. The two entries used here:
|
||||
|
||||
| Entry | Bytes | What it holds |
|
||||
|---|---|---|
|
||||
| `0x0005` DistortionInfo | 0–3 | `"0100"` |
|
||||
| | 4 | correction mode: 1 "On (Optional)", 3 "On (Required)", 0 none |
|
||||
| | 8–15 | normalisation radius in mm, a signed rational. It's 21.6 (the FX half diagonal) in every file I looked at |
|
||||
| | 16–19 | number of terms, 4 |
|
||||
| | 20, 28, 36 | the three coefficients, called `k1`, `k2`, `k3` here after ExifTool's names |
|
||||
| | 64–71 | the zoom `q` the camera uses to fill the frame |
|
||||
| `0x0008` | 0–3, 4–11, 12–19 | `"0100"`, then the optical center's offset from the center of the image area in pixels (x right, y down) |
|
||||
|
||||
The `0x0008` offset is where the in-body stabilization left the sensor. It's zero with a VR lens, since the lens does
|
||||
the stabilizing, and up to about 100 px with the Z lenses. The center of the distortion measured in the JPEG follows
|
||||
it closely, for example (−90.1, +18.6) recorded and (−91.2, +17.5) measured.
|
||||
|
||||
From an output pixel to the raw, around the optical center, the mapping is
|
||||
|
||||
```
|
||||
rho = r_out / half diagonal of the image area
|
||||
r_src = r_out * (1 + k3*rho^2 + k2*rho^4 + k1*rho^6) / (1 + q)
|
||||
```
|
||||
|
||||
So the stored order goes from the highest power down. Reading them the other way round (`k1` with `rho^2`) doesn't fit
|
||||
the JPEG at all. `q` really is the zoom: fitting a separate zoom for every file gives `1 / (1 + q)` to four decimals
|
||||
(0.9675 fitted and 0.9676 from `q` on one of them), so nothing is fitted per file. The decoder turns this into DNG
|
||||
terms the same way `docs/sony-lens-corrections.md` describes for Sony, with the center in active-area coordinates and
|
||||
the coefficients rescaled to the farthest corner of the active area.
|
||||
|
||||
## How well it matches
|
||||
|
||||
I checked it on 83 of my own Z 6II photos with three lenses between 20 and 200 mm, 45 Z 8 photos (below), plus the
|
||||
CC0 Z f sample in the corpus. The numbers are the radial error against the camera JPEG at full size (6048 px wide), out to 0.95 half
|
||||
diagonals:
|
||||
|
||||
| Lens | Focal length | Frames | Uncorrected median / p95 | Corrected median / p95 |
|
||||
|---|---:|---:|---:|---:|
|
||||
| VR 70-200mm f/2.8G (F-mount, FTZ) | 70 mm | 11 | 10.9 / 15.9 px | 0.19 / 0.57 px |
|
||||
| VR 70-200mm f/2.8G (F-mount, FTZ) | 105–140 mm | 15 | 34–42 / 38–46 px | 0.13–0.18 / 0.59–0.90 px |
|
||||
| VR 70-200mm f/2.8G (F-mount, FTZ) | 175–200 mm | 21 | 45–46 / 50–51 px | 0.10–0.18 / 0.48–0.95 px |
|
||||
| NIKKOR Z 24-70mm f/4 S | 70 mm | 8 | 126.4 / 141.2 px | 0.48 / 4.38 px |
|
||||
| NIKKOR Z 24-70mm f/4 S | 38–59 mm | 6 | 59–101 / 69–121 px | 0.36–0.76 / 4.6–6.8 px |
|
||||
| NIKKOR Z 24-70mm f/4 S | 33–34 mm | 4 | 32–44 / 43–53 px | 1.0–1.3 / 8.5–9.6 px |
|
||||
| NIKKOR Z 24-70mm f/4 S | 24 mm | 7 | 16.0 / 105.6 px | 6.90 / 23.56 px |
|
||||
| NIKKOR Z 20mm f/1.8 S | 20 mm | 5 | 8.2 / 53.9 px | 1.49 / 5.46 px |
|
||||
| Tamron 35-150mm (Z f, CC0 corpus) | 150 mm | 1 | 70.4 px median | 0.22 px median |
|
||||
|
||||
On the Z 8 (8256 px wide JPEGs) it looks the same. The 70-200 f/2.8G on the FTZ matches within 0.14 to 0.21 px median
|
||||
at 70, 135 and 200 mm. The NIKKOR Z 24-120mm f/4 S is within 0.6 to 1.0 px median from 36 to 120 mm and 1.5 px at 24
|
||||
to 28 mm, with the same kind of error toward the edges at the wide end as the Z lenses on the Z 6II. A few 14-24mm
|
||||
f/2.8G frames had Auto distortion control off, so their JPEGs aren't corrected and match about the same either way.
|
||||
|
||||
I also rendered a few with `lightcraft-cli render` at default settings and measured those against the camera JPEG.
|
||||
The median error was 0.10 px for the 70-200 at 200 mm and 0.12 px for the Z f sample. The 24-70 was 0.32 px at 70 mm,
|
||||
on a frame with a 92 px sensor shift, and 8.4 px at 24 mm. The 20 mm came out at 1.3 px. On the Z 8 it was 0.14 px for the 70-200 at 200 mm, 0.45 and 0.68 px for the 24-120
|
||||
at 68 and 120 mm, 1.8 px for the Z 20 mm and 3.7 px for the 24-120 at 24 mm. That checks the
|
||||
center, crop and DNG conversion along with the model itself.
|
||||
|
||||
The F-mount lens on the FTZ matches about as well as I can measure. The native Z lenses still have a smooth error
|
||||
toward the edges that doesn't depend on the sensor shift and is the same in every frame at a given focal length: about
|
||||
+22 px at 0.7 to 0.9 half diagonals at 24 mm, +8 px at 33 mm and +3.5 px at 70 mm. It seems the camera applies an
|
||||
extra correction for Z lenses that isn't in the readable coefficients. NikonNEFInfo `0x000e` looks encrypted and I
|
||||
didn't try to decode it. Even so, the readable correction takes most of the distortion out. At 24 mm the error at 0.7
|
||||
half diagonals goes from −82 px to +25 px, and at 70 mm from about −137 px to under 4 px.
|
||||
|
||||
## What's left out
|
||||
|
||||
Only `NIKON Z 6_2`, `NIKON Z 8` and `NIKON Z f` are corrected, matched exactly. A matching tag on another body doesn't tell us its
|
||||
pixel pitch or image area, so other bodies can be added once someone checks their files. Only the full-frame 3:2
|
||||
image area is corrected, because the stored 21.6 mm radius is the FX half diagonal, so DX and other crops are skipped.
|
||||
The decoder also skips data it doesn't recognise (another layout, radius or term count, or no correction mode),
|
||||
values that are clearly wrong (coefficients past ±1, a zoom past ±0.5, a sensor shift over 5% of the frame) and any
|
||||
mapping that folds back on itself before the farthest corner.
|
||||
|
||||
The tests in `nef_lens` cover the polynomial and zoom, the sensor-shift center and refusing other bodies, layouts
|
||||
and crops. `nikon_in_file_distortion_becomes_a_warp` runs a synthetic Z 6II NEF through the header and full decode
|
||||
and a DNG export. `nikon_in_file_distortion_is_limited_to_validated_bodies` checks the corpus: the Z f sample gets the
|
||||
warp and the D5100, D7000, D7500 and Coolpix B700 samples don't.
|
||||
|
||||
None of my photos, crops or reference images are committed, only the numbers above.
|
||||
@@ -30,6 +30,7 @@
|
||||
//! | `erf` | Epson ERF (R-D1 family): 12-bit samples in 16-byte blocks |
|
||||
//! | `mef` | Mamiya MEF (ZD): uncompressed 12-bit packed |
|
||||
//! | `kodak` | Kodak DCS520C TIFF: two-component lossless JPEG with a response curve |
|
||||
//! | `nef` | Nikon NEF and NRW: uncompressed, packed, Nikon's Huffman coding (lossless and lossy, 12 and 14 bits), small raw |
|
||||
//! | `pef` | Pentax PEF: uncompressed (packed or 16-bit words) and Pentax's Huffman coding (compression 65535) |
|
||||
//! | `srw` | Samsung SRW: the uncompressed variants (compression 32769 words, 32770 at 12 bits packed) |
|
||||
//! | `hasselblad` | Hasselblad 3FR and FFF (and other CFA TIFFs in the DNG layout without a DNG version): uncompressed and Hasselblad's lossless JPEG; implies `dng` |
|
||||
@@ -76,6 +77,12 @@ pub mod ljpeg;
|
||||
mod mef;
|
||||
#[cfg(feature = "mrw")]
|
||||
mod mrw;
|
||||
#[cfg(feature = "nef")]
|
||||
mod nef;
|
||||
#[cfg(feature = "nef")]
|
||||
mod nef_lens;
|
||||
#[cfg(feature = "nef")]
|
||||
mod nefc;
|
||||
pub mod opcodes;
|
||||
#[cfg(feature = "pef")]
|
||||
mod pef;
|
||||
@@ -328,6 +335,7 @@ impl RawFormat {
|
||||
|| (self == RawFormat::Erf && cfg!(feature = "erf"))
|
||||
|| (self == RawFormat::Mef && cfg!(feature = "mef"))
|
||||
|| (self == RawFormat::Cr2 && cfg!(feature = "cr2"))
|
||||
|| (matches!(self, RawFormat::Nef | RawFormat::Nrw) && cfg!(feature = "nef"))
|
||||
|| (self == RawFormat::Pef && cfg!(feature = "pef"))
|
||||
|| (self == RawFormat::Srw && cfg!(feature = "srw"))
|
||||
}
|
||||
@@ -612,6 +620,8 @@ fn decode_in(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawImage> {
|
||||
RawFormat::Mef => mef::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "cr2")]
|
||||
RawFormat::Cr2 => cr2::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "nef")]
|
||||
RawFormat::Nef | RawFormat::Nrw => nef::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "pef")]
|
||||
RawFormat::Pef => pef::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "srw")]
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,259 @@
|
||||
//! Nikon's in-file distortion correction (raw SubIFD tag `0xc7d5`, NikonNEFInfo), expressed as a DNG warp for
|
||||
//! the shared optics pipeline.
|
||||
//!
|
||||
//! Tag identity: the ExifTool Nikon tag-name documentation (NikonNEFInfo `0xc7d5`; its DistortionInfo `0x0005`
|
||||
//! with DistortionCorrection and RadialDistortionCoefficient1..3). The model below (which stored value multiplies
|
||||
//! which power, the normalisation, the fill-frame zoom and the optical-centre offset in entry `0x0008`) was inferred
|
||||
//! independently by matching the raw against the camera's own corrected JPEG; see docs/nikon-lens-corrections.md.
|
||||
//! No lens database or external decoder code.
|
||||
|
||||
use crate::{Opcode, Rect};
|
||||
use craft_tiff::{ByteOrder, Ifd, ParseOptions, parse_ifd_at};
|
||||
|
||||
const NEF_INFO: u16 = 0xc7d5;
|
||||
const DISTORTION_INFO: u16 = 0x0005;
|
||||
const CENTRE_OFFSET: u16 = 0x0008;
|
||||
/// Bodies whose files were checked against the camera's JPEG (docs/nikon-lens-corrections.md). Matching tags on
|
||||
/// another body do not establish its pixel pitch or image area; add a body only after checking its files.
|
||||
const VALIDATED: &[&str] = &["NIKON Z 6_2", "NIKON Z 8", "NIKON Z f"];
|
||||
/// The normalisation radius every checked file stores: the FX half-diagonal in millimetres.
|
||||
const FX_HALF_DIAGONAL_MM: f64 = 21.6;
|
||||
const SAMPLES: usize = 256;
|
||||
|
||||
/// `r_src = r_out * (1 + k3 rho^2 + k2 rho^4 + k1 rho^6) / (1 + zoom)` around the optical centre, `rho` the radius
|
||||
/// over the half-diagonal of the image area (`k1..k3` as ExifTool names them, in the order they are stored).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct Distortion {
|
||||
k: [f64; 3],
|
||||
zoom: f64,
|
||||
}
|
||||
|
||||
/// A signed rational at `at` (`i32 / i32`), when it is one.
|
||||
fn rational(b: &[u8], at: usize, order: ByteOrder) -> Option<f64> {
|
||||
let n = order.u32(b.get(at..at + 4)?.try_into().ok()?) as i32;
|
||||
let d = order.u32(b.get(at + 4..at + 8)?.try_into().ok()?) as i32;
|
||||
(d > 0).then(|| f64::from(n) / f64::from(d)).filter(|v| v.is_finite())
|
||||
}
|
||||
|
||||
/// The NikonNEFInfo entries: the blob is `"Nikon\0"`, a 4-byte version, then a little TIFF stream.
|
||||
fn nef_info(raw: &Ifd) -> Option<(Ifd, ByteOrder)> {
|
||||
let tiff = raw.bytes(NEF_INFO)?.strip_prefix(b"Nikon\0")?.get(4..)?;
|
||||
let (order, false) = craft_tiff::Tiff::sniff(tiff)? else { return None };
|
||||
let first = order.u32(tiff.get(4..8)?.try_into().ok()?);
|
||||
let opts = ParseOptions { follow_children: false, ..Default::default() };
|
||||
let (ifd, _) = parse_ifd_at(tiff, u64::from(first), order, 0, false, &opts).ok()?;
|
||||
Some((ifd, order))
|
||||
}
|
||||
|
||||
/// DistortionInfo: `"0100"`, the correction mode (0 none), the normalisation radius (mm) at 8, the term count (4)
|
||||
/// at 16, the coefficients at 20 / 28 / 36 and the zoom at 64, each a signed rational.
|
||||
fn distortion_info(b: &[u8], order: ByteOrder) -> Option<Distortion> {
|
||||
if b.get(0..4)? != b"0100" || *b.get(4)? == 0 {
|
||||
return None;
|
||||
}
|
||||
let norm = rational(b, 8, order)?;
|
||||
let terms = order.u32(b.get(16..20)?.try_into().ok()?);
|
||||
if (norm - FX_HALF_DIAGONAL_MM).abs() > 0.05 || terms != 4 {
|
||||
return None;
|
||||
}
|
||||
let k = [rational(b, 20, order)?, rational(b, 28, order)?, rational(b, 36, order)?];
|
||||
let zoom = rational(b, 64, order)?;
|
||||
// real lenses stay far inside these (the checked files: |k| < 0.13, |zoom| < 0.09)
|
||||
(k.iter().all(|v| v.abs() < 1.0) && zoom.abs() < 0.5).then_some(Distortion { k, zoom })
|
||||
}
|
||||
|
||||
/// Entry `0x0008`: `"0100"`, then the optical centre's offset from the image-area centre in pixels (x right,
|
||||
/// y down), two signed rationals. Zero when the lens stabilises (the sensor stays centred).
|
||||
fn centre_offset(b: &[u8], order: ByteOrder) -> Option<(f64, f64)> {
|
||||
(b.get(0..4)? == b"0100").then_some(())?;
|
||||
Some((rational(b, 4, order)?, rational(b, 12, order)?))
|
||||
}
|
||||
|
||||
/// The correction of a `model` NEF whose raw IFD is `raw`, for the image area `crop` inside `active` (both in
|
||||
/// sensor pixels), as an `OpcodeList3` warp in active-area coordinates. `None` for unvalidated bodies, other
|
||||
/// image areas, missing or implausible data, and maps that fold.
|
||||
pub(super) fn distortion(model: &str, raw: &Ifd, active: Rect, crop: Rect) -> Option<Opcode> {
|
||||
if !VALIDATED.contains(&model.trim()) {
|
||||
return None;
|
||||
}
|
||||
let crop = crop.clipped(active.width, active.height);
|
||||
if active.width < 2 || active.height < 2 || crop.width < 2 || crop.height < 2 {
|
||||
return None;
|
||||
}
|
||||
// the stored radius is the FX half-diagonal: only the full 3:2 image area (not a DX or other crop of the
|
||||
// sensor) is normalised by it
|
||||
if (crop.width as f64 / crop.height as f64 - 1.5).abs() > 0.005 || (crop.width as f64) < active.width as f64 * 0.95 {
|
||||
return None;
|
||||
}
|
||||
let (info, order) = nef_info(raw)?;
|
||||
let d = distortion_info(info.bytes(DISTORTION_INFO)?, order)?;
|
||||
let (ox, oy) = info.bytes(CENTRE_OFFSET).and_then(|b| centre_offset(b, order)).unwrap_or((0.0, 0.0));
|
||||
let (cw, ch) = (crop.width as f64 - 1.0, crop.height as f64 - 1.0);
|
||||
if ox.abs() > cw * 0.05 || oy.abs() > ch * 0.05 {
|
||||
return None;
|
||||
}
|
||||
let half_diagonal = cw.hypot(ch) / 2.0;
|
||||
let poly = |rho: f64| {
|
||||
let x = rho * rho;
|
||||
1.0 + x * (d.k[2] + x * (d.k[1] + x * d.k[0]))
|
||||
};
|
||||
// DNG normalises by the farthest active-area corner from the centre; the image area reaches at most there
|
||||
let (ax, ay) = (active.width as f64 - 1.0, active.height as f64 - 1.0);
|
||||
let (cx, cy) = (crop.x as f64 + cw / 2.0 + ox, crop.y as f64 + ch / 2.0 + oy);
|
||||
let farthest = cx.max(ax - cx).hypot(cy.max(ay - cy));
|
||||
let a = farthest / half_diagonal;
|
||||
// the source radius must grow with the output radius out to the farthest corner, or the map folds
|
||||
let mut previous = -1.0;
|
||||
for j in 0..=SAMPLES {
|
||||
let rho = a * j as f64 / SAMPLES as f64;
|
||||
let source = rho * poly(rho) / (1.0 + d.zoom);
|
||||
if !source.is_finite() || source <= previous {
|
||||
return None;
|
||||
}
|
||||
previous = source;
|
||||
}
|
||||
let s = 1.0 / (1.0 + d.zoom);
|
||||
let a2 = a * a;
|
||||
let planes = vec![[s, d.k[2] * a2 * s, d.k[1] * a2 * a2 * s, d.k[0] * a2 * a2 * a2 * s, 0.0, 0.0]];
|
||||
Some(Opcode::WarpRectilinear { planes, center: [cx / ax, cy / ay] })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use craft_tiff::{Entry, IfdBuilder, TiffWriter, Value};
|
||||
|
||||
const AREA: Rect = Rect { x: 0, y: 0, width: 6064, height: 4040 };
|
||||
const CROP: Rect = Rect { x: 8, y: 8, width: 6048, height: 4024 };
|
||||
|
||||
fn rat(v: f64, den: i32) -> [u8; 8] {
|
||||
let mut b = [0u8; 8];
|
||||
b[..4].copy_from_slice(&((v * f64::from(den)).round() as i32).to_le_bytes());
|
||||
b[4..].copy_from_slice(&den.to_le_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
/// DistortionInfo as the checked files lay it out (84 bytes, little-endian).
|
||||
pub(crate) fn distortion_block(mode: u8, norm: f64, terms: u32, k: [f64; 3], zoom: f64) -> Vec<u8> {
|
||||
let mut b = vec![0u8; 84];
|
||||
b[..4].copy_from_slice(b"0100");
|
||||
b[4] = mode;
|
||||
b[5] = 1;
|
||||
b[8..16].copy_from_slice(&rat(norm, 4096));
|
||||
b[16..20].copy_from_slice(&terms.to_le_bytes());
|
||||
for (i, v) in k.iter().enumerate() {
|
||||
b[20 + 8 * i..28 + 8 * i].copy_from_slice(&rat(*v, 1 << 20));
|
||||
}
|
||||
b[44..52].copy_from_slice(&rat(0.0, 1));
|
||||
b[52..60].copy_from_slice(&rat(0.0, 1));
|
||||
b[64..72].copy_from_slice(&rat(zoom, 1 << 20));
|
||||
b
|
||||
}
|
||||
|
||||
pub(crate) fn offset_block(x: f64, y: f64) -> Vec<u8> {
|
||||
let mut b = b"0100".to_vec();
|
||||
b.extend_from_slice(&rat(x, 256));
|
||||
b.extend_from_slice(&rat(y, 256));
|
||||
b.extend_from_slice(&[0, 0, 0, 0]);
|
||||
b
|
||||
}
|
||||
|
||||
/// A NikonNEFInfo (`0xc7d5`) blob with the given entries.
|
||||
pub(crate) fn nef_info_blob(entries: &[(u16, Vec<u8>)]) -> Vec<u8> {
|
||||
let mut info = IfdBuilder::new();
|
||||
for (tag, data) in entries {
|
||||
info.set(*tag, Value::Undefined(data.clone()));
|
||||
}
|
||||
let tiff = TiffWriter::new(ByteOrder::Little, false).write(&[info]).unwrap();
|
||||
let mut blob = b"Nikon\0\x01\x03\0\0".to_vec();
|
||||
blob.extend_from_slice(&tiff);
|
||||
blob
|
||||
}
|
||||
|
||||
/// A raw IFD carrying a NikonNEFInfo blob with the given entries.
|
||||
pub(super) fn raw_with(entries: &[(u16, Vec<u8>)]) -> Ifd {
|
||||
Ifd { entries: vec![Entry { tag: NEF_INFO, value: Value::Undefined(nef_info_blob(entries)), offset: 0 }], ..Default::default() }
|
||||
}
|
||||
|
||||
// a Z 24-70 f/4 S at 24 mm and at 70 mm, as stored in two of the checked files
|
||||
const WIDE: ([f64; 3], f64) = ([0.039895, 0.019211, -0.127218], -0.025043);
|
||||
const TELE: ([f64; 3], f64) = ([0.01681, -0.010601, 0.056758], 0.088301);
|
||||
|
||||
fn warp(model: &str, raw: &Ifd) -> Option<([f64; 6], [f64; 2])> {
|
||||
match distortion(model, raw, AREA, CROP)? {
|
||||
Opcode::WarpRectilinear { planes, center } => Some((planes[0], center)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The source radius the warp gives for an output radius `rho` (in image-area half-diagonals) about the centre.
|
||||
fn source_rho(p: [f64; 6], centre: [f64; 2], rho: f64) -> f64 {
|
||||
let (ax, ay) = (AREA.width as f64 - 1.0, AREA.height as f64 - 1.0);
|
||||
let (cx, cy) = (centre[0] * ax, centre[1] * ay);
|
||||
let m = cx.max(ax - cx).hypot(cy.max(ay - cy));
|
||||
let hd = (CROP.width as f64 - 1.0).hypot(CROP.height as f64 - 1.0) / 2.0;
|
||||
let t2 = (rho * hd / m).powi(2);
|
||||
rho * (p[0] + t2 * (p[1] + t2 * (p[2] + t2 * p[3])))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_warp_is_the_stored_polynomial_over_the_zoom() {
|
||||
for (k, zoom) in [WIDE, TELE] {
|
||||
let raw = raw_with(&[(DISTORTION_INFO, distortion_block(3, 21.6, 4, k, zoom))]);
|
||||
let (p, c) = warp("NIKON Z 6_2", &raw).unwrap();
|
||||
for rho in [0.0, 0.25, 0.5, 0.75, 1.0] {
|
||||
let x = rho * rho;
|
||||
let want = rho * (1.0 + k[2] * x + k[1] * x * x + k[0] * x * x * x) / (1.0 + zoom);
|
||||
assert!((source_rho(p, c, rho) - want).abs() < 2e-6, "{k:?} at {rho}");
|
||||
}
|
||||
// centred on the image area, which sits 8 px in from the sensor's top-left
|
||||
assert!((c[0] - (8.0 + 6047.0 / 2.0) / 6063.0).abs() < 1e-12 && (c[1] - (8.0 + 4023.0 / 2.0) / 4039.0).abs() < 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_optical_centre_follows_the_sensor_shift() {
|
||||
let raw = raw_with(&[(DISTORTION_INFO, distortion_block(3, 21.6, 4, TELE.0, TELE.1)), (CENTRE_OFFSET, offset_block(-90.1, 18.59))]);
|
||||
let (_, c) = warp("NIKON Z 6_2", &raw).unwrap();
|
||||
assert!((c[0] * 6063.0 - (8.0 + 6047.0 / 2.0 - 90.1)).abs() < 0.01, "{c:?}");
|
||||
assert!((c[1] * 4039.0 - (8.0 + 4023.0 / 2.0 + 18.59)).abs() < 0.01, "{c:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_unvalidated_bodies_and_unknown_layouts() {
|
||||
let good = distortion_block(3, 21.6, 4, WIDE.0, WIDE.1);
|
||||
assert!(warp("NIKON Z 6_2", &raw_with(&[(DISTORTION_INFO, good.clone())])).is_some());
|
||||
assert!(warp("NIKON Z f", &raw_with(&[(DISTORTION_INFO, good.clone())])).is_some());
|
||||
assert!(warp("NIKON Z 8", &raw_with(&[(DISTORTION_INFO, good.clone())])).is_some());
|
||||
for model in ["", "NIKON Z 6", "NIKON Z 6_3", "NIKON Z 9", "NIKON D850", "NIKON Z 6_2 x"] {
|
||||
assert!(warp(model, &raw_with(&[(DISTORTION_INFO, good.clone())])).is_none(), "{model}");
|
||||
}
|
||||
// correction off, a DX radius, another term count, a wild coefficient or zoom, no NEFInfo at all
|
||||
for bad in [
|
||||
distortion_block(0, 21.6, 4, WIDE.0, WIDE.1),
|
||||
distortion_block(3, 14.2, 4, WIDE.0, WIDE.1),
|
||||
distortion_block(3, 21.6, 3, WIDE.0, WIDE.1),
|
||||
distortion_block(3, 21.6, 4, [0.0, 0.0, 1.5], 0.0),
|
||||
distortion_block(3, 21.6, 4, WIDE.0, 0.9),
|
||||
b"0200".iter().copied().chain([0u8; 80]).collect(),
|
||||
vec![0u8; 12],
|
||||
] {
|
||||
assert!(warp("NIKON Z 6_2", &raw_with(&[(DISTORTION_INFO, bad)])).is_none());
|
||||
}
|
||||
assert!(warp("NIKON Z 6_2", &Ifd::default()).is_none());
|
||||
// a map that folds (the source radius turns back) is refused
|
||||
assert!(warp("NIKON Z 6_2", &raw_with(&[(DISTORTION_INFO, distortion_block(3, 21.6, 4, [-0.9, 0.0, 0.0], 0.0))])).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_full_3_2_image_area_is_corrected() {
|
||||
let raw = raw_with(&[(DISTORTION_INFO, distortion_block(3, 21.6, 4, WIDE.0, WIDE.1))]);
|
||||
// a DX or 1:1 crop of the sensor would need another normalisation
|
||||
assert!(distortion("NIKON Z 6_2", &raw, AREA, Rect::new(1016, 676, 4032, 2688)).is_none(), "DX");
|
||||
assert!(distortion("NIKON Z 6_2", &raw, AREA, Rect::new(1016, 8, 4024, 4024)).is_none());
|
||||
assert!(distortion("NIKON Z 6_2", &raw, AREA, Rect::new(0, 0, 1, 1)).is_none());
|
||||
// a sensor-shift offset beyond 5 % of the frame is not trusted
|
||||
let far = raw_with(&[(DISTORTION_INFO, distortion_block(3, 21.6, 4, WIDE.0, WIDE.1)), (CENTRE_OFFSET, offset_block(900.0, 0.0))]);
|
||||
assert!(warp("NIKON Z 6_2", &far).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,794 @@
|
||||
//! Nikon Huffman-compressed NEF image data (TIFF compression 34713): "lossless compressed" and "lossy compressed"
|
||||
//! (type 1 and type 2) at 12 and 14 bits.
|
||||
//!
|
||||
//! **Clean-room.** No raw-decoder source code (dcraw, LibRaw, rawspeed, rawloader/rawler, darktable, RawTherapee,
|
||||
//! libopenraw, ExifTool's Perl code, …) was read or consulted. What we used:
|
||||
//!
|
||||
//! - Prose format descriptions: Laurent Clévy, "Nikon Electronic File (NEF) format" (<http://lclevy.free.fr/nef/>),
|
||||
//! the maker-note tag table and the "0x96 (linearization table) tag format" table (version bytes, four `u16`
|
||||
//! predictor seeds at offset 2, curve size at 10, curve at 12, split value at 562 for lossy type 2, "lossy type
|
||||
//! 2 reads an incomplete table and interpolation is required"); Bill Claff, "NEF Compression"
|
||||
//! (<https://www.photonstophotos.net/NikonInfo/NEF_Compression.htm>): a lossy encoding curve followed by a
|
||||
//! non-adaptive (fixed-table) Huffman stage, and the number of encoded values per camera (683, 769, 1025, 2753,
|
||||
//! 3073, 4097 …), which confirmed our curve interpolation; the libopenraw format notes
|
||||
//! (<https://libopenraw.freedesktop.org/formats/nef/>, prose only); ExifTool's Nikon tag-name documentation
|
||||
//! (`0x0093` NEFCompression, `0x0096` NEFLinearizationTable); ITU-T T.81 Annex F/H for the "difference category
|
||||
//! + additional bits" coding and canonical (BITS/HUFFVAL) Huffman codes.
|
||||
//! - Our own black-box analysis of CC0 samples from raw.pixls.us. Nikon's Huffman tables are not stored in the
|
||||
//! files and no prose source lists them, so they were recovered from the data: several cameras (D5100, D300,
|
||||
//! D700) were shot with the same scene in compressed and uncompressed modes; a beam search assigned code words
|
||||
//! to difference categories so that the decoded compressed image matched the uncompressed one (likelihood of
|
||||
//! the decoded values given the reference, minus the bits consumed, plus a description-length penalty per new
|
||||
//! code word). Each table was then verified on ~45 files from ~35 bodies (D40 … D850, Df, Z 6, Z 50, 1 J1): with
|
||||
//! the right table the whole strip decodes and ends within the last byte of the data, with every value inside
|
||||
//! the encoded range. The predictor and difference coding are the same as Pentax PEF (see `pef.rs`).
|
||||
//!
|
||||
//! Format, as established:
|
||||
//! - One strip, MSB-first bit stream, no byte stuffing, continuous across rows. Per pixel a canonical Huffman code
|
||||
//! gives the category `n`, followed by `n` additional bits (T.81: a leading 0 means a negative difference).
|
||||
//! - Each pixel is predicted from the same-colour pixel two to the left; the first two pixels of a row from the
|
||||
//! first two pixels of the previous row of the same parity, starting at the seeds of maker note `0x0096`.
|
||||
//! - Lossless (`0x0096` version `0x46`): the decoded values are the samples. Lossy (version `0x44`): they index a
|
||||
//! curve. Type 1 (`0x44 0x10`) stores the full curve; type 2 (`0x44 0x20`, `0x44 0x40`) stores 257 points,
|
||||
//! one every `2^bits / 256` codes (`0x20`) or `2^bits / 1024` codes (`0x40`), linearly interpolated. The span
|
||||
//! was established from the files: in every `0x20` file the first interval of the 257 points rises by
|
||||
//! `2^bits / 256` (16 at 12 bit, 64 at 14 bit) and in every `0x40` file by `2^bits / 1024` (4 and 16), i.e.
|
||||
//! the curve is the identity in the dark part, one code per sample. Read with a point every `2^bits / 256`
|
||||
//! codes instead, the 12 `0x40` files that are not split decoded to values that never exceeded 255 (12 bit) or
|
||||
//! 1023 (14 bit) and whose darkest 0.01 % sat at a quarter of the black level of maker note `0x003d` (63 against
|
||||
//! 252 in 12-bit units); with the first-interval step they sit at that black level.
|
||||
//! - Values in the maker note are in the maker note's byte order (newer bodies write little-endian notes).
|
||||
//!
|
||||
//! "Lossy after split" (non-zero split row at `0x0096` offset 562, type 2 tables only, found in 11 of the CC0
|
||||
//! samples): derived by black-box analysis of those files. Rows below the split row use the lossy Huffman path
|
||||
//! above unchanged. Row `split` starts at the very next bit (no alignment); from there every pixel is a word of
|
||||
//! `bits - 4` bits (8 at 12 bit, 10 at 14 bit), MSB first, with no row padding. A word is a complete prefix code
|
||||
//! for a difference category `n` followed by the top `W - prefix length` bits of the T.81 additional bits (the low
|
||||
//! bits are dropped); the difference is the centre of the magnitude bin that was kept. The predictor is the same
|
||||
//! as above and the running value is not clamped (only the sample index is, before the curve lookup). The camera
|
||||
//! ends the strip with 0 to 7 one-bits after the last full row, which the decoder checks.
|
||||
//!
|
||||
//! Not supported (returned as [`RawError::Unsupported`], the embedded preview is used instead):
|
||||
//! - "Lossy after split" files whose fixed-rate section does not end within the last byte of the strip (see
|
||||
//! below): refused rather than decoded wrongly.
|
||||
//! - Other `0x0096` versions, bit depths other than 12/14.
|
||||
//! - Code words that never occurred in any sample (the lossy 12-bit 8-bit code word `11111110`, and code words
|
||||
//! longer than 9 bits in the lossy tables) are rejected as corrupt; they cannot code a difference within the
|
||||
//! lossy value range anyway.
|
||||
|
||||
use crate::huffman::{Bits, Huffman, diff};
|
||||
use crate::{MAX_SAMPLES, RawError, Result};
|
||||
use craft_tiff::ByteOrder;
|
||||
|
||||
/// A table code word whose symbol was never observed (see the module docs).
|
||||
const UNSEEN: u8 = u8::MAX;
|
||||
|
||||
/// Canonical Huffman table: number of codes of length 1..=16 and the symbols (difference categories) in code order.
|
||||
struct Table {
|
||||
counts: &'static [u8],
|
||||
symbols: &'static [u8],
|
||||
}
|
||||
|
||||
const LOSSLESS_12: Table = Table { counts: &[0, 1, 4, 2, 3, 1, 2], symbols: &[5, 4, 6, 3, 7, 2, 8, 1, 9, 0, 10, 11, 12] };
|
||||
const LOSSLESS_14: Table = Table { counts: &[0, 1, 4, 2, 2, 3, 1, 2], symbols: &[7, 6, 8, 5, 9, 4, 10, 3, 11, 12, 2, 0, 1, 13, 14] };
|
||||
const LOSSY_12: Table = Table { counts: &[0, 1, 5, 1, 1, 1, 1, 1, 1], symbols: &[5, 4, 3, 6, 2, 7, 1, 0, 8, 9, UNSEEN, 10] };
|
||||
const LOSSY_14: Table = Table { counts: &[0, 1, 4, 3, 1, 1, 1, 1, 1], symbols: &[5, 6, 4, 7, 8, 3, 9, 2, 1, 0, 10, 11, 12] };
|
||||
|
||||
impl Table {
|
||||
/// `(left-aligned 12-bit code, length)` per symbol, for [`Huffman::new`].
|
||||
fn codes(&self) -> Vec<(u16, u8)> {
|
||||
let mut out = vec![(0u16, 0u8); 17];
|
||||
let mut code = 0u32;
|
||||
let mut symbols = self.symbols.iter();
|
||||
for (i, &n) in self.counts.iter().enumerate() {
|
||||
let len = i as u32 + 1;
|
||||
for _ in 0..n {
|
||||
if let Some(&s) = symbols.next()
|
||||
&& let Some(slot) = out.get_mut(s as usize)
|
||||
{
|
||||
*slot = ((code << (12 - len)) as u16, len as u8);
|
||||
}
|
||||
code += 1;
|
||||
}
|
||||
code <<= 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// How the decoded values map to samples.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum Encoding {
|
||||
/// The decoded values are the samples (`0..2^bits`).
|
||||
Lossless,
|
||||
/// The decoded values index this curve.
|
||||
Lossy(Vec<u16>),
|
||||
}
|
||||
|
||||
/// Maker note `0x0096` (NEF linearization table), parsed.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct DecodeTable {
|
||||
pub encoding: Encoding,
|
||||
/// Predictor seeds `[row parity][column]`.
|
||||
pub seeds: [[i32; 2]; 2],
|
||||
/// First row of the fixed-rate section ("lossy after split"), 0 when the whole strip is Huffman coded.
|
||||
pub split: usize,
|
||||
}
|
||||
|
||||
/// Parse maker note `0x0096` for a `bits`-bit image. `order` is the maker note's byte order.
|
||||
pub(crate) fn parse_table(t: &[u8], order: ByteOrder, bits: u32) -> Result<DecodeTable> {
|
||||
let u16_at = |o: usize| t.get(o..o + 2).and_then(|s| <[u8; 2]>::try_from(s).ok()).map(|b| order.u16(b));
|
||||
let short = || RawError::Corrupt(format!("NEF: linearization table too short ({} bytes)", t.len()));
|
||||
if bits != 12 && bits != 14 {
|
||||
return Err(RawError::Unsupported(format!("Nikon compressed NEF with {bits}-bit samples")));
|
||||
}
|
||||
let (v0, v1) = (t.first().copied().ok_or_else(short)?, t.get(1).copied().ok_or_else(short)?);
|
||||
let mut seeds = [[0i32; 2]; 2];
|
||||
for (i, s) in seeds.iter_mut().flatten().enumerate() {
|
||||
*s = u16_at(2 + 2 * i).ok_or_else(short)? as i32;
|
||||
}
|
||||
let mut split = 0;
|
||||
let encoding = match (v0, v1) {
|
||||
(0x46, _) => Encoding::Lossless,
|
||||
(0x44, 0x10 | 0x20 | 0x40) => {
|
||||
let n = u16_at(10).ok_or_else(short)? as usize;
|
||||
let points: Vec<u16> = (0..n).map(|i| u16_at(12 + 2 * i)).collect::<Option<_>>().ok_or_else(short)?;
|
||||
if n < 2 {
|
||||
return Err(RawError::Corrupt(format!("NEF: linearization curve of {n} points")));
|
||||
}
|
||||
if v1 == 0x10 {
|
||||
Encoding::Lossy(points)
|
||||
} else {
|
||||
split = u16_at(562).unwrap_or(0) as usize;
|
||||
// type 2: `n` points covering `range` codes, one every `range / (n - 1)`; the curve's first interval
|
||||
// rises by exactly that step (identity in the dark part) and the table version sets the span:
|
||||
// `0x20` covers all 2^bits codes, `0x40` the first quarter (see the module docs)
|
||||
let range = if v1 == 0x40 { 1usize << (bits - 2) } else { 1usize << bits };
|
||||
if n - 1 > range || !range.is_multiple_of(n - 1) {
|
||||
return Err(RawError::Unsupported(format!("NEF: {n}-point lossy curve for {bits}-bit data")));
|
||||
}
|
||||
let step = range / (n - 1);
|
||||
let mut curve = Vec::with_capacity(range + 1);
|
||||
for pair in points.windows(2) {
|
||||
let &[a, b] = pair else { continue };
|
||||
let (a, b) = (a as u32, b as u32);
|
||||
for k in 0..step as u32 {
|
||||
let s = step as u32;
|
||||
curve.push(((a * (s - k) + b * k) / s) as u16);
|
||||
}
|
||||
}
|
||||
curve.extend(points.last().copied());
|
||||
Encoding::Lossy(curve)
|
||||
}
|
||||
}
|
||||
_ => return Err(RawError::Unsupported(format!("Nikon compressed NEF version {v0:#04x} {v1:#04x}"))),
|
||||
};
|
||||
Ok(DecodeTable { encoding, seeds, split })
|
||||
}
|
||||
|
||||
/// Difference-category prefix codes of the fixed-rate section: `(prefix bits, prefix length, category n)`.
|
||||
/// A word of `W` bits is a prefix followed by `W - length` bits of the T.81 additional bits of category `n`.
|
||||
/// (The 14-bit `010` = category 12 never occurs in the samples; its category is deduced.)
|
||||
const FIXED_12: &[(u32, u32, u32)] = &[
|
||||
(0b00, 2, 9),
|
||||
(0b010, 3, 10),
|
||||
(0b011, 3, 8),
|
||||
(0b100, 3, 7),
|
||||
(0b101, 3, 6),
|
||||
(0b110, 3, 5),
|
||||
(0b1110, 4, 4),
|
||||
(0b11110, 5, 3),
|
||||
(0b111110, 6, 2),
|
||||
(0b1111110, 7, 1),
|
||||
(0b11111110, 8, 0),
|
||||
];
|
||||
const FIXED_14: &[(u32, u32, u32)] = &[
|
||||
(0b00, 2, 8),
|
||||
(0b010, 3, 12),
|
||||
(0b011, 3, 11),
|
||||
(0b100, 3, 10),
|
||||
(0b101, 3, 9),
|
||||
(0b110, 3, 7),
|
||||
(0b1110, 4, 6),
|
||||
(0b11110, 5, 5),
|
||||
(0b111110, 6, 4),
|
||||
(0b1111110, 7, 3),
|
||||
(0b11111110, 8, 2),
|
||||
(0b111111110, 9, 1),
|
||||
(0b1111111110, 10, 0),
|
||||
];
|
||||
|
||||
/// Word (`bits - 4` bits) to dequantised difference for the fixed-rate section; `None` marks the unused all-ones word.
|
||||
fn fixed_word_table(bits: u32) -> Vec<Option<i32>> {
|
||||
let (w, codes) = if bits == 12 { (8, FIXED_12) } else { (10, FIXED_14) };
|
||||
let mut lut = vec![None; 1usize << w];
|
||||
for &(prefix, len, n) in codes {
|
||||
let kept = w - len;
|
||||
for m in 0..1u32 << kept {
|
||||
let e = n.saturating_sub(kept);
|
||||
let ex = (m << e) as i32;
|
||||
let half = (1i32 << e) >> 1;
|
||||
let d = if n == 0 {
|
||||
0
|
||||
} else if ex >= 1 << (n - 1) {
|
||||
ex + half
|
||||
} else if e > 0 {
|
||||
ex + half - (1 << n)
|
||||
} else {
|
||||
ex - ((1 << n) - 1)
|
||||
};
|
||||
if let Some(slot) = lut.get_mut(((prefix << kept) | m) as usize) {
|
||||
*slot = Some(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
lut
|
||||
}
|
||||
|
||||
/// Decode a whole `w × h` Nikon Huffman-compressed strip `src` with `bits`-bit samples.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn decode(src: &[u8], w: usize, h: usize, bits: u32, table: &DecodeTable) -> Result<Vec<u16>> {
|
||||
decode_rows(src, w, h, h, bits, table)
|
||||
}
|
||||
|
||||
/// The first `rows` rows (`w × rows` samples) of a `w × h` strip. The stream is sequential, so a header-only probe
|
||||
/// decodes just the rows its masked-column test looks at (storytold/lightcraft#708) instead of the whole image. The size and
|
||||
/// truncation checks still cover the whole image; the "lossy after split" end-of-strip rule (and corruption
|
||||
/// further down) can only be seen when the whole strip is decoded.
|
||||
pub(crate) fn decode_rows(src: &[u8], w: usize, h: usize, rows: usize, bits: u32, table: &DecodeTable) -> Result<Vec<u16>> {
|
||||
if bits != 12 && bits != 14 {
|
||||
return Err(RawError::Unsupported(format!("Nikon compressed NEF with {bits}-bit samples")));
|
||||
}
|
||||
let whole = w.checked_mul(h).filter(|&n| n > 0 && n <= MAX_SAMPLES).ok_or(RawError::Limit("NEF image size"))?;
|
||||
// every code word is at least 2 bits long: a strip that can't hold the image is truncated (also bounds the
|
||||
// allocation by the file size)
|
||||
if whole / 4 > src.len() {
|
||||
return Err(RawError::Corrupt(format!("NEF: {} bytes of compressed data for {w}x{h} pixels", src.len())));
|
||||
}
|
||||
let rows = rows.min(h);
|
||||
let n = w.saturating_mul(rows);
|
||||
let huff = Huffman::new(
|
||||
&match (&table.encoding, bits) {
|
||||
(Encoding::Lossless, 12) => LOSSLESS_12,
|
||||
(Encoding::Lossless, _) => LOSSLESS_14,
|
||||
(Encoding::Lossy(_), 12) => LOSSY_12,
|
||||
(Encoding::Lossy(_), _) => LOSSY_14,
|
||||
}
|
||||
.codes(),
|
||||
)?;
|
||||
let (lut, max): (&[u16], i32) = match &table.encoding {
|
||||
Encoding::Lossless => (&[], (1i32 << bits) - 1),
|
||||
Encoding::Lossy(curve) => (curve, curve.len() as i32 - 1),
|
||||
};
|
||||
if table.split != 0 && table.split >= h {
|
||||
return Err(RawError::Corrupt(format!("NEF: split row {} outside the {h} rows", table.split)));
|
||||
}
|
||||
let fixed = if table.split != 0 { fixed_word_table(bits) } else { Vec::new() };
|
||||
let word = bits - 4;
|
||||
let mut out = vec![0u16; n];
|
||||
let mut stream = Bits::new(src);
|
||||
let mut vpred = table.seeds;
|
||||
let mut clipped = 0usize;
|
||||
for (y, row) in out.chunks_exact_mut(w).enumerate() {
|
||||
let seeds = vpred.get_mut(y & 1).ok_or(RawError::Limit("NEF row"))?;
|
||||
let mut hpred = [0i32; 2];
|
||||
for (x, o) in row.iter_mut().enumerate() {
|
||||
let d = if table.split != 0 && y >= table.split {
|
||||
let code = stream.get(word) as usize;
|
||||
fixed.get(code).copied().flatten().ok_or_else(|| RawError::Corrupt(format!("NEF: invalid fixed-rate word at row {y}")))?
|
||||
} else {
|
||||
diff(&mut stream, &huff).ok_or_else(|| RawError::Corrupt(format!("NEF: invalid Huffman code at row {y}")))?
|
||||
};
|
||||
// a run of large differences in a corrupt stream can drive a predictor past i32 (storytold/lightcraft#1515)
|
||||
let overflow = || RawError::Corrupt(format!("NEF: predictor out of range at row {y}"));
|
||||
let p = if x < 2 {
|
||||
let s = seeds.get_mut(x).ok_or(RawError::Limit("NEF column"))?;
|
||||
*s = s.checked_add(d).ok_or_else(overflow)?;
|
||||
hpred[x & 1] = *s;
|
||||
*s
|
||||
} else {
|
||||
let p = hpred[x & 1].checked_add(d).ok_or_else(overflow)?;
|
||||
hpred[x & 1] = p;
|
||||
p
|
||||
};
|
||||
let v = p.clamp(0, max);
|
||||
clipped += (v != p && (table.split == 0 || y < table.split)) as usize;
|
||||
*o = if lut.is_empty() { v as u16 } else { lut.get(v as usize).copied().unwrap_or(0) };
|
||||
}
|
||||
// the camera pads the strip; reading more than a few bytes past its end means truncated or corrupt data
|
||||
if stream.consumed_bits() > src.len() * 8 + 64 {
|
||||
return Err(RawError::Corrupt(format!("NEF: compressed data ends at row {y} of {h}")));
|
||||
}
|
||||
}
|
||||
if table.split != 0 && rows == h {
|
||||
// the fixed-rate section must fill the rest of the strip: whole rows of w words and 0..7 bits of padding.
|
||||
// A strip may hold a few more rows than the image (one body stores 4022 rows for 4020); anything else means
|
||||
// the file does not follow the rule, so refuse it instead of decoding it wrongly.
|
||||
let left = (src.len() * 8).checked_sub(stream.consumed_bits());
|
||||
let row_bits = w * word as usize;
|
||||
if !left.is_some_and(|l| l % row_bits < 8 && l / row_bits <= 4) {
|
||||
return Err(RawError::Unsupported(format!(
|
||||
"Nikon \"lossy after split\" NEF whose fixed-rate section (from row {}) does not end within the last byte",
|
||||
table.split
|
||||
)));
|
||||
}
|
||||
}
|
||||
// a wrong table / corrupt stream drifts out of range quickly; real files stay inside (a handful of edge pixels)
|
||||
if clipped > n / 1000 + 16 {
|
||||
return Err(RawError::Corrupt(format!("NEF: {clipped} decoded values out of range")));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// MSB-first bit writer for the test encoder.
|
||||
#[derive(Default)]
|
||||
struct Writer {
|
||||
out: Vec<u8>,
|
||||
acc: u64,
|
||||
n: u32,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
fn put(&mut self, v: u32, len: u32) {
|
||||
for i in (0..len).rev() {
|
||||
self.acc = (self.acc << 1) | ((v >> i) & 1) as u64;
|
||||
self.n += 1;
|
||||
if self.n == 8 {
|
||||
self.out.push(self.acc as u8);
|
||||
self.acc = 0;
|
||||
self.n = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn finish(mut self) -> Vec<u8> {
|
||||
if self.n > 0 {
|
||||
self.out.push((self.acc << (8 - self.n)) as u8);
|
||||
}
|
||||
self.out.extend_from_slice(&[0; 4]);
|
||||
self.out
|
||||
}
|
||||
}
|
||||
|
||||
fn category(d: i32) -> u32 {
|
||||
32 - d.unsigned_abs().leading_zeros()
|
||||
}
|
||||
|
||||
/// Encode `values` (decoded domain: samples or curve indices) the way the camera does.
|
||||
fn encode(values: &[i32], w: usize, table: &Table, seeds: [[i32; 2]; 2]) -> Vec<u8> {
|
||||
let codes = table.codes();
|
||||
let mut wr = Writer::default();
|
||||
let mut vpred = seeds;
|
||||
for (y, row) in values.chunks(w).enumerate() {
|
||||
for (x, &v) in row.iter().enumerate() {
|
||||
let pred = if x < 2 { vpred[y & 1][x] } else { row[x - 2] };
|
||||
if x < 2 {
|
||||
vpred[y & 1][x] = v;
|
||||
}
|
||||
let d = v - pred;
|
||||
let k = category(d);
|
||||
let (code, len) = codes[k as usize];
|
||||
assert!(len > 0, "no code for category {k}");
|
||||
wr.put(code as u32 >> (12 - len), len as u32);
|
||||
let extra = if d < 0 { d + (1 << k) - 1 } else { d };
|
||||
wr.put(extra as u32, k);
|
||||
}
|
||||
}
|
||||
wr.finish()
|
||||
}
|
||||
|
||||
/// Deterministic test image: smooth gradient + texture + hard edges, within `0..=max`.
|
||||
fn image(w: usize, h: usize, max: i32, seed: u32) -> Vec<i32> {
|
||||
let mut s = seed;
|
||||
(0..w * h)
|
||||
.map(|i| {
|
||||
let (x, y) = ((i % w) as i32, (i / w) as i32);
|
||||
s = s.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
|
||||
let noise = (s >> 24) as i32 % 9 - 4;
|
||||
let base = (x * 37 + y * 11) % (max / 2) + if (x / 7 + y / 5) % 3 == 0 { max / 3 } else { 0 };
|
||||
(base + noise).clamp(0, max)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn lossless(bits: u32) -> DecodeTable {
|
||||
let s = 1 << (bits - 3);
|
||||
DecodeTable { encoding: Encoding::Lossless, seeds: [[s, s], [s, s]], split: 0 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tables_are_complete_prefix_codes() {
|
||||
for t in [LOSSLESS_12, LOSSLESS_14, LOSSY_12, LOSSY_14] {
|
||||
assert_eq!(t.counts.iter().map(|&c| c as usize).sum::<usize>(), t.symbols.len());
|
||||
let kraft: f64 = t.counts.iter().enumerate().map(|(i, &c)| c as f64 / (1u64 << (i + 1)) as f64).sum();
|
||||
assert!(kraft <= 1.0);
|
||||
assert!(Huffman::new(&t.codes()).is_ok());
|
||||
}
|
||||
assert_eq!(LOSSLESS_14.codes()[7], (0b00 << 10, 2));
|
||||
assert_eq!(LOSSLESS_14.codes()[0], (0b111110 << 6, 6));
|
||||
assert_eq!(LOSSY_12.codes()[10], (0b111111110 << 3, 9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossless_roundtrip_12_and_14() {
|
||||
for (bits, table) in [(12, LOSSLESS_12), (14, LOSSLESS_14)] {
|
||||
let (w, h) = (64, 9);
|
||||
let max = (1 << bits) - 1;
|
||||
let mut img = image(w, h, max, bits);
|
||||
// extreme jumps exercise the longest categories
|
||||
img[5] = 0;
|
||||
img[7] = max;
|
||||
img[w + 9] = max;
|
||||
let t = lossless(bits);
|
||||
let src = encode(&img, w, &table, t.seeds);
|
||||
let out = decode(&src, w, h, bits, &t).unwrap();
|
||||
assert_eq!(out, img.iter().map(|&v| v as u16).collect::<Vec<_>>(), "{bits}-bit");
|
||||
}
|
||||
}
|
||||
|
||||
fn lossy_table(bits: u32, order: ByteOrder) -> Vec<u8> {
|
||||
// version 0x44 0x20, seeds, 257 points of a concave curve, padding up to the split value (0)
|
||||
let mut t = vec![0x44, 0x20];
|
||||
let put = |t: &mut Vec<u8>, v: u16| t.extend_from_slice(&if order == ByteOrder::Big { v.to_be_bytes() } else { v.to_le_bytes() });
|
||||
for _ in 0..4 {
|
||||
put(&mut t, 300);
|
||||
}
|
||||
put(&mut t, 257);
|
||||
let max = ((1u32 << bits) - 1) as f64;
|
||||
for i in 0..257u32 {
|
||||
put(&mut t, (max * (i as f64 / 64.0).min(1.0).sqrt()) as u16);
|
||||
}
|
||||
t.resize(624, 0);
|
||||
t
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossy_type2_curve_and_roundtrip() {
|
||||
for (bits, table) in [(12u32, LOSSY_12), (14, LOSSY_14)] {
|
||||
for order in [ByteOrder::Big, ByteOrder::Little] {
|
||||
let t = parse_table(&lossy_table(bits, order), order, bits).unwrap();
|
||||
assert_eq!(t.seeds, [[300, 300], [300, 300]]);
|
||||
let Encoding::Lossy(curve) = &t.encoding else { panic!() };
|
||||
let step = (1usize << bits) / 256;
|
||||
assert_eq!(curve.len(), (1 << bits) + 1);
|
||||
assert_eq!(curve[64 * step], (1 << bits) - 1);
|
||||
assert!(curve.windows(2).all(|p| p[0] <= p[1]));
|
||||
// the encoded range is the curve's rising part
|
||||
let (w, h) = (48, 7);
|
||||
let img = image(w, h, 64 * step as i32 - 1, 7);
|
||||
let src = encode(&img, w, &table, t.seeds);
|
||||
let out = decode(&src, w, h, bits, &t).unwrap();
|
||||
assert_eq!(out, img.iter().map(|&v| curve[v as usize]).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maker note `0x0096` as bytes, written out by hand: version `0x44 <v1>`, four seeds, 257 points (little-endian
|
||||
/// as newer bodies write it) of a curve whose first 256 points rise by `step` and whose last is the white
|
||||
/// level, and a zero split value at offset 562.
|
||||
fn hand_table(v1: u8, seed: u16, step: u16, white: u16) -> Vec<u8> {
|
||||
let mut t = vec![0x44, v1];
|
||||
for _ in 0..4 {
|
||||
t.extend_from_slice(&seed.to_le_bytes());
|
||||
}
|
||||
t.extend_from_slice(&257u16.to_le_bytes());
|
||||
for k in 0..256u16 {
|
||||
t.extend_from_slice(&(k * step).to_le_bytes());
|
||||
}
|
||||
t.extend_from_slice(&white.to_le_bytes());
|
||||
t.resize(624, 0);
|
||||
t
|
||||
}
|
||||
|
||||
/// Known answers for the `0x44 0x40` curve, from bytes and bit strings written by hand (nothing here uses the
|
||||
/// decoder's span rule or the test encoder): the first interval of the points rises by `2^bits / 1024`, so
|
||||
/// the curve is the identity for one code per sample and spans `2^bits / 4` codes. The `0x20` curve of the
|
||||
/// same sizes keeps its span of all `2^bits` codes.
|
||||
#[test]
|
||||
fn lossy_type2_version_0x40_spans_a_quarter_of_the_range() {
|
||||
// 12 bit: points 0, 4, 8 … 1020, 4095
|
||||
let t = parse_table(&hand_table(0x40, 252, 4, 4095), ByteOrder::Little, 12).unwrap();
|
||||
let Encoding::Lossy(curve) = &t.encoding else { panic!() };
|
||||
assert_eq!(curve.len(), 1025);
|
||||
assert_eq!(curve[0..6], [0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!((curve[252], curve[255], curve[1020]), (252, 255, 1020));
|
||||
assert_eq!((curve[1021], curve[1022], curve[1023], curve[1024]), (1788, 2557, 3326, 4095));
|
||||
// the pixels p0 = 252 + 4, p1 = 252, p2 = p0 - 1, p3 = p1, coded as 011 100 | 11110 | 1110 0 | 11110
|
||||
// (LOSSY_12: category 3 = 011, 0 = 11110, 1 = 1110; extra bits 100 = +4, 0 = -1)
|
||||
let out = decode(&[0x73, 0xdc, 0xf0], 4, 1, 12, &t).unwrap();
|
||||
assert_eq!(out, [256, 252, 255, 252]);
|
||||
// 14 bit: points 0, 16, 32 … 4080, 16383
|
||||
let t = parse_table(&hand_table(0x40, 1008, 16, 16383), ByteOrder::Little, 14).unwrap();
|
||||
let Encoding::Lossy(curve) = &t.encoding else { panic!() };
|
||||
assert_eq!(curve.len(), 4097);
|
||||
assert_eq!(curve[0..6], [0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!((curve[1008], curve[4080], curve[4096]), (1008, 4080, 16383));
|
||||
// the same pixels coded as 1100 100 | 111110 | 11110 0 | 111110
|
||||
// (LOSSY_14: category 3 = 1100, 0 = 111110, 1 = 11110)
|
||||
let out = decode(&[0xc9, 0xf7, 0x9f, 0x00], 4, 1, 14, &t).unwrap();
|
||||
assert_eq!(out, [1012, 1008, 1011, 1008]);
|
||||
// version 0x20 with the same points is a curve over all 2^bits codes: the points are 16 codes apart
|
||||
let t = parse_table(&hand_table(0x20, 252, 16, 4095), ByteOrder::Little, 12).unwrap();
|
||||
let Encoding::Lossy(curve) = &t.encoding else { panic!() };
|
||||
assert_eq!((curve.len(), curve[16], curve[17], curve[4095]), (4097, 16, 17, 4094));
|
||||
// 200 points can't be spaced evenly over the 1024 codes of a 12-bit 0x40 table
|
||||
let mut odd = hand_table(0x40, 252, 4, 4095);
|
||||
odd[10..12].copy_from_slice(&200u16.to_le_bytes());
|
||||
assert!(parse_table(&odd, ByteOrder::Little, 12).is_err());
|
||||
}
|
||||
|
||||
/// Pack a string of `0`/`1` (spaces ignored) MSB first, padding the last byte with one-bits like the camera.
|
||||
fn pack(bits: &str) -> Vec<u8> {
|
||||
let b: Vec<bool> = bits.chars().filter(|c| !c.is_whitespace()).map(|c| c == '1').collect();
|
||||
b.chunks(8).map(|c| (0..8).fold(0u8, |a, i| (a << 1) | c.get(i).copied().unwrap_or(true) as u8)).collect()
|
||||
}
|
||||
|
||||
fn split_table(v1: u8, seed: u16, step: u16, white: u16, split: u16) -> DecodeTable {
|
||||
let mut t = hand_table(v1, seed, step, white);
|
||||
t[562..564].copy_from_slice(&split.to_le_bytes());
|
||||
parse_table(&t, ByteOrder::Little, if step == 4 { 12 } else { 14 }).unwrap()
|
||||
}
|
||||
|
||||
/// 12-bit "lossy after split": row 0 Huffman coded, rows 1 and 2 as 8-bit words that follow the last Huffman
|
||||
/// bit directly. Bit strings written by hand from the rule (prefix, then the kept top bits of the extra bits).
|
||||
const SPLIT_12: &str = "011 100 11110 1110 0 11110 11111110 11110100 11100111 01110000 00000001 11111110 01111111 11111110";
|
||||
|
||||
#[test]
|
||||
fn lossy_after_split_known_answers_12_bit() {
|
||||
let t = split_table(0x40, 252, 4, 4095, 1);
|
||||
assert_eq!(t.split, 1);
|
||||
let out = decode(&pack(SPLIT_12), 4, 3, 12, &t).unwrap();
|
||||
// row 1: d = 0 (n0), +4 (n3, exact), -8 (n4, exact), +132 (n8 keeps 5 bits: 16 << 3 plus half a bin, 4);
|
||||
// row 2: -500 (n9 keeps 6 bits: bin 1 of the negative side, centre -508 + 8), then +252 on the running
|
||||
// value -244 which is not clamped (8, not 252)
|
||||
assert_eq!(out, [256, 252, 255, 252, 252, 256, 244, 388, 0, 252, 8, 252]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossy_after_split_known_answers_14_bit() {
|
||||
let t = split_table(0x40, 1008, 16, 16383, 1);
|
||||
// row 0 as in the 0x40 test above; row 1 in 10-bit words: 0 (n0), +32 (n6, exact), -254 (n8, exact),
|
||||
// +510 (n9 keeps 7 bits: 127 << 2 plus 2)
|
||||
let bits = "1100 100 111110 11110 0 111110 1111111110 1110100000 0000000001 1011111111";
|
||||
let out = decode(&pack(bits), 4, 2, 14, &t);
|
||||
// word 3 is `00` + 8 bits: 00 00000001 -> n8 extra 00000001 = -254
|
||||
assert_eq!(out.unwrap(), [1012, 1008, 1011, 1008, 1008, 1040, 754, 1550]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossy_after_split_dequantisation_is_symmetric() {
|
||||
let lut = fixed_word_table(12);
|
||||
assert_eq!((lut[0b00_000000], lut[0b00_111111]), (Some(-508), Some(508)));
|
||||
assert_eq!((lut[0b011_00000], lut[0b011_11111]), (Some(-252), Some(252)));
|
||||
assert_eq!(lut[0b11111110], Some(0));
|
||||
assert_eq!(lut[0xff], None);
|
||||
let lut = fixed_word_table(14);
|
||||
assert_eq!((lut[0b00_00000000], lut[0b00_11111111]), (Some(-255), Some(255)));
|
||||
assert_eq!(lut[0b101_0000000], Some(-510));
|
||||
assert_eq!(lut[0b1111111110], Some(0));
|
||||
assert_eq!(lut[0x3ff], None);
|
||||
for bits in [12, 14] {
|
||||
let lut = fixed_word_table(bits);
|
||||
assert_eq!(lut.iter().filter(|d| d.is_none()).count(), 1, "{bits}-bit: only the all-ones word is unused");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossy_after_split_refuses_strips_that_do_not_follow_the_rule() {
|
||||
let t = split_table(0x40, 252, 4, 4095, 1);
|
||||
let mut longer = pack(SPLIT_12);
|
||||
longer.extend_from_slice(&[0xff; 3]);
|
||||
assert!(matches!(decode(&longer, 4, 3, 12, &t), Err(RawError::Unsupported(_))));
|
||||
let shorter = &pack(SPLIT_12)[..10];
|
||||
assert!(decode(shorter, 4, 3, 12, &t).is_err());
|
||||
// the unused all-ones word
|
||||
let bad = pack("011 100 11110 1110 0 11110 11111111 11111110 11111110 11111110");
|
||||
assert!(matches!(decode(&bad, 4, 2, 12, &t), Err(RawError::Corrupt(_))));
|
||||
// a split row beyond the image
|
||||
assert!(matches!(decode(&pack(SPLIT_12), 4, 1, 12, &t), Err(RawError::Corrupt(_))));
|
||||
// through the container
|
||||
let mut table = hand_table(0x40, 252, 4, 4095);
|
||||
table[562..564].copy_from_slice(&1u16.to_le_bytes());
|
||||
let ok = crate::decode(&nef_file(pack(SPLIT_12), 4, 3, 12, table.clone(), ByteOrder::Little)).unwrap();
|
||||
assert_eq!(ok.data, crate::RawData::U16(vec![256, 252, 255, 252, 252, 256, 244, 388, 0, 252, 8, 252]));
|
||||
let e = crate::decode(&nef_file(longer, 4, 3, 12, table, ByteOrder::Little));
|
||||
assert!(matches!(e, Err(RawError::Unsupported(_))), "{e:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossy_type1_full_curve() {
|
||||
let mut t = vec![0x44, 0x10, 0, 10, 0, 10, 0, 10, 0, 10, 0, 5];
|
||||
for v in [0u16, 100, 400, 900, 1600] {
|
||||
t.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
let parsed = parse_table(&t, ByteOrder::Big, 12).unwrap();
|
||||
assert_eq!(parsed.encoding, Encoding::Lossy(vec![0, 100, 400, 900, 1600]));
|
||||
let img = vec![0, 4, 1, 3, 2, 2, 4, 0];
|
||||
let src = encode(&img, 4, &LOSSY_12, parsed.seeds);
|
||||
assert_eq!(decode(&src, 4, 2, 12, &parsed).unwrap(), vec![0, 1600, 100, 900, 400, 400, 1600, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsupported_and_malformed_tables() {
|
||||
let unsupported = |r: Result<DecodeTable>| matches!(r, Err(RawError::Unsupported(_)));
|
||||
let corrupt = |r: Result<DecodeTable>| matches!(r, Err(RawError::Corrupt(_)));
|
||||
let mut split = lossy_table(12, ByteOrder::Big);
|
||||
split[562..564].copy_from_slice(&345u16.to_be_bytes());
|
||||
assert_eq!(parse_table(&split, ByteOrder::Big, 12).unwrap().split, 345);
|
||||
assert!(unsupported(parse_table(&[0x49, 0x30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ByteOrder::Big, 12)));
|
||||
assert!(unsupported(parse_table(&lossy_table(12, ByteOrder::Big), ByteOrder::Big, 16)));
|
||||
assert!(corrupt(parse_table(&[], ByteOrder::Big, 12)));
|
||||
assert!(corrupt(parse_table(&[0x46, 0x30, 8], ByteOrder::Big, 14)));
|
||||
assert!(corrupt(parse_table(&lossy_table(12, ByteOrder::Big)[..100], ByteOrder::Big, 12)));
|
||||
// 300 points don't divide the 12-bit range
|
||||
let mut odd = lossy_table(12, ByteOrder::Big);
|
||||
odd[10..12].copy_from_slice(&300u16.to_be_bytes());
|
||||
assert!(parse_table(&odd, ByteOrder::Big, 12).is_err());
|
||||
assert!(parse_table(&[0x44, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], ByteOrder::Big, 12).is_err());
|
||||
}
|
||||
|
||||
/// A minimal NEF: CFA SubIFD with one compressed strip, Exif maker note (`Nikon\0` v2 header + embedded TIFF in
|
||||
/// `mn_order`) holding the linearization table.
|
||||
fn nef_file(strip: Vec<u8>, w: u32, h: u32, bits: u16, table: Vec<u8>, mn_order: ByteOrder) -> Vec<u8> {
|
||||
use craft_tiff::tags::{self as t, photometric};
|
||||
use craft_tiff::{IfdBuilder, ImageData, TiffWriter, Value};
|
||||
let mut mn = IfdBuilder::new();
|
||||
mn.set(0x0096, Value::Undefined(table));
|
||||
let mut note = b"Nikon\0\x02\x10\0\0".to_vec();
|
||||
note.extend(TiffWriter::new(mn_order, false).write(&[mn]).unwrap());
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(t::MAKER_NOTE, Value::Undefined(note));
|
||||
let mut raw = IfdBuilder::new();
|
||||
raw.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0]));
|
||||
raw.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
|
||||
raw.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
|
||||
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![bits]));
|
||||
raw.set(t::COMPRESSION, Value::Short(vec![t::compression::NIKON]));
|
||||
raw.set(t::PHOTOMETRIC, Value::Short(vec![photometric::CFA]));
|
||||
raw.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2]));
|
||||
raw.set(t::CFA_PATTERN_EP, Value::Byte(vec![0, 1, 1, 2]));
|
||||
raw.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![strip] });
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(t::MAKE, Value::Ascii("NIKON CORPORATION".into()));
|
||||
ifd0.set(t::MODEL, Value::Ascii("NIKON TEST".into()));
|
||||
ifd0.set_child(t::EXIF_IFD, exif);
|
||||
ifd0.add_sub_ifd(raw);
|
||||
TiffWriter::new(ByteOrder::Big, false).write(&[ifd0]).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_through_the_nef_container() {
|
||||
// lossless 14-bit, big-endian maker note
|
||||
let (w, h) = (40usize, 6usize);
|
||||
let img = image(w, h, 16383, 11);
|
||||
let mut table = vec![0x46, 0x30];
|
||||
for _ in 0..4 {
|
||||
table.extend_from_slice(&2048u16.to_be_bytes());
|
||||
}
|
||||
table.resize(46, 0);
|
||||
let src = encode(&img, w, &LOSSLESS_14, [[2048; 2]; 2]);
|
||||
let bytes = nef_file(src, w as u32, h as u32, 14, table, ByteOrder::Big);
|
||||
let r = crate::decode(&bytes).unwrap();
|
||||
assert_eq!(r.data, crate::RawData::U16(img.iter().map(|&v| v as u16).collect()));
|
||||
assert_eq!(r.cfa.as_ref().unwrap().name(), "RGGB");
|
||||
assert_eq!(crate::probe_info(&bytes).unwrap(), r.info());
|
||||
// lossy 12-bit type 2, little-endian maker note (newer bodies)
|
||||
let table = lossy_table(12, ByteOrder::Little);
|
||||
let t = parse_table(&table, ByteOrder::Little, 12).unwrap();
|
||||
let Encoding::Lossy(curve) = &t.encoding else { panic!() };
|
||||
let img = image(w, h, 1023, 5);
|
||||
let src = encode(&img, w, &LOSSY_12, t.seeds);
|
||||
let r = crate::decode(&nef_file(src.clone(), w as u32, h as u32, 12, table.clone(), ByteOrder::Little)).unwrap();
|
||||
assert_eq!(r.data, crate::RawData::U16(img.iter().map(|&v| curve[v as usize]).collect()));
|
||||
// a split file whose strip does not follow the fixed-rate rule is refused, never decoded wrongly
|
||||
let mut split = table;
|
||||
split[562..564].copy_from_slice(&3u16.to_le_bytes());
|
||||
let e = crate::decode(&nef_file(src, w as u32, h as u32, 12, split, ByteOrder::Little));
|
||||
assert!(e.is_err(), "{e:?}");
|
||||
}
|
||||
|
||||
/// storytold/lightcraft#708: the header probe of a compressed NEF decodes only the first [`crate::nef::MASK_ROWS`]
|
||||
/// rows (the ones the masked-column test reads) instead of the whole Huffman stream, and still finds the masked
|
||||
/// columns the full decode finds. A strip cut off after those rows proves it: the probe succeeds with the same
|
||||
/// description while the full decode reports the truncation.
|
||||
#[test]
|
||||
fn header_probe_stops_after_the_masked_column_rows() {
|
||||
use crate::nef::MASK_ROWS;
|
||||
let (w, h) = (160usize, MASK_ROWS * 3);
|
||||
let mut img = image(w, h, 16383, 7);
|
||||
// eight optically masked columns at 0, as the D5100 / D7000 write them
|
||||
for row in img.chunks_mut(w) {
|
||||
row[w - 8..].fill(0);
|
||||
}
|
||||
let t = lossless(14);
|
||||
let table = {
|
||||
let mut v = vec![0x46, 0x30];
|
||||
for s in t.seeds.iter().flatten() {
|
||||
v.extend_from_slice(&(*s as u16).to_be_bytes());
|
||||
}
|
||||
v.resize(46, 0);
|
||||
v
|
||||
};
|
||||
let src = encode(&img, w, &LOSSLESS_14, t.seeds);
|
||||
let whole = nef_file(src.clone(), w as u32, h as u32, 14, table.clone(), ByteOrder::Big);
|
||||
let full = crate::decode(&whole).unwrap();
|
||||
assert_eq!(full.active_area, crate::Rect::new(0, 0, w - 8, h), "the masked columns are found in a compressed file");
|
||||
assert_eq!(crate::probe_info(&whole).unwrap(), full.info());
|
||||
// the same stream cut off after two thirds of the rows (the first rows' codes are a prefix of the whole)
|
||||
let cut = encode(&img[..w * (h * 2 / 3)], w, &LOSSLESS_14, t.seeds).len();
|
||||
assert!(cut > w * h / 4, "enough bytes to pass the size check");
|
||||
let truncated = nef_file(src[..cut].to_vec(), w as u32, h as u32, 14, table, ByteOrder::Big);
|
||||
assert!(matches!(crate::decode(&truncated), Err(RawError::Corrupt(_))), "the full decode sees the truncation");
|
||||
assert_eq!(crate::probe_info(&truncated).unwrap(), full.info(), "the header probe never reads that far");
|
||||
// decode_rows itself: a prefix of the whole decode, and the whole image's size check still applies
|
||||
let top = decode_rows(&src, w, h, MASK_ROWS, 14, &t).unwrap();
|
||||
assert_eq!(top.len(), w * MASK_ROWS);
|
||||
assert_eq!(top[..], decode(&src, w, h, 14, &t).unwrap()[..w * MASK_ROWS]);
|
||||
assert_eq!(decode_rows(&src, w, h, h + 5, 14, &t).unwrap().len(), w * h, "rows are clamped to the height");
|
||||
assert!(decode_rows(&src[..w * h / 4 - 1], w, h, 1, 14, &t).is_err(), "a strip too short for the image is corrupt in any mode");
|
||||
}
|
||||
|
||||
/// A long run of the largest difference drove a predictor past i32: a panic in builds with overflow checks,
|
||||
/// wrapped values otherwise (storytold/lightcraft#1515). Both the row and the column predictors now stop with an error.
|
||||
#[test]
|
||||
fn predictor_overflow_is_an_error_not_a_panic() {
|
||||
let codes = LOSSLESS_14.codes();
|
||||
let k = (0..codes.len()).rev().find(|&k| codes[k].1 > 0).unwrap();
|
||||
let d = (1u32 << k) - 1;
|
||||
let stream = |n: usize| {
|
||||
let mut wr = Writer::default();
|
||||
let (code, len) = codes[k];
|
||||
for _ in 0..n {
|
||||
wr.put(code as u32 >> (12 - len), len as u32);
|
||||
wr.put(d, k as u32);
|
||||
}
|
||||
wr.finish()
|
||||
};
|
||||
// differences one predictor needs to pass i32::MAX
|
||||
let steps = (i32::MAX as u32 / d) as usize + 2;
|
||||
let table = DecodeTable { encoding: Encoding::Lossless, seeds: [[0; 2]; 2], split: 0 };
|
||||
// along a row: every other sample adds to the same predictor
|
||||
let w = 2 * steps + 2;
|
||||
let err = decode(&stream(w), w, 1, 14, &table).unwrap_err();
|
||||
assert!(matches!(err, RawError::Corrupt(ref m) if m.contains("predictor")), "{err}");
|
||||
// down two columns: each row adds to the seed of its parity
|
||||
let h = 2 * steps + 2;
|
||||
let err = decode(&stream(2 * h), 2, h, 14, &table).unwrap_err();
|
||||
assert!(matches!(err, RawError::Corrupt(ref m) if m.contains("predictor")), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_and_corrupted_streams_error_never_panic() {
|
||||
let (w, h) = (64, 16);
|
||||
let t = lossless(14);
|
||||
let img = image(w, h, 16383, 3);
|
||||
let src = encode(&img, w, &LOSSLESS_14, t.seeds);
|
||||
// truncation: too short for the image, or ends early
|
||||
for cut in [0, 1, 10, src.len() / 2, src.len() - 40] {
|
||||
assert!(decode(&src[..cut], w, h, 14, &t).is_err(), "cut {cut}");
|
||||
}
|
||||
// garbage and bit flips: an error or (rarely) some image, never a panic
|
||||
let mut s = 12345u32;
|
||||
for i in 0..200 {
|
||||
let mut bad = src.clone();
|
||||
for _ in 0..1 + i % 8 {
|
||||
s = s.wrapping_mul(1_103_515_245).wrapping_add(12345);
|
||||
let at = (s >> 8) as usize % bad.len();
|
||||
bad[at] ^= 1 << (s % 8);
|
||||
}
|
||||
let _ = decode(&bad, w, h, 14, &t);
|
||||
let noise: Vec<u8> = (0..src.len()).map(|k| ((k as u32).wrapping_mul(2_654_435_761) >> (i % 24)) as u8).collect();
|
||||
let _ = decode(&noise, w, h, 14, &t);
|
||||
let _ = decode(&noise, w, h, 12, &lossless(12));
|
||||
}
|
||||
// all-ones never forms a valid lossy 12-bit code
|
||||
assert!(decode(&[0xff; 64], 8, 8, 12, &parse_table(&lossy_table(12, ByteOrder::Big), ByteOrder::Big, 12).unwrap()).is_err());
|
||||
// absurd dimensions are limited before allocating
|
||||
assert!(decode(&src, usize::MAX, 2, 14, &t).is_err());
|
||||
assert!(decode(&src, 1 << 20, 1 << 20, 14, &t).is_err());
|
||||
assert!(decode(&src, 0, 2, 14, &t).is_err());
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ fn cr2() -> Vec<u8> {
|
||||
fn samples() -> Vec<Vec<u8>> {
|
||||
vec![
|
||||
cr2(),
|
||||
cfa_tiff("NIKON CORPORATION", 1, 12, ByteOrder::Big),
|
||||
cfa_tiff("NIKON CORPORATION", 34713, 14, ByteOrder::Big),
|
||||
pef(1),
|
||||
pef(65535),
|
||||
cfa_tiff("SAMSUNG", 1, 12, ByteOrder::Little),
|
||||
|
||||
Reference in new issue
Block a user