mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 20:17:18 -06:00
LR-PROF-CAMERACOLOR: read an untagged camera JPEG in the colour space the raw's EXIF states, for every make (#778)
A Sony body shot in Adobe RGB writes ColorSpace Uncalibrated with InteropIndex R03 in the raw's EXIF and leaves the embedded JPEG untagged, so the look was fitted against the JPEG decoded as sRGB. The EXIF fallback now applies to all makes (the Nikon maker note still wins); DNGs are skipped, since their previews come from the converter.
This commit is contained in:
@@ -22,8 +22,9 @@ use lightcraft_raw::{RawFormat, RawImage, color::CameraTransform, profile::HsvTa
|
||||
/// that leave the fitted look alone. `RENDER_CACHE_VERSION` covers renders; this covers proxies.
|
||||
///
|
||||
/// 1: the fit as of #499's follow-up; 2: Sony DRO (tone curve lowered to Sony's curve without DRO)
|
||||
/// and the ILCE-7CR profile (#528, #583, #568, #616).
|
||||
pub const LOOK_VERSION: u32 = 2;
|
||||
/// and the ILCE-7CR profile (#528, #583, #568, #616); 3: the camera JPEG read in the colour space the raw's EXIF
|
||||
/// states, for every make (#778).
|
||||
pub const LOOK_VERSION: u32 = 3;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct CameraLook {
|
||||
|
||||
@@ -62,7 +62,7 @@ impl SettingsHashes {
|
||||
}
|
||||
|
||||
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
|
||||
pub const RENDER_CACHE_VERSION: u64 = 42;
|
||||
pub const RENDER_CACHE_VERSION: u64 = 43;
|
||||
|
||||
/// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
|
||||
pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];
|
||||
|
||||
@@ -14,16 +14,35 @@ pub enum PreviewColorSpace {
|
||||
AdobeRgb,
|
||||
}
|
||||
|
||||
/// Nikon maker-note ColorSpace (0x001e: 1 = sRGB, 2 = Adobe RGB), per Nikon tag documentation.
|
||||
/// This is a fallback only: a JPEG's own ICC/EXIF colour declaration takes precedence.
|
||||
/// The colour space the raw says its embedded JPEG is in, for a JPEG that declares none itself (no ICC profile,
|
||||
/// no EXIF), first available first: the Nikon maker-note ColorSpace (0x001e: 1 = sRGB, 2 = Adobe RGB, per Nikon
|
||||
/// tag documentation); the raw's own EXIF ColorSpace (0xA001: 1 = sRGB), or Uncalibrated (0xFFFF) with the
|
||||
/// Interoperability IFD's InteropIndex "R03", the DCF option file for Adobe RGB (as a Sony body shot in Adobe RGB
|
||||
/// writes, issue #778; per the Exif and DCF specifications), not for a DNG. This is a fallback only: a JPEG's own ICC/EXIF colour
|
||||
/// declaration takes precedence.
|
||||
pub fn embedded_preview_color_space(bytes: &[u8]) -> Option<PreviewColorSpace> {
|
||||
let tiff = Tiff::parse(bytes).ok()?;
|
||||
let make = tiff.ifds.first()?.string(t::MAKE)?;
|
||||
if !make.to_ascii_uppercase().starts_with("NIKON") {
|
||||
let make = tiff.ifds.first()?.string(t::MAKE).unwrap_or_default();
|
||||
let exif = tiff.exif()?;
|
||||
if make.to_ascii_uppercase().starts_with("NIKON")
|
||||
&& let Some(space) = nikon_color_space(bytes, &tiff, exif, &make)
|
||||
{
|
||||
return Some(space);
|
||||
}
|
||||
// a DNG's EXIF describes the original capture; its previews are rendered by the converter
|
||||
if tiff.ifds.first()?.contains(t::DNG_VERSION) {
|
||||
return None;
|
||||
}
|
||||
let e = tiff.exif()?.get(t::MAKER_NOTE)?;
|
||||
let mn = makernote::parse_makernote(bytes, e.offset, e.count() as u64, tiff.order, &make)?;
|
||||
match exif.u16(0xA001)? {
|
||||
1 => Some(PreviewColorSpace::Srgb),
|
||||
0xFFFF if exif.interop.as_deref()?.bytes(0x0001)?.starts_with(b"R03") => Some(PreviewColorSpace::AdobeRgb),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn nikon_color_space(bytes: &[u8], tiff: &Tiff, exif: &Ifd, make: &str) -> Option<PreviewColorSpace> {
|
||||
let e = exif.get(t::MAKER_NOTE)?;
|
||||
let mn = makernote::parse_makernote(bytes, e.offset, e.count() as u64, tiff.order, make)?;
|
||||
match mn.ifd.u16(0x001e)? {
|
||||
1 => Some(PreviewColorSpace::Srgb),
|
||||
2 => Some(PreviewColorSpace::AdobeRgb),
|
||||
|
||||
Vendored
+39
@@ -251,6 +251,45 @@ mod tests {
|
||||
assert_eq!(crate::embedded_preview_color_space(b"not a TIFF"), None);
|
||||
}
|
||||
|
||||
/// Issue #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 | ||||