Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ jobs:
# doctests, which `cargo llvm-cov` cannot instrument on stable.
- name: Clippy
run: mise run lint
# `mise run lint` cannot reach the real-camera DNG conformance tier: it is excluded from the
# workspace and nothing depends on it, so no workspace command compiles it, and a gamut-dng
# API change breaks it unnoticed until the extended run on master. Its expensive half — the
# Adobe DNG SDK — is already built by this job for gamut-dng's own dev-dependency on
# `gamut-dng-oracle`, so what is added here is the tier's own Rust targets. `check` needs no
# corpus, so this job does not grow the ~178 MiB fetch that `test-dng-real` requires.
- name: Real-DNG conformance tier compiles
run: mise run check-dng-real
- name: gamut-ffi feature sync
run: mise run check-ffi-features
- name: gamut-ffi header sync
Expand Down
11 changes: 11 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ run = "git submodule update --init --checkout third_party/gamut-dng-samples"
description = "Validate gamut-dng against real camera DNGs (issue #174; needs fetch-dng-samples)"
run = "cargo test --manifest-path tooling/gamut-dng-real-conformance/Cargo.toml"

# The compile half of the tier above, and the only part of it the per-PR lane can afford. That
# crate is excluded from the workspace *and* nothing depends on it — it is invoked by manifest
# path — so `clippy --workspace --all-targets` and `test --workspace` never build it, and a
# gamut-dng API change can break it while every per-PR gate stays green (#353 removed
# `DecodedDng::exif_extra` and only master's extended run noticed, at compile time, before a
# single sample was decoded). `check` needs no corpus, which is what makes this cheap enough for
# the lint job; the corpus-backed assertions stay in `test-dng-real`.
[tasks.check-dng-real]
description = "Compile the real-camera DNG conformance tier (no corpus needed)"
run = "cargo check --manifest-path tooling/gamut-dng-real-conformance/Cargo.toml --all-targets"

# Doctests only. On stable, `cargo llvm-cov` cannot instrument doctests (that needs nightly), so
# the coverage gate — which CI uses as its green-test gate — silently skips them. This task is the
# missing slice: CI's lint lane runs it, reusing the `--all-targets --all-features` build it just
Expand Down
88 changes: 86 additions & 2 deletions tooling/gamut-dng-real-conformance/examples/probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
//! --example probe -- third_party/gamut-dng-samples/apple/iphone-12-pro/IMG_1361.DNG
//! ```

use gamut_dng::{DngDecoder, DngRewrite, SubImageData, deconstruct};
use gamut_dng::{
ColorProfileInfo, DngDecoder, DngRewrite, HsvTable, NoiseProfile, SubImageData, deconstruct,
};

fn main() {
for path in std::env::args().skip(1) {
Expand Down Expand Up @@ -97,11 +99,20 @@ fn report_decode(data: &[u8]) {
),
None => println!(" profile : none (no colour calibration in the file)"),
}
report_color_profile(decoded.color_profile.as_ref());
report_noise_profile(decoded.noise_profile.as_ref());
// `exif` counts the EXIF sub-IFD's own entries, not an extras list: the whole directory is
// carried inside `DngMetadata::exif`, so there is no unmodelled EXIF residue to count.
println!(
" extras : ifd0={} raw={} exif={} gain_map={} gain_map2={}",
decoded.ifd0_extra.len(),
decoded.raw_extra.len(),
decoded.exif_extra.len(),
decoded
.metadata
.exif
.as_ref()
.and_then(|e| e.exif_ifd())
.map_or(0, |ifd| ifd.fields().len()),
decoded.gain_table_map.is_some(),
decoded.gain_table_map2.is_some(),
);
Expand Down Expand Up @@ -136,6 +147,79 @@ fn report_decode(data: &[u8]) {
}
}

/// The camera-profile colour tags `CameraProfile` does not model: the rendering tables and curve,
/// the profile exposure offset, the DNG 1.6 third calibration set and the reduction matrices.
///
/// These reach a caller typed rather than as `ifd0_extra` residue, so a file's colour rendering is
/// visible here without reading raw tag numbers back by hand.
fn report_color_profile(info: Option<&ColorProfileInfo>) {
let Some(info) = info else {
println!(" color_profile : none (no rendering tables, curve or third calibration set)");
return;
};
println!(
" color_profile : hue_sat=[{}, {}, {}] look={} tone_curve={} exposure_offset={:?}",
hsv_table(info.hue_sat_map1.as_ref()),
hsv_table(info.hue_sat_map2.as_ref()),
hsv_table(info.hue_sat_map3.as_ref()),
hsv_table(info.look_table.as_ref()),
info.tone_curve
.as_ref()
.map_or_else(|| "-".to_string(), |c| format!("{} pts", c.len())),
info.baseline_exposure_offset,
);
println!(
" third calibration set: matrix={} illuminant={:?} calibration={} forward={}",
info.color_matrix3.is_some(),
info.calibration_illuminant3,
info.camera_calibration3.is_some(),
info.forward_matrix3.is_some(),
);
println!(
" reduction matrices: 1={} 2={} 3={}",
matrix_terms(info.reduction_matrix1.as_deref()),
matrix_terms(info.reduction_matrix2.as_deref()),
matrix_terms(info.reduction_matrix3.as_deref()),
);
}

/// One hue/saturation/value table's divisions and encoding, or `-` when the file carries none.
fn hsv_table(table: Option<&HsvTable>) -> String {
table.map_or_else(
|| "-".to_string(),
|t| {
format!(
"{}x{}x{} {:?}",
t.hue_divisions, t.saturation_divisions, t.value_divisions, t.encoding
)
},
)
}

/// A reduction matrix's term count, or `-` when absent. Its shape is `3 x ColorPlanes`, so the
/// count is three times the number of colour planes it reduces from.
fn matrix_terms(matrix: Option<&[f64]>) -> String {
matrix.map_or_else(|| "-".to_string(), |m| format!("{} terms", m.len()))
}

/// The sensor's noise model, read from the raw IFD (the spec's home for it) with an IFD 0 fallback.
fn report_noise_profile(profile: Option<&NoiseProfile>) {
let Some(profile) = profile else {
println!(" noise_profile : none");
return;
};
let models: Vec<String> = profile
.planes
.iter()
.map(|m| format!("scale={:.3e} offset={:.3e}", m.scale, m.offset))
.collect();
println!(
" noise_profile : planes={} [{}]",
profile.planes.len(),
models.join(", "),
);
}

/// The preserving rewrite round-trip.
fn report_rewrite(data: &[u8]) {
let opened = match DngRewrite::open(data) {
Expand Down
Loading