mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
craft-raw: Hasselblad 3FR and FFF (feature hasselblad) (#867)
* 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.
This commit is contained in:
1 parent
7f0f352d21
commit
feb941cc19
9 files changed
+512
-11
No files matched your search
@@ -91,12 +91,14 @@ Source provenance
|
||||
|
||||
crates/raw (craft-raw) is moved from LightCraft, with behaviour ported from two other readers:
|
||||
- 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 and their tests),
|
||||
opcodes.rs, profile.rs, gaintable.rs, semantic.rs, preview.rs, vendor/crw.rs, vendor/hasselblad.rs
|
||||
and their tests),
|
||||
revision 5ff567a4a2e067bef1b6bce152c11e7f727ccbaf.
|
||||
https://github.com/storytold/lightcraft
|
||||
Copyright (c) 2026 ArtCraft Team and the LightCraft contributors.
|
||||
- PhotoCraft, crates/raw/src/dng.rs and tests/it/dng.rs (default-crop defaults, white-level
|
||||
fallbacks, the 8-bit rule for lossy JPEG), revision c1b22bdaae2a1359ba3d164eca4d4b0c7378b547.
|
||||
- PhotoCraft, crates/raw/src/dng.rs, hasselblad.rs and tests/it/dng.rs (default-crop defaults,
|
||||
white-level fallbacks, the 8-bit rule for lossy JPEG, Hasselblad's per-site black levels),
|
||||
revision c1b22bdaae2a1359ba3d164eca4d4b0c7378b547.
|
||||
https://github.com/storytold/photocraft
|
||||
Copyright (c) 2026 ArtCraft Team and the PhotoCraft contributors.
|
||||
- FilmCraft, crates/dng/src/lib.rs (the CinemaDNG frame tags and time code), revision
|
||||
|
||||
@@ -87,8 +87,8 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
|
||||
- [`craft-raw`](crates/raw/README.md): camera raw files, the file-format layer, from LightCraft with
|
||||
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 and
|
||||
Canon CRW (through `craft-crw`) so far; demosaicing and everything after stay with the develop
|
||||
opcode lists, embedded previews), one Cargo feature per format. DNG reading and writing,
|
||||
Hasselblad 3FR/FFF and Canon CRW (through `craft-crw`) so far; demosaicing and everything after stay with the develop
|
||||
pipeline.
|
||||
- [`craft-tiff`](crates/tiff/README.md): TIFF and BigTIFF IFD reader and writer, from LightCraft:
|
||||
every field type, the IFD chain, SubIFDs and Exif/GPS/Interop pointer IFDs, maker-note IFDs
|
||||
|
||||
@@ -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) 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 and Canon CRW so far."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -11,11 +11,13 @@ repository.workspace = true
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["dng", "jxl", "crw"]
|
||||
default = ["dng", "jxl", "crw", "hasselblad"]
|
||||
## 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).
|
||||
jxl = ["dng", "dep:jxl-oxide"]
|
||||
## Hasselblad 3FR / FFF: the DNG-layout reader plus the embedded 3FR header and the RGB preview.
|
||||
hasselblad = ["dng"]
|
||||
## Canon CRW (CIFF) through craft-crw, the one CRW decoder.
|
||||
crw = ["dep:craft-crw", "dep:chrono"]
|
||||
|
||||
|
||||
@@ -53,10 +53,11 @@ One module and one Cargo feature per maker or container family. All are on by de
|
||||
| `dng` | Adobe DNG | Uncompressed at 1 to 16 bits (packed MSB-first, or 8/16-bit in file byte order), lossless JPEG (all seven predictors, 1 to 4 components, point transform, restart intervals), lossy JPEG (compression 34892, 8-bit baseline tiles or strips), Deflate with predictors 1, 2 and the floating-point predictors (16, 24 and 32-bit floats), strips and tiles, planar or interleaved, CFA of any period up to 16 × 16 (Bayer, X-Trans) and LinearRaw with 1, 3 or 4 samples, DNG 1.7 row and column interleave, reduced-resolution LinearRaw copies (`decode_reduced`), semantic masks, CinemaDNG frame tags. Writing: uncompressed, lossless JPEG tiles, Deflate tiles (integer, half and single floats) |
|
||||
| `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 |
|
||||
| `hasselblad` | Hasselblad 3FR and FFF, and other CFA TIFFs in the DNG layout without a DNG version tag (`RawFormat::CfaTiff`) | Uncompressed and Hasselblad's lossless JPEG (predictor selection value 8); RGGB from the sensor origin when the file states no `CFAPattern`; an FFF's colour matrix from the 3FR header it embeds; the 8-bit RGB preview handed on as a baseline TIFF. Implies `dng` |
|
||||
|
||||
Not decoded: GoPro VC-5 DNG (GPR), CFA with more than three colours, non-rectangular CFA layouts.
|
||||
`probe` names every container it can tell apart whatever the features (CR2, CR3, NEF, NRW, ARW,
|
||||
RAF, ORF, RW2, PEF, SRW, MRW, X3F, DNG-layout CFA TIFF such as 3FR/FFF, other TIFF-based raws);
|
||||
RAF, ORF, RW2, PEF, SRW, MRW, X3F, other TIFF-based raws);
|
||||
their decoders are still in the apps and move here one maker family at a time.
|
||||
`RawFormat::is_supported` says what this build decodes.
|
||||
|
||||
@@ -92,6 +93,7 @@ the three supported was kept:
|
||||
| PhotoCraft, FilmCraft | A lossy tile's JPEG must cover the part of its tile inside the image | a smaller JPEG left the rest of the tile black |
|
||||
| FilmCraft | CinemaDNG `FrameRate`, `TimeCodes` and `ReelName` (`RawImage::cinema`), read and written | not read |
|
||||
| craft-crw | A big-endian (`MM`) CIFF header is a CRW | `probe` knew only `II` |
|
||||
| PhotoCraft | Hasselblad: four `BlackLevel` values without `BlackLevelRepeatDim` are one per site of the 2 × 2 CFA (files that are not DNGs only) | the first value was used for every site |
|
||||
|
||||
Left as LightCraft has it, with the reason:
|
||||
|
||||
@@ -101,6 +103,11 @@ Left as LightCraft has it, with the reason:
|
||||
- An edge tile's lossless or lossy JPEG smaller than the nominal tile is accepted when it covers
|
||||
the rows inside the image. PhotoCraft and FilmCraft refuse any size mismatch; LightCraft's
|
||||
tolerance is for writers that encode edge tiles at their visible size.
|
||||
- A Hasselblad-layout file without `AsShotNeutral` is not claimed (`OtherTiff`, preview only):
|
||||
nothing in it says how to read its colour. PhotoCraft decodes any Hasselblad CFA IFD and measures
|
||||
a missing white level from the samples, which is develop-side work. PhotoCraft refuses the
|
||||
lossless-JPEG files; LightCraft's selection-value-8 decoder reads them. Both measured the same
|
||||
CFA phase (red at the sensor origin) on different bodies (H4D-40, X2D II 100C).
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -333,6 +333,13 @@ fn decode_ifd(bytes: &[u8], tiff: &Tiff, pixels: &Ifd, main: &Ifd, mode: Mode, f
|
||||
// (a FLOAT / DOUBLE tag can hold NaN or infinity: such a level reads as 0, in its place)
|
||||
let level = |v: f64| Some(v as f32).filter(|v| v.is_finite()).unwrap_or(0.0);
|
||||
let mut values: Vec<f32> = levels(t::BLACK_LEVEL).f64s(t::BLACK_LEVEL).unwrap_or_default().into_iter().map(level).collect();
|
||||
// A TIFF that is not a DNG may state four black levels without `BlackLevelRepeatDim`: one per site of its 2 × 2
|
||||
// CFA, row-major (PhotoCraft's reading of Hasselblad files). In a DNG the count has to match the repeat.
|
||||
let (br, bc) = if format != RawFormat::Dng && cpp == 1 && values.len() == 4 && !levels(t::BLACK_LEVEL).contains(t::BLACK_LEVEL_REPEAT_DIM) {
|
||||
(2, 2)
|
||||
} else {
|
||||
(br, bc)
|
||||
};
|
||||
if values.len() != br * bc * cpp {
|
||||
values = vec![values.first().copied().unwrap_or(0.0)];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
//! Hasselblad 3FR and FFF raws ([`crate::RawFormat::CfaTiff`]).
|
||||
//!
|
||||
//! Sources: TIFF 6.0 (container), the DNG Specification (the tags these files borrow) and black-box analysis, done
|
||||
//! for LightCraft, of a CC0 H4D-40 FFF from raw.pixls.us (931, "Hasselblad - H4D-40 - 16bit (4:3).fff"); every
|
||||
//! number below was measured on it. PhotoCraft's reader measured the same layout independently on an X2D II 100C
|
||||
//! (red at the data origin, against the camera's embedded JPEG).
|
||||
//!
|
||||
//! - Both are TIFFs without a DNG version tag that describe their raw image with DNG's tags: a full-resolution
|
||||
//! CFA sub-IFD with `BlackLevel`, `WhiteLevel`, `DefaultCropOrigin` / `DefaultCropSize`, and `AsShotNeutral`.
|
||||
//! [`crate::dng::decode_as`] reads all of that; this module adds what that reader does not know about.
|
||||
//! - The raw image is uncompressed (X1D, X1D II, X2D, CFV 100C) or Hasselblad's lossless JPEG with predictor
|
||||
//! selection value 8 ([`crate::ljpeg::decode_hasselblad`]); the H4D-40 FFF's 7410 x 5586 strip is the latter.
|
||||
//! - No `CFAPattern`. The layout is RGGB from the sensor origin: averaged over the 6 x 6 sensor blocks behind each
|
||||
//! pixel of the file's 1217 x 913 RGB IFD0 image (the default crop is exactly six times its size), the sites at
|
||||
//! (0, 0) correlate 0.976 with its red and 0.726 with its blue, the sites at (1, 1) 0.896 with blue and 0.847
|
||||
//! with red, and the two green sites' diagonal differences are half those of the other pair.
|
||||
//! - An FFF is a 3FR converted on the desktop. It has no colour matrix of its own: the camera's matrix stays in
|
||||
//! the original 3FR header, which the FFF carries whole as a little-endian TIFF in the private IFD0 tag
|
||||
//! `0xC51B` (4096 bytes in the sample: an IFD0 with Make, Model, `ColorMatrix1` and `AsShotNeutral`, a
|
||||
//! `SubIFDs` pair for the raw and preview images, whose data offsets point into the old 3FR and are not read).
|
||||
//! [`decode`] takes the matrices from there when the outer file has none. The white balance stays the outer
|
||||
//! file's, which is the one the converter last saved.
|
||||
//! - No JPEG preview. IFD0 is an uncompressed 8-bit RGB image (1217 x 913 in the H4D-40 FFF, IFD1 pointing at the
|
||||
//! same strip; 218 x 218 in the CFV 3FR, under the file's own `Orientation`); the FFF also has a 16-bit RGB
|
||||
//! copy in a `SubIFDs` child, with a `TransferFunction` whose meaning is not known, so it is not used.
|
||||
//! [`rgb_preview`] hands the largest 8-bit one on as a small baseline TIFF.
|
||||
use crate::dng;
|
||||
use crate::{ColorData, Limits, Mode, RawFormat, RawImage, Result};
|
||||
use craft_tiff::image::{Layout, chunk_bytes};
|
||||
use craft_tiff::{ByteOrder, IfdBuilder, ImageData, ParseOptions, Tiff, TiffWriter, Value, tags as t};
|
||||
|
||||
/// Private IFD0 tag holding the original 3FR header (a complete TIFF) inside an FFF.
|
||||
pub(crate) const ORIGINAL_HEADER: u16 = 0xc51b;
|
||||
|
||||
/// Upper bound on the embedded header's size: the sample's is 4 KiB; a larger block is not a header.
|
||||
const MAX_HEADER: usize = 1 << 20;
|
||||
|
||||
/// Decode a 3FR / FFF (or another TIFF raw in the DNG layout, see [`dng::is_plain_cfa_tiff`]).
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawImage> {
|
||||
let mut img = dng::decode_as(bytes, mode, RawFormat::CfaTiff, limits)?;
|
||||
if !has_matrix(&img.color)
|
||||
&& let Some(header) = original_header_color(bytes)
|
||||
{
|
||||
adopt_matrices(&mut img.color, &header);
|
||||
}
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
/// The colour tags of the original 3FR header an FFF embeds in [`ORIGINAL_HEADER`], when it has a colour matrix.
|
||||
fn original_header_color(bytes: &[u8]) -> Option<ColorData> {
|
||||
let outer = Tiff::parse(bytes).ok()?;
|
||||
let block = outer.ifds.first()?.bytes(ORIGINAL_HEADER)?;
|
||||
if block.len() > MAX_HEADER {
|
||||
return None;
|
||||
}
|
||||
let opts = ParseOptions { max_ifds: 16, max_depth: 2, ..Default::default() };
|
||||
let header = Tiff::parse_with(block, &opts).ok()?;
|
||||
let ifd0 = header.ifds.first()?;
|
||||
let raw = dng::raw_ifd(&header).unwrap_or(ifd0);
|
||||
let color = dng::color_data(ifd0, raw, header.order);
|
||||
has_matrix(&color).then_some(color)
|
||||
}
|
||||
|
||||
/// Whether the colour tags hold a camera matrix.
|
||||
fn has_matrix(color: &ColorData) -> bool {
|
||||
color.color_matrix.iter().any(Option::is_some)
|
||||
}
|
||||
|
||||
/// Upper bound on a preview's pixel bytes (8-bit RGB): 64 MP, far above the 1.1 MP of the largest one seen.
|
||||
const MAX_PREVIEW_BYTES: usize = (64 << 20) * 3;
|
||||
|
||||
/// The file's largest uncompressed 8-bit RGB image (chunky, in strips), as a little-endian baseline TIFF holding
|
||||
/// just those pixels, which any TIFF reader decodes. No `Orientation` tag is written: the viewer
|
||||
/// applies the raw file's, which is the one that stands over this image in IFD0.
|
||||
pub(crate) fn rgb_preview(tiff: &Tiff, bytes: &[u8]) -> Option<Vec<u8>> {
|
||||
let info = tiff
|
||||
.all_ifds()
|
||||
.into_iter()
|
||||
.filter_map(|i| i.image().ok())
|
||||
.filter(|i| {
|
||||
i.photometric == t::photometric::RGB
|
||||
&& i.compression == t::compression::NONE
|
||||
&& i.samples_per_pixel == 3
|
||||
&& i.bits_per_sample.iter().all(|&b| b == 8)
|
||||
&& i.planar == 1
|
||||
&& i.sample_format == 1
|
||||
&& matches!(i.layout, Layout::Strips { .. })
|
||||
&& i.width > 0
|
||||
&& i.height > 0
|
||||
})
|
||||
.max_by_key(|i| u64::from(i.width) * u64::from(i.height))?;
|
||||
let (w, h) = (info.width as usize, info.height as usize);
|
||||
let size = w.checked_mul(h)?.checked_mul(3)?;
|
||||
if size > MAX_PREVIEW_BYTES {
|
||||
return None;
|
||||
}
|
||||
let mut pixels = Vec::with_capacity(size);
|
||||
for chunk in info.chunks(bytes.len() as u64) {
|
||||
pixels.extend_from_slice(chunk_bytes(bytes, &chunk)?);
|
||||
if pixels.len() >= size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// a short last strip is a truncated file: no preview rather than a picture with a black band
|
||||
if pixels.len() < size {
|
||||
return None;
|
||||
}
|
||||
pixels.truncate(size);
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![info.width]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![info.height]));
|
||||
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![8, 8, 8]));
|
||||
ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![3]));
|
||||
ifd.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::RGB]));
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![t::compression::NONE]));
|
||||
ifd.set(t::PLANAR_CONFIGURATION, Value::Short(vec![1]));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: info.height, strips: vec![pixels] });
|
||||
TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).ok()
|
||||
}
|
||||
|
||||
/// Take the camera matrices (and the illuminants they belong to) of `from`; the rest of `color` stays.
|
||||
fn adopt_matrices(color: &mut ColorData, from: &ColorData) {
|
||||
color.illuminant = from.illuminant;
|
||||
color.color_matrix = from.color_matrix;
|
||||
color.forward_matrix = from.forward_matrix;
|
||||
color.camera_calibration = from.camera_calibration;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{RawData, RawError, decode as decode_file, probe, probe_info};
|
||||
use craft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter, Value, tags as t};
|
||||
|
||||
const MATRIX: [(i32, i32); 9] =
|
||||
[(686, 1000), (-196, 1000), (-32, 1000), (-437, 1000), (1228, 1000), (235, 1000), (-165, 1000), (317, 1000), (675, 1000)];
|
||||
|
||||
fn rgb_ifd() -> IfdBuilder {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![1]));
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![4]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![3]));
|
||||
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![8, 8, 8]));
|
||||
ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![3]));
|
||||
ifd.set(t::PHOTOMETRIC, Value::Short(vec![2]));
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![1]));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: 3, strips: vec![(0..36).collect()] });
|
||||
ifd
|
||||
}
|
||||
|
||||
/// The camera's 3FR header as an FFF embeds it: a little-endian TIFF whose IFD0 has the colour matrix.
|
||||
fn original_header(matrix: bool) -> Vec<u8> {
|
||||
let mut ifd0 = rgb_ifd();
|
||||
ifd0.set(t::MAKE, Value::Ascii("Hasselblad".into()));
|
||||
ifd0.set(t::MODEL, Value::Ascii("Hasselblad H4D".into()));
|
||||
if matrix {
|
||||
ifd0.set(t::COLOR_MATRIX_1, Value::SRational(MATRIX.to_vec()));
|
||||
}
|
||||
ifd0.set(t::AS_SHOT_NEUTRAL, Value::Rational(vec![(43, 100), (1, 1), (79, 100)]));
|
||||
TiffWriter::new(ByteOrder::Little, false).write(&[ifd0]).unwrap()
|
||||
}
|
||||
|
||||
/// An FFF in miniature: big-endian, IFD0 an RGB image with `AsShotNeutral` and the embedded header `header`,
|
||||
/// a 16-bit uncompressed CFA sub-IFD of 8 x 6 samples, no colour matrix of its own unless `own_matrix`.
|
||||
fn fff(header: Option<Vec<u8>>, own_matrix: bool) -> Vec<u8> {
|
||||
fff_with(header, own_matrix, vec![])
|
||||
}
|
||||
|
||||
/// [`fff`] with more `SubIFDs` children of IFD0 (other preview images).
|
||||
fn fff_with(header: Option<Vec<u8>>, own_matrix: bool, extra: Vec<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![8]));
|
||||
raw.set(t::IMAGE_LENGTH, Value::Long(vec![6]));
|
||||
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
|
||||
raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
|
||||
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
|
||||
raw.set(t::COMPRESSION, Value::Short(vec![1]));
|
||||
raw.set(t::BLACK_LEVEL, Value::Rational(vec![(256, 1)]));
|
||||
raw.set(t::WHITE_LEVEL, Value::Short(vec![65535]));
|
||||
raw.set_image(ImageData::Strips { rows_per_strip: 6, strips: vec![(0..96u8).collect()] });
|
||||
let mut ifd0 = rgb_ifd();
|
||||
ifd0.set(t::MAKE, Value::Ascii("Hasselblad".into()));
|
||||
ifd0.set(t::MODEL, Value::Ascii("Hasselblad H4D-40".into()));
|
||||
ifd0.set(t::AS_SHOT_NEUTRAL, Value::Rational(vec![(2, 5), (1, 1), (4, 5)]));
|
||||
if own_matrix {
|
||||
ifd0.set(t::COLOR_MATRIX_1, Value::SRational(vec![(1, 1), (0, 1), (0, 1), (0, 1), (1, 1), (0, 1), (0, 1), (0, 1), (1, 1)]));
|
||||
}
|
||||
if let Some(h) = header {
|
||||
ifd0.set(ORIGINAL_HEADER, Value::Undefined(h));
|
||||
}
|
||||
ifd0.add_sub_ifd(raw);
|
||||
for e in extra {
|
||||
ifd0.add_sub_ifd(e);
|
||||
}
|
||||
TiffWriter::new(ByteOrder::Big, false).write(&[ifd0]).unwrap()
|
||||
}
|
||||
|
||||
fn matrix_row0(c: &ColorData) -> Option<[f64; 3]> {
|
||||
c.color_matrix[0].map(|m| m[0])
|
||||
}
|
||||
|
||||
/// The FFF's own tags give the samples and white balance; the colour matrix comes from the 3FR header it
|
||||
/// embeds, so a developer has the camera's matrix and needs no fallback colour.
|
||||
#[test]
|
||||
fn fff_takes_the_colour_matrix_from_the_embedded_3fr_header() {
|
||||
let bytes = fff(Some(original_header(true)), false);
|
||||
assert_eq!(probe(&bytes), Some(RawFormat::CfaTiff));
|
||||
let r = decode_file(&bytes).unwrap();
|
||||
assert_eq!(r.data, RawData::U16((0..48u16).map(|i| u16::from_be_bytes([2 * i as u8, 2 * i as u8 + 1])).collect()));
|
||||
assert_eq!(r.cfa.as_ref().unwrap().name(), "RGGB");
|
||||
assert_eq!(matrix_row0(&r.color), Some([0.686, -0.196, -0.032]));
|
||||
assert_eq!(r.color.as_shot_neutral, Some([0.4, 1.0, 0.8]), "the outer file's white balance");
|
||||
// the import probe says the same
|
||||
assert_eq!(matrix_row0(&probe_info(&bytes).unwrap().color), Some([0.686, -0.196, -0.032]));
|
||||
}
|
||||
|
||||
/// A matrix of the file's own wins; without one anywhere (no header, or a header without a matrix) the
|
||||
/// file still decodes, with the fallback colour.
|
||||
#[test]
|
||||
fn own_matrix_wins_and_a_missing_one_is_not_an_error() {
|
||||
let own = decode_file(&fff(Some(original_header(true)), true)).unwrap();
|
||||
assert_eq!(matrix_row0(&own.color), Some([1.0, 0.0, 0.0]));
|
||||
for header in [None, Some(original_header(false)), Some(b"not a tiff".to_vec())] {
|
||||
let r = decode_file(&fff(header, false)).unwrap();
|
||||
assert!(!has_matrix(&r.color));
|
||||
}
|
||||
}
|
||||
|
||||
/// A truncated or damaged file never panics: an error, or a decode.
|
||||
#[test]
|
||||
fn damaged_files_do_not_panic() {
|
||||
let bytes = fff(Some(original_header(true)), false);
|
||||
for n in (0..bytes.len()).step_by(7) {
|
||||
let _ = decode_file(&bytes[..n]);
|
||||
let _ = probe_info(&bytes[..n]);
|
||||
let _ = crate::embedded_preview(&bytes[..n]);
|
||||
}
|
||||
let mut header = original_header(true);
|
||||
for i in (8..header.len()).step_by(3) {
|
||||
header[i] ^= 0x5a;
|
||||
match decode_file(&fff(Some(header.clone()), false)) {
|
||||
Ok(_) | Err(RawError::Corrupt(_) | RawError::Tiff(_) | RawError::Unsupported(_) | RawError::Limit(_) | RawError::NotRaw) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `(width, height, pixels, has an Orientation tag)` of a wrapped preview.
|
||||
fn wrapped(tiff: &[u8]) -> (u32, u32, Vec<u8>, bool) {
|
||||
let t = Tiff::parse(tiff).unwrap();
|
||||
let ifd = &t.ifds[0];
|
||||
let info = ifd.image().unwrap();
|
||||
let chunks = info.chunks(tiff.len() as u64);
|
||||
let pixels = chunks.iter().flat_map(|c| chunk_bytes(tiff, c).unwrap().to_vec()).collect();
|
||||
(info.width, info.height, pixels, ifd.contains(t::ORIENTATION))
|
||||
}
|
||||
|
||||
/// No JPEG in the file: the 8-bit RGB IFD0 comes out as a baseline TIFF of the same pixels, without an
|
||||
/// orientation of its own (the raw file's applies).
|
||||
#[test]
|
||||
fn rgb_ifd0_is_the_embedded_preview() {
|
||||
let bytes = fff(Some(original_header(true)), false);
|
||||
let preview = crate::embedded_preview(&bytes).expect("a preview");
|
||||
assert_eq!(wrapped(&preview), (4, 3, (0..36).collect(), false));
|
||||
assert_eq!(crate::embedded_preview_covering(&bytes, 2000), Some(preview));
|
||||
}
|
||||
|
||||
/// The largest 8-bit RGB image wins; a bigger 16-bit one (the FFF's copy with a `TransferFunction`) is not
|
||||
/// used, and nor is a planar or compressed one.
|
||||
#[test]
|
||||
fn only_8_bit_chunky_uncompressed_rgb_is_taken() {
|
||||
let image = |w: u32, h: u32, bits: u16, planar: u16, compression: u16| {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![1]));
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
|
||||
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![bits; 3]));
|
||||
ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![3]));
|
||||
ifd.set(t::PHOTOMETRIC, Value::Short(vec![2]));
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![compression]));
|
||||
ifd.set(t::PLANAR_CONFIGURATION, Value::Short(vec![planar]));
|
||||
let n = (w * h * 3 * u32::from(bits / 8)) as usize;
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![vec![7; n]] });
|
||||
ifd
|
||||
};
|
||||
let bytes = fff_with(None, false, vec![image(16, 12, 16, 1, 1), image(12, 9, 8, 2, 1), image(12, 9, 8, 1, 8)]);
|
||||
assert_eq!(wrapped(&crate::embedded_preview(&bytes).unwrap()).0, 4, "IFD0's 4 x 3");
|
||||
let bytes = fff_with(None, false, vec![image(6, 6, 8, 1, 1)]);
|
||||
assert_eq!(wrapped(&crate::embedded_preview(&bytes).unwrap()), (6, 6, vec![7; 108], false));
|
||||
}
|
||||
|
||||
/// A preview strip cut short by a truncated file gives no preview, not a partial picture.
|
||||
#[test]
|
||||
fn short_preview_strip_gives_none() {
|
||||
let mut ifd0 = rgb_ifd();
|
||||
ifd0.set_image(ImageData::Strips { rows_per_strip: 3, strips: vec![vec![1; 30]] });
|
||||
let tiff = TiffWriter::new(ByteOrder::Little, false).write(&[ifd0]).unwrap();
|
||||
assert_eq!(rgb_preview(&Tiff::parse(&tiff).unwrap(), &tiff), None);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
//! | `dng` | Adobe DNG: uncompressed (any depth, packed), lossless JPEG, lossy JPEG, Deflate (integer and floating point), strips and tiles, CFA and LinearRaw, DNG 1.7 interleaved storage; reading and writing |
|
||||
//! | `jxl` | JPEG XL compressed DNG (DNG 1.7, compression 52546) through `jxl-oxide`; implies `dng` |
|
||||
//! | `crw` | Canon CRW (CIFF), decoded by `craft-crw` and returned through this crate's types |
|
||||
//! | `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` |
|
||||
//!
|
||||
//! [`probe`] names every container it can tell apart whatever the features; [`RawFormat::is_supported`] says
|
||||
//! whether this build decodes one.
|
||||
@@ -49,6 +50,8 @@ mod dng;
|
||||
#[cfg(feature = "dng")]
|
||||
pub mod dngwrite;
|
||||
pub mod gaintable;
|
||||
#[cfg(feature = "hasselblad")]
|
||||
mod hasselblad;
|
||||
#[cfg(feature = "jxl")]
|
||||
mod jxl;
|
||||
pub mod ljpeg;
|
||||
@@ -290,7 +293,9 @@ impl RawFormat {
|
||||
/// Whether [`decode`] reads this container in this build (possibly not every compression inside it): the
|
||||
/// format's decoder is in the crate and its Cargo feature is on.
|
||||
pub fn is_supported(self) -> bool {
|
||||
(self == RawFormat::Dng && cfg!(feature = "dng")) || (self == RawFormat::Crw && cfg!(feature = "crw"))
|
||||
(self == RawFormat::Dng && cfg!(feature = "dng"))
|
||||
|| (self == RawFormat::Crw && cfg!(feature = "crw"))
|
||||
|| (self == RawFormat::CfaTiff && cfg!(feature = "hasselblad"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,6 +554,8 @@ fn decode_in(bytes: &[u8], mode: Mode, limits: &Limits) -> Result<RawImage> {
|
||||
RawFormat::Dng => dng::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "crw")]
|
||||
RawFormat::Crw => crw::decode(bytes, mode, limits),
|
||||
#[cfg(feature = "hasselblad")]
|
||||
RawFormat::CfaTiff => hasselblad::decode(bytes, mode, limits),
|
||||
RawFormat::OtherTiff => Err(RawError::Unsupported(other_tiff_reason(bytes))),
|
||||
other => Err(RawError::Unsupported(format!("{other:?} files are not decoded by this build"))),
|
||||
}
|
||||
|
||||
@@ -200,7 +200,8 @@ fn candidates<'a>(data: &'a [u8], ifd: &Ifd, base: u64, out: &mut Vec<&'a [u8]>,
|
||||
}
|
||||
}
|
||||
|
||||
/// The largest embedded preview, if any: a JPEG, or (DNG 1.7) a JPEG XL file.
|
||||
/// The largest embedded preview, if any: a JPEG, or (DNG 1.7) a JPEG XL file, or for a Hasselblad 3FR / FFF, which
|
||||
/// has no JPEG, its uncompressed RGB preview wrapped as a baseline TIFF.
|
||||
pub fn embedded_preview(bytes: &[u8]) -> Option<Vec<u8>> {
|
||||
preview_covering(bytes, None)
|
||||
}
|
||||
@@ -265,7 +266,11 @@ fn preview_covering(bytes: &[u8], min_edge: Option<u32>) -> Option<Vec<u8>> {
|
||||
.or(largest)
|
||||
.map(|s| if is_jxl(s) { s.to_vec() } else { trim_eoi(s).to_vec() });
|
||||
// a TIFF raw whose preview no tag points to: one we can't decode (Leaf MOS), or Epson ERF (in its maker note)
|
||||
best.or_else(|| format.filter(|f| !f.is_supported() || *f == crate::RawFormat::Erf).and_then(|_| scan_for_jpeg(bytes)))
|
||||
let best = best.or_else(|| format.filter(|f| !f.is_supported() || *f == crate::RawFormat::Erf).and_then(|_| scan_for_jpeg(bytes)));
|
||||
// Hasselblad 3FR / FFF carry no JPEG, only uncompressed RGB images: one wrapped as a baseline TIFF
|
||||
#[cfg(feature = "hasselblad")]
|
||||
let best = best.or_else(|| (format == Some(crate::RawFormat::CfaTiff)).then(|| crate::hasselblad::rgb_preview(&tiff, bytes)).flatten());
|
||||
best
|
||||
}
|
||||
|
||||
/// The end of the DCT JPEG whose SOI is at `soi` (exclusive index after its EOI) and its component count,
|
||||
|
||||
@@ -392,6 +392,177 @@ fn hasselblad_make_with_sony_raw_coding_is_arw() {
|
||||
assert_ne!(probe(&file("HASSELBLAD", 7, false)), Some(RawFormat::Arw));
|
||||
}
|
||||
|
||||
#[cfg(feature = "hasselblad")]
|
||||
mod cfa_tiff {
|
||||
use super::*;
|
||||
|
||||
// --- TIFF raws that describe their raw IFD the way DNG does but carry no DNG version tag ---
|
||||
|
||||
/// 8 x 6 samples, 16 bits, value `1000 + 100 y + x`.
|
||||
fn cfa_samples() -> Vec<u16> {
|
||||
(0..48u16).map(|i| 1000 + 100 * (i / 8) + i % 8).collect()
|
||||
}
|
||||
|
||||
/// A small raw in the layout of the Hasselblad 3FR/FFF family: IFD0 is a reduced RGB image with the
|
||||
/// colour tags, a SubIFD holds the full-resolution CFA image (no `CFAPattern`, DNG-style levels and
|
||||
/// crop). `strip` is the CFA image's data and `compression` its coding.
|
||||
fn plain_cfa_tiff(compression: u16, strip: Vec<u8>, pattern: Option<[u8; 4]>) -> 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![8]));
|
||||
raw.set(t::IMAGE_LENGTH, Value::Long(vec![6]));
|
||||
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
|
||||
raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
|
||||
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
|
||||
raw.set(t::COMPRESSION, Value::Short(vec![compression]));
|
||||
raw.set(t::BLACK_LEVEL, Value::Rational(vec![(256, 1)]));
|
||||
raw.set(t::WHITE_LEVEL, Value::Long(vec![60000]));
|
||||
raw.set(t::DEFAULT_CROP_ORIGIN, Value::Short(vec![2, 2]));
|
||||
raw.set(t::DEFAULT_CROP_SIZE, Value::Short(vec![4, 2]));
|
||||
if let Some(p) = pattern {
|
||||
raw.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2]));
|
||||
raw.set(t::CFA_PATTERN_EP, Value::Byte(p.to_vec()));
|
||||
}
|
||||
raw.set_image(ImageData::Strips { rows_per_strip: 6, strips: vec![strip] });
|
||||
let mut ifd0 = rgb_ifd(4, 3);
|
||||
ifd0.set(t::MAKE, Value::Ascii("Hasselblad".into()));
|
||||
ifd0.set(t::COLOR_MATRIX_1, Value::SRational(vec![(5, 10), (-1, 10), (0, 10), (-5, 10), (12, 10), (3, 10), (-1, 10), (2, 10), (6, 10)]));
|
||||
ifd0.set(t::AS_SHOT_NEUTRAL, Value::Rational(vec![(2, 5), (1, 1), (3, 5)]));
|
||||
ifd0.add_sub_ifd(raw);
|
||||
write(&[ifd0])
|
||||
}
|
||||
|
||||
fn le_words(v: &[u16]) -> Vec<u8> {
|
||||
v.iter().flat_map(|x| x.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
/// Known answer: samples, black and white level, crop and colour come from the file's own tags; the
|
||||
/// layout it does not state is RGGB.
|
||||
#[test]
|
||||
fn uncompressed_cfa_tiff_without_a_dng_version_decodes() {
|
||||
let bytes = plain_cfa_tiff(1, le_words(&cfa_samples()), None);
|
||||
assert_eq!(probe(&bytes), Some(RawFormat::CfaTiff));
|
||||
let r = decode(&bytes).unwrap();
|
||||
assert_eq!((r.format, r.width, r.height, r.bits), (RawFormat::CfaTiff, 8, 6, 16));
|
||||
assert_eq!(r.data, RawData::U16(cfa_samples()));
|
||||
assert_eq!(r.cfa.as_ref().unwrap().name(), "RGGB");
|
||||
assert_eq!((r.black.mean(), r.white.clone()), (256.0, vec![60000.0]));
|
||||
assert_eq!(r.crop, Rect::new(2, 2, 4, 2));
|
||||
assert!(r.color.color_matrix[0].is_some());
|
||||
assert_eq!(r.color.as_shot_neutral, Some([0.4, 1.0, 0.6]));
|
||||
// headers only: the same description without samples
|
||||
let i = probe_info(&bytes).unwrap();
|
||||
assert_eq!((i.format, i.cfa.as_ref().map(Cfa::name), i.crop), (RawFormat::CfaTiff, Some("RGGB".to_string()), Rect::new(2, 2, 4, 2)));
|
||||
}
|
||||
|
||||
/// Without DNG's white-balance tag the file is left alone (a Kodak or Sinar TIFF with a CFA IFD, say).
|
||||
#[test]
|
||||
fn cfa_tiff_without_a_white_balance_tag_is_not_claimed() {
|
||||
let mut raw = IfdBuilder::new();
|
||||
raw.set(t::IMAGE_WIDTH, Value::Long(vec![8]));
|
||||
raw.set(t::IMAGE_LENGTH, Value::Long(vec![6]));
|
||||
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
|
||||
raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
|
||||
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
|
||||
raw.set(t::COMPRESSION, Value::Short(vec![1]));
|
||||
raw.set_image(ImageData::Strips { rows_per_strip: 6, strips: vec![le_words(&cfa_samples())] });
|
||||
let mut ifd0 = rgb_ifd(4, 3);
|
||||
ifd0.add_sub_ifd(raw);
|
||||
assert_eq!(probe(&write(&[ifd0])), Some(RawFormat::OtherTiff));
|
||||
}
|
||||
|
||||
/// A stated `CFAPattern` wins over the RGGB default.
|
||||
#[test]
|
||||
fn cfa_tiff_keeps_a_stated_pattern() {
|
||||
let bytes = plain_cfa_tiff(1, le_words(&cfa_samples()), Some([2, 1, 1, 0]));
|
||||
assert_eq!(decode(&bytes).unwrap().cfa.unwrap().name(), "BGGR");
|
||||
}
|
||||
|
||||
/// The lossless-JPEG coding of the same layout goes through the lossless decoder (one strip for the
|
||||
/// whole image, as the 3FR/FFF files have it).
|
||||
#[test]
|
||||
fn lossless_jpeg_cfa_tiff_decodes() {
|
||||
let strip = ljpeg::encode(&cfa_samples(), 8, 6, 1, 16, 1, 0);
|
||||
let bytes = plain_cfa_tiff(7, strip, None);
|
||||
assert_eq!(probe(&bytes), Some(RawFormat::CfaTiff));
|
||||
let r = decode(&bytes).unwrap();
|
||||
assert_eq!(r.data, RawData::U16(cfa_samples()));
|
||||
assert_eq!(r.cfa.unwrap().name(), "RGGB");
|
||||
}
|
||||
|
||||
/// Hasselblad's own lossless-JPEG coding (selection value 8, as in FFF and most compressed 3FR files) of the
|
||||
/// same layout decodes: the file is read like the uncompressed one.
|
||||
#[test]
|
||||
fn selection_value_8_cfa_tiff_decodes() {
|
||||
let strip = ljpeg::tests::encode_hasselblad(&cfa_samples(), 8, 6);
|
||||
let bytes = plain_cfa_tiff(7, strip, None);
|
||||
assert_eq!(probe(&bytes), Some(RawFormat::CfaTiff));
|
||||
let r = decode(&bytes).unwrap();
|
||||
assert_eq!((r.format, r.width, r.height), (RawFormat::CfaTiff, 8, 6));
|
||||
assert_eq!(r.data, RawData::U16(cfa_samples()));
|
||||
assert_eq!(r.cfa.unwrap().name(), "RGGB");
|
||||
assert_eq!(probe_info(&bytes).unwrap().format, RawFormat::CfaTiff);
|
||||
}
|
||||
|
||||
/// A lossless-JPEG scan whose predictor selection value is neither 0 to 7 (T.81) nor 8 (Hasselblad) is not a
|
||||
/// coding the lossless decoder knows: the file stays an undecodable raw, with that as the reason, and keeps the
|
||||
/// stand-in preview an unsupported container gets.
|
||||
#[test]
|
||||
fn cfa_tiff_with_an_unknown_predictor_stays_undecodable() {
|
||||
let mut strip = ljpeg::encode(&cfa_samples(), 8, 6, 1, 16, 1, 0);
|
||||
let sos = strip.windows(2).position(|w| w == [0xff, 0xda]).unwrap();
|
||||
let ss = sos + 2 + 2 + 1 + 2;
|
||||
assert_eq!(strip[ss], 1, "selection value byte");
|
||||
strip[ss] = 9;
|
||||
assert!(matches!(ljpeg::decode(&strip, 1 << 20), Err(RawError::Unsupported(w)) if w.contains("selection value 9")));
|
||||
let bytes = plain_cfa_tiff(7, strip, None);
|
||||
assert_eq!(probe(&bytes), Some(RawFormat::OtherTiff));
|
||||
assert!(!RawFormat::OtherTiff.is_supported());
|
||||
assert_eq!(embedded_preview(&bytes), None, "no JPEG in the file, and an undecodable raw gets no RGB stand-in");
|
||||
let Err(RawError::Unsupported(why)) = decode(&bytes) else { panic!("expected Unsupported") };
|
||||
assert!(why.contains("lossless JPEG") && why.contains("selection value 9"), "{why}");
|
||||
}
|
||||
|
||||
/// Other codings of a CFA IFD (here Deflate) are not claimed by the plain reader.
|
||||
#[test]
|
||||
fn cfa_tiff_with_another_coding_is_not_claimed() {
|
||||
let bytes = plain_cfa_tiff(8, vec![0; 96], None);
|
||||
assert_eq!(probe(&bytes), Some(RawFormat::OtherTiff));
|
||||
}
|
||||
|
||||
/// PhotoCraft's reader: four black levels without a `BlackLevelRepeatDim` are one per site of the 2 × 2 CFA in a
|
||||
/// file that is not a DNG. The DNG reader took the first for every site. In a DNG the count must match the repeat.
|
||||
#[test]
|
||||
fn four_black_levels_without_a_repeat_are_per_cfa_site_outside_dng() {
|
||||
let with_blacks = |dng: bool| {
|
||||
let mut raw = IfdBuilder::new();
|
||||
raw.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0]));
|
||||
raw.set(t::IMAGE_WIDTH, Value::Long(vec![8]));
|
||||
raw.set(t::IMAGE_LENGTH, Value::Long(vec![6]));
|
||||
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
|
||||
raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
|
||||
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
|
||||
raw.set(t::COMPRESSION, Value::Short(vec![1]));
|
||||
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(t::BLACK_LEVEL, Value::Short(vec![250, 252, 254, 256]));
|
||||
raw.set_image(ImageData::Strips { rows_per_strip: 6, strips: vec![le_words(&cfa_samples())] });
|
||||
let mut ifd0 = rgb_ifd(4, 3);
|
||||
ifd0.set(t::MAKE, Value::Ascii("Hasselblad".into()));
|
||||
ifd0.set(t::AS_SHOT_NEUTRAL, Value::Rational(vec![(2, 5), (1, 1), (3, 5)]));
|
||||
if dng {
|
||||
ifd0.set(t::DNG_VERSION, Value::Byte(vec![1, 4, 0, 0]));
|
||||
}
|
||||
ifd0.add_sub_ifd(raw);
|
||||
decode(&write(&[ifd0])).unwrap().black
|
||||
};
|
||||
let b = with_blacks(false);
|
||||
assert_eq!((b.repeat_rows, b.repeat_cols, b.values.clone()), (2, 2, vec![250.0, 252.0, 254.0, 256.0]));
|
||||
assert_eq!((b.at(0, 0, 0, 1), b.at(1, 0, 0, 1), b.at(0, 1, 0, 1), b.at(3, 3, 0, 1)), (250.0, 252.0, 254.0, 256.0));
|
||||
assert_eq!(with_blacks(true), BlackLevel::uniform(250.0), "a DNG's count has to match its repeat");
|
||||
}
|
||||
}
|
||||
|
||||
/// A Samsung-branded body whose maker note has a Pentax layout is read by the Pentax reader; another
|
||||
/// Samsung file stays with the Samsung format.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user