mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
* craft-raw: the file-format layer of camera raw files, with DNG and CRW A new crate for #587 and #778: bytes in, the sensor data out as a neutral RawImage (samples, CFA, levels, active area, crop, orientation, colour tags, opcode lists, profile tables), and back for DNG. One Cargo feature per format: dng (read and write), jxl (JPEG XL DNG), crw (through craft-crw). The base is LightCraft's crates/raw at 5ff567a4: dng, dngwrite, tiffraw, unpack, ljpeg, jxl, semantic, preview, the parsing half of opcodes, profile and gaintable, the model and probe. Demosaicing and everything after it stay with the develop pipeline. Ported from PhotoCraft's and FilmCraft's DNG readers, each with a test: the default crop's per-tag defaults, an unusable crop falling back to the active area, non-finite white levels, the white default of a linearized image, the AsShotWhiteXY range, 8-bit-only lossy JPEG, lossy tiles that must cover their part of the image, and the CinemaDNG frame tags. * craft-raw: Hasselblad 3FR and FFF (feature hasselblad) RawFormat::CfaTiff decodes: the DNG-layout reader plus what these files add. Uncompressed data or Hasselblad's lossless JPEG (selection value 8), RGGB when no CFAPattern is stated, an FFF's colour matrix from the 3FR header it embeds, and the 8-bit RGB preview handed on as a baseline TIFF. Moved from LightCraft's vendor/hasselblad.rs at 5ff567a4 with its tests. Ported from PhotoCraft's reader: four BlackLevel values without a BlackLevelRepeatDim are one per CFA site in a file that is not a DNG. * craft-raw: Minolta MRW, Epson ERF, Mamiya MEF and Kodak DCS520C Four formats only LightCraft decodes, one feature each (mrw, erf, mef, kodak), moved from its vendor modules at 5ff567a4 with their tests; the MRW block container comes from lightcraft-meta. src/shared.rs holds the level helpers the maker decoders share. ERF and MEF state no white balance: the estimate LightCraft made from the preview is develop-side work, so wb_multipliers is None for them here. * craft-raw: shared scan budget for tagged previews, tile-sized lossy JPEG budget, bound on Deflate tile padding Ported from storytold/lightcraft#3695. Candidate preview ranges are deduplicated and charged to one byte budget and one header-walk budget; a lossy JPEG decoded for a tile may hold no more samples than the tile rounded up to whole MCUs (and never more than Limits::max_lossy_tile_samples); a Deflate chunk whose nominal size is mostly padding outside the image is refused before it is inflated. * craft-raw: lossless JPEG sample bound per referenced byte; previews larger than the file are passed over Ported from storytold/lightcraft#3910 (the parts whose code is in craft-raw today). check_image refuses a lossless JPEG image that declares more than 32 samples per referenced byte, counted once per chunk that points at the bytes so identical tiles stored once still decode. The Hasselblad RGB preview search passes over an image whose pixel bytes exceed the file and still takes a smaller complete one. * craft-raw: test for the Deflate padding bound * craft-raw: Pentax PEF and Samsung SRW Moved from LightCraft crates/raw (vendor/pef.rs, vendor/srw.rs) at 5ff567a4. The Huffman difference decoder and bit reader that PEF shares with the Nikon, Panasonic and Sony codings become the huffman module. * craft-raw: Canon CR2, with sRAW and mRAW Moved from LightCraft crates/raw (vendor/cr2.rs, vendor/cr2/sraw.rs) at 5ff567a4. The white-balance presets a CR2 lists come back as data in ColorData::kelvin_presets (the ordering from LightCraft's kelvin_presets.rs; the temperature and tint arithmetic stays with the developer). * craft-raw: an sRAW preview larger than the file is refused before its buffer is reserved Ported from storytold/lightcraft#3910 (cr2::sraw::read_preview). * WIP craft-raw: Nikon NEF/NRW (does not compile yet: limits threading, develop() calls in tests) * craft-raw: Nikon NEF/NRW, with PhotoCraft's maker-note compression routing and 12-bit D4 white level * craft-raw: Panasonic RW2 / RWL / RAW, every raw format * craft-raw: RW2 format 8 refuses strips stating fewer bits than samples Ported from storytold/lightcraft#3910. * craft-raw: RW2 format 8 refuses samples above the declared bit depth Ported from storytold/photocraft#11838. * craft-raw: the RW2 format 8 robustness seed carries a valid stream Its random payload decoded only while out-of-depth samples were clamped; mutations now start from a file that decodes. * craft-raw: Fujifilm RAF, uncompressed and compressed, with lens corrections * craft-raw: compressed RAF stripes cap their capacity hint at the stripe's bits Ported from storytold/lightcraft#3910. * craft-raw: test RAF orientation and camera from the embedded JPEG's Exif * WIP craft-raw: Olympus ORF * craft-raw: RAF embedded preview, and its range from the header alone (bounded_preview) * craft-raw: Olympus ORF, the uncompressed layouts, with its maker-note preview and WB_GLevel * WIP craft-raw: Sony ARW * craft-raw: tiled lossless ARW decodes hold one group of tiles at a time Ported from storytold/lightcraft#3910. * craft-raw: dark 12-bit uncompressed ARWs keep Sony's 14-bit levels, and the DNG writer's depth holds the white level Ported from storytold/lightcraft#3429. * craft-raw: Sony ARW family, docs, seeds and the derivation records * WIP craft-raw: Canon CR3 * craft-raw: Canon CR3 (CRX) through craft-metadata's container, header plans, CR3 previews and the coverage records * craft-raw: say where a raw's black and white levels came from (LevelSource) Ported from storytold/lightcraft#3319. * craft-raw: decode_format builds with every format feature off --------- Co-authored-by: radialmonster <radial@gmail.com> Co-authored-by: wnanbei <wnanbei@gmail.com> Co-authored-by: lownlyrider <lownlyrider@gmail.com> Co-authored-by: Julien LEQUEN <jlequen@crypt-0n.fr>
1515 lines
82 KiB
Rust
1515 lines
82 KiB
Rust
//! Nikon NEF / NRW — uncompressed and Huffman-compressed (lossless, lossy) variants.
|
||
//!
|
||
//! Sources: TIFF 6.0 (the raw image is a standard CFA SubIFD), Laurent Clévy's NEF structure notes (prose: IFD
|
||
//! layout, SubIFDs, maker note header) and the ExifTool Nikon tag-name documentation (`0x000c` WB_RBLevels,
|
||
//! `0x003d` BlackLevel, `0x0096` NEFLinearizationTable). The Huffman-compressed data (compression 34713) is decoded
|
||
//! by [`crate::nefc`], which documents its clean-room sources; "lossy after split" files are decoded when their strip follows the
|
||
//! rule documented there; files it can't decode are reported as [`RawError::Unsupported`] and their embedded previews still work. So are High Efficiency NEFs
|
||
//! (HE / HE★, Z 8, Z 9, Z 6III, Z f): they keep compression 34713 but carry a wavelet codestream that starts with
|
||
//! the JPEG XS markers SOC + CAP (ISO/IEC 21122-1, `FF10 FF50`) and have no `0x0096` table (storytold/lightcraft#193).
|
||
//! Strips labelled 34713 without a `0x0096`
|
||
//! linearization table are packed uncompressed data in several measured layouts ([`PackedLayout`]) or, when the strip
|
||
//! is exactly three bytes per pixel, Nikon small raw ([`small_raw`]); the D1X's genuinely Huffman-coded strip stays
|
||
//! refused.
|
||
|
||
use crate::nefc;
|
||
use crate::shared::white_from_data;
|
||
use crate::tiffraw::{Packing, check_image, read_image};
|
||
use crate::{BlackLevel, Camera, Cfa, ColorData, Limits, Mode, OpcodeLists, Orientation, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||
use craft_tiff::image::ImageInfo;
|
||
use craft_tiff::tags::{self as t, photometric};
|
||
use craft_tiff::{ByteOrder, Ifd, Tiff, makernote};
|
||
|
||
const WB_RB_LEVELS: u16 = 0x000c;
|
||
const BLACK_LEVEL: u16 = 0x003d;
|
||
const LINEARIZATION_TABLE: u16 = 0x0096;
|
||
const CROP_AREA: u16 = 0x0045;
|
||
/// Maker note NEFCompression (ExifTool's Nikon tag documentation): `0x0093`, or in newer bodies the `u16` at
|
||
/// byte 10 of `0x0051`. 6 is packed 12-bit, 10 packed 14-bit, 13 and 14 High Efficiency and High Efficiency★.
|
||
const NEF_COMPRESSION: u16 = 0x0093;
|
||
const NEF_COMPRESSION_RECORD: u16 = 0x0051;
|
||
|
||
fn nef_compression(mn: Option<&makernote::MakerNote>) -> Option<u64> {
|
||
let m = mn?;
|
||
m.ifd.u64(NEF_COMPRESSION).or_else(|| {
|
||
let v: [u8; 2] = m.ifd.bytes(NEF_COMPRESSION_RECORD)?.get(10..12)?.try_into().ok()?;
|
||
Some(u64::from(m.order.u16(v)))
|
||
})
|
||
}
|
||
/// JPEG XS start of codestream (SOC, `FF10`) followed by its mandatory capabilities marker (CAP, `FF50`).
|
||
const JPEG_XS_START: [u8; 4] = [0xff, 0x10, 0xff, 0x50];
|
||
/// Why a High Efficiency NEF shows its embedded preview: the UI shows the part before " (".
|
||
pub(crate) const HIGH_EFFICIENCY: &str =
|
||
"Nikon High Efficiency NEF (HE / HE★, a JPEG XS-based codec with no public specification; shoot RAW Lossless compressed to edit the raw data)";
|
||
/// Maker note `0x0014`, the "NRW" data block of Coolpix raws: `"NRW "` + a 4-character version, then fields. In
|
||
/// version `0104` the `u32` at byte `0x20` is the black level in sample units (200 in the four 12-bit files here
|
||
/// that have 200 as their darkest samples, 0 in the one whose samples reach 0).
|
||
const NRW_DATA: u16 = 0x0014;
|
||
const NRW_BLACK_AT: usize = 0x20;
|
||
|
||
/// The NRW data block, when the maker note has one.
|
||
fn nrw_data(mn: Option<&makernote::MakerNote>) -> Option<(&[u8], ByteOrder)> {
|
||
let m = mn?;
|
||
m.ifd.bytes(NRW_DATA).filter(|b| b.starts_with(b"NRW ")).map(|b| (b, m.order))
|
||
}
|
||
|
||
/// Black level from the NRW data block, bounded to a quarter of the sample range so an unknown version's field
|
||
/// can't turn into a large offset.
|
||
fn nrw_black(mn: Option<&makernote::MakerNote>, bits: u32) -> Option<f32> {
|
||
let (b, order) = nrw_data(mn)?;
|
||
let v = order.u32(b.get(NRW_BLACK_AT..NRW_BLACK_AT + 4)?.try_into().ok()?);
|
||
(u64::from(v) < (1u64 << bits.min(16)) / 4).then_some(v as f32)
|
||
}
|
||
|
||
/// Nikon CropArea is `[left, top, width, height]` in sensor pixels (maker-note tag documentation).
|
||
/// Keep the active area/CFA origin unchanged: the default crop is applied after demosaicing.
|
||
fn default_crop(mn: Option<&makernote::MakerNote>, w: usize, h: usize) -> Rect {
|
||
let fallback = Rect::new(0, 0, w, h);
|
||
let Some(values) = mn.and_then(|m| m.ifd.u64s(CROP_AREA)) else { return fallback };
|
||
let [x, y, width, height] = values.as_slice() else { return fallback };
|
||
if *width == 0 || *height == 0 || x.checked_add(*width).is_none_or(|v| v > w as u64) || y.checked_add(*height).is_none_or(|v| v > h as u64) {
|
||
return fallback;
|
||
}
|
||
Rect::new(*x as usize, *y as usize, *width as usize, *height as usize)
|
||
}
|
||
|
||
/// A body that records no CropArea, measured on its own files: the image area inside the sensor data (where the
|
||
/// camera's full-size JPEG sits when the two are aligned) and, when the data's own top would be wrong, the level
|
||
/// where its channels clip.
|
||
struct MeasuredBody {
|
||
model: &'static str,
|
||
/// Sensor data size and sample depth of the measured files.
|
||
size: (usize, usize),
|
||
bits: u32,
|
||
/// Image area in sensor pixels: x, y, width, height.
|
||
crop: (usize, usize, usize, usize),
|
||
white: Option<f32>,
|
||
}
|
||
|
||
/// * **NIKON D4** (148 uncompressed 14-bit NEFs from one body, 2023–2026, ISO 100–10000): the data is 4992 × 3292,
|
||
/// with 2 columns of optical black left of the image and 42 right of it, then 8 empty ones; the camera's
|
||
/// 4928 × 3280 JPEG sits at (8, 6). Without this area the optical black shows as purple stripes on both sides.
|
||
/// Green saturates at 15546–15672 while red and blue run on to 15687–16383, so the data's own top (~16300) leaves
|
||
/// clipped highlights magenta; one level of 15520 for every channel turns them white and keeps the white balance.
|
||
/// * **NIKON D7100** (storytold/lightcraft#1248; 14-bit NEFs from the public zjqc/photographing-art repository, with their
|
||
/// Lightroom Classic exports): the data holds picture, not masked lines, beyond the camera's frame. In full frame
|
||
/// it is 6036 × 4020 and the camera's JPEG is 6000 × 4000; in the 1.3x crop mode it is 4812 × 3212 and the JPEG
|
||
/// 4800 × 3200. Fitting renders of the full data to Lightroom's exports (feature matches, homography) puts
|
||
/// Lightroom's frame, the camera JPEG's size, at (18, 10) and (6, 6): within 0.5 px mean on DSC_1316, DSC_2763
|
||
/// and DSC_1486. Without it, renders are larger than Lightroom's and its crops land up to 6 px off (18 px in
|
||
/// full frame).
|
||
/// * **NIKON D90** (550 compressed 12-bit NEFs from one body, 10 measured): the data is 4352 × 2868 and its last 42
|
||
/// columns (from 4310) are filler at a flat ~254 whatever the exposure, above the dark image samples (black 0), so
|
||
/// the masked-column test keeps them and they develop as a pink band on the right. The camera's 4288 × 2848 JPEG
|
||
/// sits at (10, 10). Green saturates at 3840–3851 while red and blue run on to 4095; one level of 3830 keeps
|
||
/// clipped highlights white.
|
||
/// * **NIKON D7000** and **NIKON D5100** (and the Coolpix A, the same readout): the data is 4992 × 3280, the picture its
|
||
/// first 4948 columns, then 36 masked ones at one level (~600, with a black level of 0) and 8 of 0. The masked
|
||
/// columns sit above the dark-column trim's threshold, so without this area they show as a coloured bar on the right
|
||
/// edge (the top in portrait; storytold/lightcraft#976). The camera's 4928 × 3264 JPEG sits at (8, 8): in the D5100's 3 CC0
|
||
/// samples, and in the D7000's one clean one, with the D5100 and Coolpix A as siblings (see below).
|
||
/// * **The other bodies** below keep masked or dark lines inside the whole data (a survey of every Nikon raw on raw.pixls.us):
|
||
/// each crop is its camera JPEG's frame as `cargo xtask raw-frame` measures it on raw.pixls.us CC0 samples (the
|
||
/// D200, D3200 and Nikon 1 V1 also on other publicly posted sample files), with at least 3 clean
|
||
/// files agreeing: of the model (bit depths pooled: both read out the same area), or, with at least
|
||
/// one of its own, together with models of the same readout (data and JPEG size) that agree on the same frame
|
||
/// (*siblings*). The D4's 12-bit mode reads out the 14-bit area measured above; the Df, the same readout, agrees on
|
||
/// (8, 6) in 6 files. Bodies whose crop holds only picture beyond the camera frame have no entry (no change).
|
||
const MEASURED_BODIES: &[MeasuredBody] = &[
|
||
MeasuredBody { model: "NIKON D4", size: (4992, 3292), bits: 14, crop: (8, 6, 4928, 3280), white: Some(15520.0) },
|
||
// the 12-bit samples are the 14-bit ones shifted right by two, and so is the level: 15520 >> 2
|
||
MeasuredBody { model: "NIKON D4", size: (4992, 3292), bits: 12, crop: (8, 6, 4928, 3280), white: Some(3880.0) },
|
||
MeasuredBody { model: "NIKON D7100", size: (6036, 4020), bits: 14, crop: (18, 10, 6000, 4000), white: None },
|
||
MeasuredBody { model: "NIKON D7100", size: (4812, 3212), bits: 14, crop: (6, 6, 4800, 3200), white: None },
|
||
MeasuredBody { model: "NIKON D90", size: (4352, 2868), bits: 12, crop: (10, 10, 4288, 2848), white: Some(3830.0) },
|
||
MeasuredBody { model: "NIKON D7000", size: (4992, 3280), bits: 14, crop: (8, 8, 4928, 3264), white: None },
|
||
MeasuredBody { model: "NIKON D7000", size: (4992, 3280), bits: 12, crop: (8, 8, 4928, 3264), white: None },
|
||
MeasuredBody { model: "NIKON D5100", size: (4992, 3280), bits: 14, crop: (8, 8, 4928, 3264), white: None },
|
||
// siblings: D7000 (4), D5100 (3); masked: right 36
|
||
MeasuredBody { model: "COOLPIX A", size: (4992, 3280), bits: 14, crop: (8, 8, 4928, 3264), white: None },
|
||
// 6 files each; masked: left 2, right 2
|
||
MeasuredBody { model: "NIKON D3", size: (4288, 2844), bits: 12, crop: (15, 6, 4256, 2832), white: None },
|
||
MeasuredBody { model: "NIKON D3", size: (4288, 2844), bits: 14, crop: (15, 6, 4256, 2832), white: None },
|
||
MeasuredBody { model: "NIKON D3S", size: (4288, 2844), bits: 12, crop: (15, 6, 4256, 2832), white: None },
|
||
MeasuredBody { model: "NIKON D3S", size: (4288, 2844), bits: 14, crop: (15, 6, 4256, 2832), white: None },
|
||
MeasuredBody { model: "NIKON D700", size: (4288, 2844), bits: 12, crop: (15, 6, 4256, 2832), white: None },
|
||
MeasuredBody { model: "NIKON D700", size: (4288, 2844), bits: 14, crop: (15, 6, 4256, 2832), white: None },
|
||
// 6 and 4 files; masked: right up to 32
|
||
MeasuredBody { model: "NIKON D300", size: (4352, 2868), bits: 12, crop: (15, 10, 4288, 2848), white: None },
|
||
MeasuredBody { model: "NIKON D300", size: (4352, 2868), bits: 14, crop: (15, 10, 4288, 2848), white: None },
|
||
MeasuredBody { model: "NIKON D300S", size: (4352, 2868), bits: 12, crop: (15, 10, 4288, 2848), white: None },
|
||
MeasuredBody { model: "NIKON D300S", size: (4352, 2868), bits: 14, crop: (15, 10, 4288, 2848), white: None },
|
||
// D2X (1) and D2Xs (2) as siblings; dark: right 1
|
||
MeasuredBody { model: "NIKON D2Xs", size: (4320, 2868), bits: 12, crop: (16, 10, 4288, 2848), white: None },
|
||
// D3000, D40X, D60, D80: one file each, siblings; masked: right 4, bottom 3
|
||
MeasuredBody { model: "NIKON D3000", size: (3904, 2616), bits: 12, crop: (14, 9, 3872, 2592), white: None },
|
||
MeasuredBody { model: "NIKON D40X", size: (3904, 2616), bits: 12, crop: (14, 9, 3872, 2592), white: None },
|
||
MeasuredBody { model: "NIKON D60", size: (3904, 2616), bits: 12, crop: (14, 9, 3872, 2592), white: None },
|
||
MeasuredBody { model: "NIKON D80", size: (3904, 2616), bits: 12, crop: (14, 9, 3872, 2592), white: None },
|
||
// 3 files; masked: left 1, right 3 (the D80's data size, framed elsewhere)
|
||
MeasuredBody { model: "NIKON D200", size: (3904, 2616), bits: 12, crop: (16, 12, 3872, 2592), white: None },
|
||
// 22 files; masked: right 46
|
||
MeasuredBody { model: "NIKON D3200", size: (6080, 4012), bits: 12, crop: (8, 6, 6016, 4000), white: None },
|
||
// 8 clean of 19, the rest ~0.3 px between pixels on the same frame; dark: bottom 2
|
||
MeasuredBody { model: "NIKON 1 V1", size: (3904, 2606), bits: 12, crop: (12, 6, 3872, 2592), white: None },
|
||
// Best guesses, short of the evidence rule; every frame below keeps the masked lines out by several pixels.
|
||
// D3100: 3 of 4 clean files at (24, 6), the 4th at (22, 7); masked: left 6, right 22
|
||
MeasuredBody { model: "NIKON D3100", size: (4672, 3084), bits: 12, crop: (24, 6, 4608, 3072), white: None },
|
||
// D5000: 4 of 5 clean files at (11, 10), the 5th at (11, 12); the D90's sensor, whose own files also align at
|
||
// x = 11; masked: right 42 (the same filler as the D90's)
|
||
MeasuredBody { model: "NIKON D5000", size: (4352, 2868), bits: 12, crop: (11, 10, 4288, 2848), white: None },
|
||
// D40: 2 of 2 clean files at (16, 9); masked: right 1. The D50 / D70 / D70s share its data size but not its frame
|
||
MeasuredBody { model: "NIKON D40", size: (3040, 2014), bits: 12, crop: (16, 9, 3008, 2000), white: None },
|
||
// D50, D70, D70s (one readout): one clean D50 and one clean D70 file at (15, 7); the D70s's 2 files align
|
||
// between pixels next to it, at (15.4, 6.9) and (15.6, 6.4); masked: right 1
|
||
MeasuredBody { model: "NIKON D50", size: (3040, 2014), bits: 12, crop: (15, 7, 3008, 2000), white: None },
|
||
MeasuredBody { model: "NIKON D70", size: (3040, 2014), bits: 12, crop: (15, 7, 3008, 2000), white: None },
|
||
MeasuredBody { model: "NIKON D70s", size: (3040, 2014), bits: 12, crop: (15, 7, 3008, 2000), white: None },
|
||
// 3 files; dark: bottom 4
|
||
MeasuredBody { model: "NIKON D4S", size: (4936, 3288), bits: 12, crop: (4, 4, 4928, 3280), white: None },
|
||
MeasuredBody { model: "NIKON D4S", size: (4936, 3288), bits: 14, crop: (4, 4, 4928, 3280), white: None },
|
||
// 4 files; dark: bottom up to 4
|
||
MeasuredBody { model: "NIKON D5600", size: (6016, 4016), bits: 12, crop: (8, 8, 6000, 4000), white: None },
|
||
MeasuredBody { model: "NIKON D5600", size: (6016, 4016), bits: 14, crop: (8, 8, 6000, 4000), white: None },
|
||
// 4 files each; masked: right 8
|
||
MeasuredBody { model: "NIKON D600", size: (6080, 4028), bits: 12, crop: (8, 6, 6016, 4016), white: None },
|
||
MeasuredBody { model: "NIKON D600", size: (6080, 4028), bits: 14, crop: (8, 6, 6016, 4016), white: None },
|
||
MeasuredBody { model: "NIKON D610", size: (6080, 4028), bits: 12, crop: (8, 6, 6016, 4016), white: None },
|
||
MeasuredBody { model: "NIKON D610", size: (6080, 4028), bits: 14, crop: (8, 6, 6016, 4016), white: None },
|
||
// D800: 6 files; D800E: 2, with the D800 as sibling; masked: right 14 (storytold/lightcraft#914, the top in portrait)
|
||
MeasuredBody { model: "NIKON D800", size: (7424, 4924), bits: 12, crop: (8, 6, 7360, 4912), white: None },
|
||
MeasuredBody { model: "NIKON D800", size: (7424, 4924), bits: 14, crop: (8, 6, 7360, 4912), white: None },
|
||
MeasuredBody { model: "NIKON D800E", size: (7424, 4924), bits: 14, crop: (8, 6, 7360, 4912), white: None },
|
||
// 3 files; dark: bottom 1
|
||
MeasuredBody { model: "NIKON D810", size: (7380, 4928), bits: 12, crop: (12, 8, 7360, 4912), white: None },
|
||
MeasuredBody { model: "NIKON D810", size: (7380, 4928), bits: 14, crop: (12, 8, 7360, 4912), white: None },
|
||
// 6 files; masked: left 2, right 42 (the D4's readout)
|
||
MeasuredBody { model: "NIKON Df", size: (4992, 3292), bits: 12, crop: (8, 6, 4928, 3280), white: None },
|
||
MeasuredBody { model: "NIKON Df", size: (4992, 3292), bits: 14, crop: (8, 6, 4928, 3280), white: None },
|
||
];
|
||
|
||
fn measured_body(model: &str, w: usize, h: usize, bits: u32) -> Option<&'static MeasuredBody> {
|
||
MEASURED_BODIES.iter().find(|b| b.model == model.trim() && b.size == (w, h) && b.bits == bits)
|
||
}
|
||
|
||
/// Pixel height over width from the raw IFD's resolution tags: the D1X and D1H write 600 x 300 per inch for a
|
||
/// 4028 x 1324 frame, rows twice as tall as the columns are wide. Only ratios from 1.5 to 4 count (a frame
|
||
/// squashed 3:1 or 2:1), the rest is rounding or a stray tag.
|
||
pub(crate) fn pixel_aspect_of(raw: &Ifd) -> f32 {
|
||
match (raw.f64(t::X_RESOLUTION), raw.f64(t::Y_RESOLUTION)) {
|
||
(Some(x), Some(y)) if x > 0.0 && y > 0.0 && (1.5..=4.0).contains(&(x / y)) => (x / y).round() as f32,
|
||
_ => 1.0,
|
||
}
|
||
}
|
||
|
||
fn raw_ifd(tiff: &Tiff) -> Option<&Ifd> {
|
||
tiff.all_ifds()
|
||
.into_iter()
|
||
.filter(|i| i.u16(t::PHOTOMETRIC) == Some(photometric::CFA))
|
||
.max_by_key(|i| i.u64(t::IMAGE_WIDTH).unwrap_or(0).saturating_mul(i.u64(t::IMAGE_LENGTH).unwrap_or(0)))
|
||
}
|
||
|
||
/// Rows the masked-column test reads, from the top of the frame. The header probe ([`Mode::Header`]) decodes
|
||
/// only these (a compressed strip is one sequential Huffman stream, so it can stop here; an uncompressed one reads
|
||
/// just its first strips), and the full decode judges the active width from the same rows, so
|
||
/// [`crate::probe_info`] equals [`RawImage::info`] of the decoded image (storytold/lightcraft#708). On every corpus NEF the
|
||
/// answer is the same for 8 to 512 rows as for the whole frame: the masked columns decode to 0 (compressed) or
|
||
/// ~19 (uncompressed) while live columns sit at the black level (~150 at 12 bit, ~600 at 14 bit) or above.
|
||
pub(crate) const MASK_ROWS: usize = 128;
|
||
|
||
/// Width without the optically masked columns some bodies append on the right (the D5100 and D7000 write 8, with
|
||
/// no maker-note tag that says so, in compressed and uncompressed files alike): trailing columns (at most 64) whose
|
||
/// mean over the first [`MASK_ROWS`] rows of `d` is below 1% of the nominal white level and a tenth of the image
|
||
/// interior. Kept even for CFA phase.
|
||
pub(crate) fn trailing_masked_columns(d: &[u16], w: usize, bits: u32) -> usize {
|
||
let rows = d.len().checked_div(w).unwrap_or(0).min(MASK_ROWS);
|
||
if w < 128 || rows == 0 {
|
||
return w;
|
||
}
|
||
let col_mean = |x: usize| (0..rows).map(|y| d.get(y * w + x).copied().unwrap_or(0) as f64).sum::<f64>() / rows as f64;
|
||
let interior = (w / 4..w * 3 / 4).step_by(w / 64).map(col_mean).sum::<f64>() / 32.0;
|
||
let white = ((1u32 << bits.clamp(1, 16)) - 1) as f64;
|
||
let dark = (white * 0.01).min(interior * 0.1);
|
||
let mut aw = w;
|
||
while aw > w - 64 && col_mean(aw - 1) < dark {
|
||
aw -= 1;
|
||
}
|
||
aw & !1
|
||
}
|
||
|
||
pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawImage> {
|
||
let tiff = Tiff::parse(bytes)?;
|
||
let ifd0 = tiff.ifds.first().ok_or_else(|| RawError::Corrupt("TIFF without IFDs".into()))?;
|
||
let raw = raw_ifd(&tiff).ok_or_else(|| RawError::Unsupported("NEF without a CFA image IFD".into()))?;
|
||
let info = raw.image()?;
|
||
let (w, h) = (info.width as usize, info.height as usize);
|
||
let bits = info.bits() as u32;
|
||
let make = ifd0.string(t::MAKE).unwrap_or_default();
|
||
let mn =
|
||
tiff.exif().and_then(|e| e.get(t::MAKER_NOTE)).and_then(|e| makernote::parse_makernote(bytes, e.offset, e.count() as u64, tiff.order, &make));
|
||
if let Some(strip) = small_raw_strip(&info, bytes.len(), mn.as_ref()) {
|
||
return small_raw(bytes, &tiff, &info, strip, mode, limits);
|
||
}
|
||
// the header probe reads only the rows the masked-column test needs
|
||
let rows = match mode {
|
||
Mode::Full => h,
|
||
Mode::Header => h.min(MASK_ROWS),
|
||
};
|
||
let data = if info.compression == t::compression::NIKON {
|
||
compressed(bytes, &info, mn.as_ref(), rows, limits)?
|
||
} else {
|
||
uncompressed(bytes, &info, tiff.order, mn.as_ref(), rows, limits)?
|
||
};
|
||
let RawData::U16(ref samples) = data else { return Err(RawError::Unsupported("float NEF".into())) };
|
||
|
||
// Maker note 0x003d is in 14-bit units whatever the sample depth: 12-bit files from the D750, D780, D850,
|
||
// D7500 and Z 50 store 600 / 1008 / 400 / 400 / 1008 while their darkest samples are 150 / ~252 / 99 / 99 / ~250
|
||
// (our measurements on CC0 raw.pixls.us samples; the same bodies' 14-bit files match the tag as is).
|
||
let black_scale = if bits == 12 { 0.25 } else { 1.0 };
|
||
let black = match mn.as_ref().and_then(|m| m.ifd.f64s(BLACK_LEVEL)).as_deref() {
|
||
Some([a, b, c, d]) if [a, b, c, d].iter().all(|v| **v < 16384.0) => BlackLevel {
|
||
repeat_rows: 2,
|
||
repeat_cols: 2,
|
||
values: [a, b, c, d].iter().map(|v| (**v * black_scale) as f32).collect(),
|
||
..Default::default()
|
||
},
|
||
_ => BlackLevel::uniform(nrw_black(mn.as_ref(), bits).unwrap_or_else(|| be16_black(&info, bytes.len() as u64, mn.as_ref(), samples))),
|
||
};
|
||
let wb = mn
|
||
.as_ref()
|
||
.and_then(|m| m.ifd.f64s(WB_RB_LEVELS))
|
||
.filter(|v| v.len() >= 2 && v[0] > 0.1 && v[1] > 0.1 && v[0] < 10.0 && v[1] < 10.0)
|
||
.map(|v| [v[0] as f32, 1.0, v[1] as f32]);
|
||
let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) {
|
||
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
|
||
_ => Cfa::bayer_static("RGGB"),
|
||
};
|
||
let model = ifd0.string(t::MODEL).unwrap_or_default();
|
||
let body = measured_body(&model, w, h, bits);
|
||
let active_w = trailing_masked_columns(samples, w, bits);
|
||
// the white level is measured from the whole image (or known for a measured body); the probe (no samples in its
|
||
// result) takes the nominal one
|
||
let white = match (body.and_then(|b| b.white), mode) {
|
||
(Some(white), _) => white,
|
||
(None, Mode::Full) => white_from_data(samples, bits),
|
||
(None, Mode::Header) => ((1u32 << bits.clamp(1, 16)) - 1) as f32,
|
||
};
|
||
let crop = match body {
|
||
Some(&MeasuredBody { crop: (x, y, cw, ch), .. }) if x + cw <= active_w && y + ch <= h => Rect::new(x, y, cw, ch),
|
||
_ => default_crop(mn.as_ref(), active_w, h),
|
||
};
|
||
let pixel_aspect = pixel_aspect_of(raw);
|
||
let img = RawImage {
|
||
format: RawFormat::Nef,
|
||
width: w,
|
||
height: h,
|
||
cpp: 1,
|
||
data: match mode {
|
||
Mode::Full => data,
|
||
Mode::Header => RawData::U16(Vec::new()),
|
||
},
|
||
cfa: Some(cfa),
|
||
bits,
|
||
black,
|
||
white: vec![white],
|
||
active_area: Rect::new(0, 0, active_w, h),
|
||
crop,
|
||
orientation: Orientation::from_exif(ifd0.u16(t::ORIENTATION).unwrap_or(1)),
|
||
color: ColorData::default(),
|
||
wb_multipliers: wb,
|
||
linearized: false,
|
||
pixel_aspect,
|
||
// Nikon's in-file distortion correction, about the optical centre (which follows the sensor shift)
|
||
opcodes: OpcodeLists {
|
||
list3: crate::nef_lens::distortion(&model, raw, Rect::new(0, 0, active_w, h), crop).into_iter().collect(),
|
||
..Default::default()
|
||
},
|
||
camera: tiff.ifds.first().map(Camera::from_ifd0).unwrap_or_default(),
|
||
cinema: Default::default(),
|
||
lens_metadata: Default::default(),
|
||
level_sources: Default::default(),
|
||
};
|
||
img.validate_for(mode)?;
|
||
Ok(img)
|
||
}
|
||
|
||
/// Uncompressed strips: 16-bit words or 12-bit packed, told apart by the strip size. Packed 12-bit data is an MSB-first
|
||
/// byte stream, except in files with an NRW data block (Coolpix), where it is MSB-first inside little-endian 32-bit
|
||
/// words (the rows are whole words). Reads the first `rows` rows (the strips that hold them) of the `info.height`.
|
||
fn uncompressed(
|
||
bytes: &[u8],
|
||
info: &ImageInfo,
|
||
order: ByteOrder,
|
||
mn: Option<&makernote::MakerNote>,
|
||
rows: usize,
|
||
limits: &Limits,
|
||
) -> Result<RawData> {
|
||
let (w, h) = (info.width as usize, info.height as usize);
|
||
let bits = info.bits() as u32;
|
||
let row_samples = w * info.samples_per_pixel as usize;
|
||
let chunks = info.chunks(bytes.len() as u64);
|
||
let rows_in_first = chunks.first().map(|c| c.height as usize).unwrap_or(h).max(1);
|
||
let bytes_per_row = chunks.first().map(|c| c.len as usize / rows_in_first).unwrap_or(0);
|
||
let packing = if bytes_per_row >= row_samples * 2 {
|
||
Packing::Word16
|
||
} else if bits == 12 && bytes_per_row * 8 >= row_samples * 12 && bytes_per_row * 8 < row_samples * 13 {
|
||
if nrw_data(mn).is_some() && bytes_per_row.is_multiple_of(4) { Packing::Words32Msb } else { Packing::Msb }
|
||
} else if info.compression == 1 && bits != 8 && bits != 16 {
|
||
return Err(RawError::Unsupported(format!("NEF uncompressed packing ({bytes_per_row} bytes per {row_samples}-sample row)")));
|
||
} else {
|
||
Packing::Msb
|
||
};
|
||
if rows >= h {
|
||
return read_image(bytes, info, order, packing, limits);
|
||
}
|
||
// the whole image's limits apply either way; then only the strips holding the first `rows` rows are unpacked
|
||
check_image(bytes, info, limits)?;
|
||
let top = ImageInfo { height: rows as u32, ..info.clone() };
|
||
read_image(bytes, &top, order, packing, limits)
|
||
}
|
||
|
||
/// Packed layouts of strips that are labelled compression 34713 but have no linearization table (maker note
|
||
/// `0x0096`): they hold plain packed samples, not Huffman codes. The layouts were measured from CC0 raw.pixls.us
|
||
/// files by a clean-room black-box analysis (strip byte-count identity, same-colour smoothness and correlation with
|
||
/// the camera JPEG against at least eight alternative packings). Every layout is a single strip with
|
||
/// `stride = StripByteCounts / height`, selected only when `stride * height` equals the strip size exactly.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
enum PackedLayout {
|
||
/// LSB-first bit stream of `bits` per sample (12: 3 bytes per 2 samples, 14: 7 bytes per 4), rows byte aligned.
|
||
/// 12-bit rows are `3w/2` bytes (older bodies) or padded to 16 bytes (Z 6, D6); 14-bit rows are `7w/4` bytes
|
||
/// padded to 16 bytes.
|
||
Lsb { stride: usize },
|
||
/// D100: 16-byte chunks of fifteen data bytes holding ten MSB-first 12-bit samples, plus one zero pad byte. The
|
||
/// rows carry more samples than the IFD width (3040 against 3034); the extra columns are not image.
|
||
Chunked16 { stride: usize },
|
||
/// Coolpix NRW: one big-endian 16-bit word per pixel, rows `2w` bytes with no header or padding; the 12-bit sample
|
||
/// is the upper 12 bits of the word (the low nibble is zero, rarely 8).
|
||
Be16,
|
||
}
|
||
|
||
fn packed_layout(w: usize, h: usize, bits: u32, strip: u64, stated: bool) -> Option<PackedLayout> {
|
||
if w == 0 || h == 0 || !matches!(bits, 12 | 14) {
|
||
return None;
|
||
}
|
||
let strip = usize::try_from(strip).ok()?;
|
||
if !strip.is_multiple_of(h) {
|
||
return None;
|
||
}
|
||
let stride = strip / h;
|
||
let tight = w.checked_mul(bits as usize)?.div_ceil(8);
|
||
let tight = if bits == 12 && w.is_multiple_of(2) || bits == 14 && w.is_multiple_of(4) { tight } else { return None };
|
||
let padded = tight.checked_next_multiple_of(16)?;
|
||
let lsb_ok = match bits {
|
||
12 => stride == tight || stride == padded,
|
||
_ => stride == padded || stated && stride == tight,
|
||
};
|
||
if lsb_ok {
|
||
return Some(PackedLayout::Lsb { stride });
|
||
}
|
||
None
|
||
}
|
||
|
||
fn d100_layout(w: usize, h: usize, bits: u32, strip: u64) -> Option<PackedLayout> {
|
||
let strip = usize::try_from(strip).ok()?;
|
||
if bits != 12 || h == 0 || !strip.is_multiple_of(h) {
|
||
return None;
|
||
}
|
||
let stride = strip / h;
|
||
let per_row = stride / 16 * 10;
|
||
(stride.is_multiple_of(16) && per_row >= w && per_row < w.checked_add(10)?).then_some(PackedLayout::Chunked16 { stride })
|
||
}
|
||
|
||
/// 12-bit strips of exactly `2 * w * h` bytes: big-endian 16-bit words (Coolpix P330, P7700, P7800 NRW, found by a
|
||
/// clean-room black-box analysis of the CC0 files). Tried after the other layouts, whose strides (`1.5w` to
|
||
/// `1.5w + 15`, or ten samples per 16 bytes) only reach `2w` for rows narrower than 40 samples, so it can't take a
|
||
/// strip they handle.
|
||
fn be16_layout(w: usize, h: usize, bits: u32, strip: u64) -> Option<PackedLayout> {
|
||
let want = w.checked_mul(h)?.checked_mul(2)?;
|
||
(bits == 12 && w > 0 && h > 0 && usize::try_from(strip).ok()? == want).then_some(PackedLayout::Be16)
|
||
}
|
||
|
||
/// Black level of the big-endian 16-bit layout, which carries neither `0x003d` nor an NRW block and has no masked
|
||
/// pixels (the borders of the frame are image). The darkest samples of the three measured bodies are 214 (P330),
|
||
/// 217 (P7700) and 201 (P7800, the only one with a dark enough frame: its per-channel 1st percentile is 209 / 225 /
|
||
/// 223), and the other 12-bit Coolpix NRWs state 200 in their NRW block; so 200, never above the darkest sample.
|
||
const BE16_BLACK: f32 = 200.0;
|
||
|
||
fn be16_black(info: &ImageInfo, file_len: u64, mn: Option<&makernote::MakerNote>, samples: &[u16]) -> f32 {
|
||
let has_table = mn.is_some_and(|m| m.ifd.bytes(LINEARIZATION_TABLE).is_some());
|
||
let single = matches!(info.chunks(file_len).as_slice(), [c] if be16_layout(info.width as usize, info.height as usize, info.bits() as u32, c.len).is_some());
|
||
if info.compression != t::compression::NIKON || has_table || info.samples_per_pixel != 1 || !single {
|
||
return 0.0;
|
||
}
|
||
let darkest = samples.iter().copied().min().unwrap_or(0);
|
||
BE16_BLACK.min(f32::from(darkest))
|
||
}
|
||
|
||
/// Unpack a [`PackedLayout`] strip into `w * h` samples. Short input leaves the missing samples zero.
|
||
fn unpack_packed(src: &[u8], w: usize, h: usize, bits: u32, layout: PackedLayout) -> Vec<u16> {
|
||
use rayon::prelude::*;
|
||
let mut out = vec![0u16; w * h];
|
||
let stride = match layout {
|
||
PackedLayout::Lsb { stride } | PackedLayout::Chunked16 { stride } => stride,
|
||
PackedLayout::Be16 => w.saturating_mul(2),
|
||
};
|
||
out.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
|
||
let Some(line) = src.get(y * stride..) else { return };
|
||
let line = &line[..stride.min(line.len())];
|
||
match layout {
|
||
PackedLayout::Lsb { .. } => crate::unpack::unpack_lsb(line, bits, row),
|
||
PackedLayout::Be16 => {
|
||
let (words, _) = line.as_chunks::<2>();
|
||
for (dst, word) in row.iter_mut().zip(words) {
|
||
*dst = u16::from_be_bytes(*word) >> 4;
|
||
}
|
||
}
|
||
PackedLayout::Chunked16 { .. } => {
|
||
let mut samples = vec![0u16; line.len() / 16 * 10];
|
||
for (i, dst) in samples.chunks_mut(10).enumerate() {
|
||
if let Some(chunk) = line.get(i * 16..i * 16 + 15) {
|
||
crate::unpack::unpack_msb(chunk, 12, dst);
|
||
}
|
||
}
|
||
let n = row.len().min(samples.len());
|
||
row[..n].copy_from_slice(&samples[..n]);
|
||
}
|
||
}
|
||
});
|
||
out
|
||
}
|
||
|
||
/// Nikon Huffman-compressed strip (see [`crate::nefc`]); the decode table is maker note `0x0096`. Without a table the
|
||
/// strip may be packed uncompressed data (see [`PackedLayout`]).
|
||
fn compressed(bytes: &[u8], info: &ImageInfo, mn: Option<&makernote::MakerNote>, rows: usize, limits: &Limits) -> Result<RawData> {
|
||
let starts_with = |magic: &[u8]| {
|
||
let start = info.chunks(bytes.len() as u64).first().and_then(|c| usize::try_from(c.offset).ok());
|
||
start.and_then(|o| bytes.get(o..o.checked_add(magic.len())?)) == Some(magic)
|
||
};
|
||
// the maker note's own compression mode decides first: Z bodies label packed data 34713 and may still carry
|
||
// a 0x0096 table
|
||
let mode = nef_compression(mn);
|
||
if starts_with(&JPEG_XS_START) || matches!(mode, Some(13 | 14)) {
|
||
return Err(RawError::Unsupported(HIGH_EFFICIENCY.into()));
|
||
}
|
||
if let Some(depth) = match mode {
|
||
Some(6) => Some(12),
|
||
Some(10) => Some(14),
|
||
_ => None,
|
||
} {
|
||
if info.bits() as u32 != depth {
|
||
return Err(RawError::Unsupported(format!("Nikon packed {depth}-bit NEF with {}-bit samples", info.bits())));
|
||
}
|
||
return packed(bytes, info, rows, true, limits);
|
||
}
|
||
let Some(table) = mn.and_then(|m| Some((m.ifd.bytes(LINEARIZATION_TABLE)?, m.order))) else {
|
||
return packed(bytes, info, rows, false, limits);
|
||
};
|
||
if info.samples_per_pixel != 1 {
|
||
return Err(RawError::Unsupported(format!("Nikon compressed NEF with {} samples per pixel", info.samples_per_pixel)));
|
||
}
|
||
let bits = info.bits() as u32;
|
||
if u64::from(info.width).saturating_mul(u64::from(info.height)) > limits.samples() as u64 {
|
||
return Err(RawError::Limit("image too large"));
|
||
}
|
||
let table = nefc::parse_table(table.0, table.1, bits)?;
|
||
// one strip in every sample; several would be contiguous parts of the same bit stream
|
||
let chunks = info.chunks(bytes.len() as u64);
|
||
let (Some(first), Some(last)) = (chunks.first(), chunks.last()) else { return Err(RawError::Corrupt("NEF: no image data".into())) };
|
||
let end = last.offset.saturating_add(last.len).min(bytes.len() as u64);
|
||
let src = usize::try_from(first.offset).ok().zip(usize::try_from(end).ok()).and_then(|(a, b)| bytes.get(a..b));
|
||
let src = src.ok_or_else(|| RawError::Corrupt("NEF: image data outside the file".into()))?;
|
||
Ok(RawData::U16(nefc::decode_rows(src, info.width as usize, info.height as usize, rows, bits, &table)?))
|
||
}
|
||
|
||
/// A "compressed" strip of packed samples: the strip size must match a measured layout exactly. `stated` is set
|
||
/// when the maker note names the packed mode, which also allows 14-bit rows without padding.
|
||
fn packed(bytes: &[u8], info: &ImageInfo, rows: usize, stated: bool, limits: &Limits) -> Result<RawData> {
|
||
let (w, h) = (info.width as usize, info.height as usize);
|
||
let bits = info.bits() as u32;
|
||
let chunks = info.chunks(bytes.len() as u64);
|
||
let layout = match chunks.as_slice() {
|
||
[c] if info.samples_per_pixel == 1 => {
|
||
packed_layout(w, h, bits, c.len, stated).or_else(|| d100_layout(w, h, bits, c.len)).or_else(|| be16_layout(w, h, bits, c.len))
|
||
}
|
||
_ => None,
|
||
};
|
||
let (Some(layout), [c]) = (layout, chunks.as_slice()) else {
|
||
return Err(RawError::Unsupported("Nikon compressed NEF without a linearization table (maker note 0x96)".into()));
|
||
};
|
||
let src = craft_tiff::image::chunk_bytes(bytes, c)
|
||
.filter(|src| u64::try_from(src.len()).ok() == Some(c.len))
|
||
.ok_or_else(|| RawError::Corrupt("NEF: incomplete image strip".into()))?;
|
||
if (w as u64).saturating_mul(h as u64) > limits.samples() as u64 {
|
||
return Err(RawError::Limit("image too large"));
|
||
}
|
||
Ok(RawData::U16(unpack_packed(src, w, rows.min(h), bits, layout)))
|
||
}
|
||
|
||
/// Nikon small raw (sRAW), measured clean-room from the CC0 raw.pixls.us D810 (2036) and D4S (2227) samples. The raw SubIFD is labelled compression 34713,
|
||
/// 12 bits, one CFA sample per pixel, but it carries no linearization table (maker note `0x0096`) and its single strip
|
||
/// is exactly `3 * width * height` bytes. It holds a YCbCr 4:2:2 image of the IFD size, not a CFA mosaic (the CFA
|
||
/// tags are dummies):
|
||
///
|
||
/// - the strip is an LSB-first 12-bit stream (`s0 = b0 | (b1 & 0x0f) << 8`, `s1 = b1 >> 4 | b2 << 4`), a row is `3 * width`
|
||
/// bytes with no padding, i.e. `2 * width` samples;
|
||
/// - the samples come in groups of four, `[Y_left, Y_right, Cb, Cr]`, for the pixel pair `(2k, 2k + 1)`: chroma is
|
||
/// halved horizontally and not vertically;
|
||
/// - luma is linear and already black-subtracted (the darkest samples are near 0) and never uses the top two bits
|
||
/// (maxima 873 and 1004 of 4095), so the white level is 1023; chroma is centred on 2048 (fitted neutral 2045-2049).
|
||
///
|
||
/// The chroma is a camera colour-difference scale, not JFIF Cb/Cr. In linear terms the stored values behave as
|
||
/// `R - Y = R_GAIN * (Cr - 2048)` and `B - Y = B_GAIN * (Cb - 2048)` on the luma scale: regressing the camera JPEG's
|
||
/// linear `(R - Y) / Y` and `(B - Y) / Y` on `(Cr - 2048) / Y` and `(Cb - 2048) / Y` over mid-tone pixels gave slopes
|
||
/// 4.92 / 5.09 (D810, correlation 0.94 / 0.88) and 3.52 / 3.73 (D4S, 0.98 / 0.97); the gains below are the rounded
|
||
/// means. They are approximate (the two bodies differ by about 30 %), the luma is not tone-mapped, and the data is
|
||
/// treated as already white balanced (neutral D4S areas have `Cb`, `Cr` within 2 of 2048 and equal JPEG R, G, B). G
|
||
/// follows from the luma equation with the BT.601 weights. The chroma is upsampled horizontally with linear
|
||
/// interpolation, centred between the two pixels of a pair. Output is linear RGB scaled by 16 (to a 14-bit range),
|
||
/// like Sony's downsized ARWs.
|
||
const SMALL_RAW_WHITE: f32 = 1023.0;
|
||
const SMALL_RAW_SCALE: f32 = 16.0;
|
||
const SMALL_RAW_CHROMA_ZERO: f32 = 2048.0;
|
||
const SMALL_RAW_R_GAIN: f32 = 4.2;
|
||
const SMALL_RAW_B_GAIN: f32 = 4.4;
|
||
|
||
/// The strip of a small raw: compression 34713, 12 bits, no maker note `0x0096`, one strip of exactly
|
||
/// `3 * width * height` bytes (even width).
|
||
fn small_raw_strip(info: &ImageInfo, file_len: usize, mn: Option<&makernote::MakerNote>) -> Option<craft_tiff::image::Chunk> {
|
||
if info.compression != t::compression::NIKON || info.bits() != 12 || info.samples_per_pixel != 1 {
|
||
return None;
|
||
}
|
||
if mn.is_some_and(|m| m.ifd.bytes(LINEARIZATION_TABLE).is_some()) {
|
||
return None;
|
||
}
|
||
let (w, h) = (info.width as u64, info.height as u64);
|
||
let chunks = info.chunks(file_len as u64);
|
||
match chunks.as_slice() {
|
||
[c] if w.is_multiple_of(2) && w > 0 && h > 0 && w.checked_mul(h).and_then(|n| n.checked_mul(3)) == Some(c.len) => Some(*c),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// Decode one row of a small raw (`3 * width` bytes) into `3 * width` linear RGB values on the 14-bit scale.
|
||
/// Short input reads as zero bytes.
|
||
fn small_raw_row(src: &[u8], w: usize, out: &mut [u16]) {
|
||
let Some(n) = w.checked_mul(3).filter(|n| *n <= out.len()) else { return };
|
||
if w == 0 || !w.is_multiple_of(2) {
|
||
return;
|
||
}
|
||
let mut s = vec![0u16; 2 * w];
|
||
crate::unpack::unpack_lsb(&src[..src.len().min(n)], 12, &mut s);
|
||
let pairs = w / 2;
|
||
let chroma = |k: usize| {
|
||
let g = &s[4 * k + 2..4 * k + 4];
|
||
(f32::from(g[0]) - SMALL_RAW_CHROMA_ZERO, f32::from(g[1]) - SMALL_RAW_CHROMA_ZERO)
|
||
};
|
||
for k in 0..pairs {
|
||
let (cb, cr) = chroma(k);
|
||
for side in 0..2 {
|
||
let (nb, nr) = if side == 0 { chroma(k.saturating_sub(1)) } else { chroma((k + 1).min(pairs - 1)) };
|
||
let (u, v) = (0.75 * cb + 0.25 * nb, 0.75 * cr + 0.25 * nr);
|
||
let y = f32::from(s[4 * k + side]);
|
||
let r = y + SMALL_RAW_R_GAIN * v;
|
||
let b = y + SMALL_RAW_B_GAIN * u;
|
||
let g = (y - 0.299 * r - 0.114 * b) / 0.587;
|
||
let dst = (2 * k + side) * 3;
|
||
for (i, value) in [r, g, b].into_iter().enumerate() {
|
||
out[dst + i] = (value * SMALL_RAW_SCALE).round().clamp(0.0, 65535.0) as u16;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn small_raw(bytes: &[u8], tiff: &Tiff, info: &ImageInfo, strip: craft_tiff::image::Chunk, mode: Mode, limits: &Limits) -> Result<RawImage> {
|
||
use rayon::prelude::*;
|
||
let (w, h) = (info.width as usize, info.height as usize);
|
||
let stride = w.checked_mul(3).ok_or(RawError::Limit("image too large"))?;
|
||
let count = stride.checked_mul(h).filter(|n| *n > 0 && *n <= limits.samples()).ok_or(RawError::Limit("image too large"))?;
|
||
let src = craft_tiff::image::chunk_bytes(bytes, &strip)
|
||
.filter(|src| u64::try_from(src.len()).ok() == Some(strip.len))
|
||
.ok_or_else(|| RawError::Corrupt("NEF: incomplete image strip".into()))?;
|
||
let data = match mode {
|
||
Mode::Full => {
|
||
let mut data = vec![0u16; count];
|
||
data.par_chunks_mut(stride).enumerate().for_each(|(y, out)| {
|
||
small_raw_row(src.get(y * stride..).unwrap_or(&[]), w, out);
|
||
});
|
||
data
|
||
}
|
||
Mode::Header => Vec::new(),
|
||
};
|
||
let img = RawImage {
|
||
format: RawFormat::Nef,
|
||
width: w,
|
||
height: h,
|
||
cpp: 3,
|
||
data: RawData::U16(data),
|
||
cfa: None,
|
||
bits: 14,
|
||
black: BlackLevel::uniform(0.0),
|
||
white: vec![SMALL_RAW_WHITE * SMALL_RAW_SCALE],
|
||
active_area: Rect::new(0, 0, w, h),
|
||
crop: Rect::new(0, 0, w, h),
|
||
orientation: Orientation::from_exif(tiff.ifds.first().and_then(|i| i.u16(t::ORIENTATION)).unwrap_or(1)),
|
||
color: ColorData::default(),
|
||
// the stored colour is already white balanced (see above)
|
||
wb_multipliers: Some([1.0; 3]),
|
||
linearized: false,
|
||
pixel_aspect: 1.0,
|
||
opcodes: OpcodeLists::default(),
|
||
camera: tiff.ifds.first().map(Camera::from_ifd0).unwrap_or_default(),
|
||
cinema: Default::default(),
|
||
lens_metadata: Default::default(),
|
||
level_sources: Default::default(),
|
||
};
|
||
img.validate_for(mode)?;
|
||
Ok(img)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use craft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter, Value};
|
||
|
||
fn nef(compression: u16, bits: u16, strips: Vec<Vec<u8>>, w: u32, h: u32, rps: u32) -> Vec<u8> {
|
||
nef_with_note(compression, bits, strips, w, h, rps, None)
|
||
}
|
||
|
||
/// [`nef`] with an optional maker note (`Nikon\0` v2 header + embedded big-endian TIFF holding `note`).
|
||
fn nef_with_note(compression: u16, bits: u16, strips: Vec<Vec<u8>>, w: u32, h: u32, rps: u32, note: Option<IfdBuilder>) -> Vec<u8> {
|
||
nef_in(ByteOrder::Big, compression, bits, strips, w, h, rps, note)
|
||
}
|
||
|
||
/// [`nef_with_note`] in either byte order (file and maker note).
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn nef_in(order: ByteOrder, compression: u16, bits: u16, strips: Vec<Vec<u8>>, w: u32, h: u32, rps: u32, note: Option<IfdBuilder>) -> Vec<u8> {
|
||
nef_model_in(order, "NIKON TEST", compression, bits, strips, w, h, rps, note)
|
||
}
|
||
|
||
/// [`nef_in`] from camera `model`.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn nef_model_in(
|
||
order: ByteOrder,
|
||
model: &str,
|
||
compression: u16,
|
||
bits: u16,
|
||
strips: Vec<Vec<u8>>,
|
||
w: u32,
|
||
h: u32,
|
||
rps: u32,
|
||
note: Option<IfdBuilder>,
|
||
) -> Vec<u8> {
|
||
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![compression]));
|
||
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![2, 1, 1, 0]));
|
||
raw.set_image(ImageData::Strips { rows_per_strip: rps, strips });
|
||
let mut ifd0 = IfdBuilder::new();
|
||
ifd0.set(t::MAKE, Value::Ascii("NIKON CORPORATION".into()));
|
||
ifd0.set(t::MODEL, Value::Ascii(model.into()));
|
||
if let Some(mn) = note {
|
||
let mut bytes = b"Nikon\0\x02\x10\0\0".to_vec();
|
||
bytes.extend(TiffWriter::new(order, false).write(&[mn]).unwrap());
|
||
let mut exif = IfdBuilder::new();
|
||
exif.set(t::MAKER_NOTE, Value::Undefined(bytes));
|
||
ifd0.set_child(t::EXIF_IFD, exif);
|
||
}
|
||
ifd0.add_sub_ifd(raw);
|
||
TiffWriter::new(order, false).write(&[ifd0]).unwrap()
|
||
}
|
||
|
||
/// A Z 6II NEF carrying NikonNEFInfo gets Nikon's distortion correction as one warp, in the header probe and
|
||
/// the full decode alike, and keeps it through DNG export; another body with the same data gets none.
|
||
#[test]
|
||
fn nikon_in_file_distortion_becomes_a_warp() {
|
||
use crate::nef_lens::tests::{distortion_block, nef_info_blob, offset_block};
|
||
let (w, h) = (48u32, 32u32);
|
||
let file = |model: &str| {
|
||
let words: Vec<u8> = (0..w * h).flat_map(|i| (600 + (i % 97) as u16 * 50).to_be_bytes()).collect();
|
||
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![14]));
|
||
raw.set(t::COMPRESSION, Value::Short(vec![1]));
|
||
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]));
|
||
let info = nef_info_blob(&[
|
||
(0x0005, distortion_block(3, 21.6, 4, [0.039895, 0.019211, -0.127218], -0.025043)),
|
||
(0x0008, offset_block(1.5, -0.75)),
|
||
]);
|
||
raw.set(0xc7d5, Value::Undefined(info));
|
||
raw.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![words] });
|
||
let mut ifd0 = IfdBuilder::new();
|
||
ifd0.set(t::MAKE, Value::Ascii("NIKON CORPORATION".into()));
|
||
ifd0.set(t::MODEL, Value::Ascii(model.into()));
|
||
ifd0.add_sub_ifd(raw);
|
||
TiffWriter::new(ByteOrder::Little, false).write(&[ifd0]).unwrap()
|
||
};
|
||
let bytes = file("NIKON Z 6_2");
|
||
let header = decode(&bytes, Mode::Header, &Limits::default()).unwrap();
|
||
let full = decode(&bytes, Mode::Full, &Limits::default()).unwrap();
|
||
assert_eq!(header.info(), full.info());
|
||
assert_eq!(header.opcodes.list3, full.opcodes.list3);
|
||
let [crate::Opcode::WarpRectilinear { planes, center }] = full.opcodes.list3.as_slice() else { panic!("{:?}", full.opcodes.list3) };
|
||
assert!((planes[0][0] - 1.0 / (1.0 - 0.025043)).abs() < 1e-5, "{planes:?}");
|
||
assert!((center[0] * 47.0 - (23.5 + 1.5)).abs() < 0.01 && (center[1] * 31.0 - (15.5 - 0.75)).abs() < 0.01, "{center:?}");
|
||
#[cfg(feature = "dng")]
|
||
{
|
||
let dng = crate::write_dng(&full, &Default::default()).unwrap();
|
||
assert_eq!(crate::decode(&dng).unwrap().opcodes.list3, full.opcodes.list3);
|
||
}
|
||
assert!(decode(&file("NIKON D850"), Mode::Full, &Limits::default()).unwrap().opcodes.list3.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn preview_color_space_is_read_from_the_nikon_note() {
|
||
for (value, expected) in [(1, Some(crate::PreviewColorSpace::Srgb)), (2, Some(crate::PreviewColorSpace::AdobeRgb)), (3, None)] {
|
||
let mut mn = IfdBuilder::new();
|
||
mn.set(0x001e, Value::Short(vec![value]));
|
||
let bytes = nef_with_note(1, 14, vec![vec![0; 128]], 8, 8, 8, Some(mn));
|
||
assert_eq!(crate::embedded_preview_color_space(&bytes), expected);
|
||
}
|
||
assert_eq!(crate::embedded_preview_color_space(&nef(1, 14, vec![vec![0; 128]], 8, 8, 8)), None);
|
||
assert_eq!(crate::embedded_preview_color_space(b"not a TIFF"), None);
|
||
}
|
||
|
||
/// storytold/lightcraft#778: any make's raw falls back to its EXIF ColorSpace, with Uncalibrated + InteropIndex "R03" read as
|
||
/// Adobe RGB; the Nikon note wins; a DNG is left alone.
|
||
#[test]
|
||
fn preview_color_space_falls_back_to_the_exif_color_space() {
|
||
let file = |make: &str, cs: u16, interop: Option<&str>, note: Option<u16>, dng: bool| {
|
||
let mut exif = IfdBuilder::new();
|
||
exif.set(0xA001, Value::Short(vec![cs]));
|
||
if let Some(i) = interop {
|
||
let mut io = IfdBuilder::new();
|
||
io.set(0x0001, Value::Ascii(i.into()));
|
||
exif.set_child(t::INTEROP_IFD, io);
|
||
}
|
||
if let Some(v) = note {
|
||
let mut mn = IfdBuilder::new();
|
||
mn.set(0x001e, Value::Short(vec![v]));
|
||
let mut bytes = b"Nikon |