# Conflicts:
#	crates/engine/src/media.rs
This commit is contained in:
Claude 2026-10-11 11:42:42 +00:00
commit cba621e55c
No known key found for this signature in database
4 changed files with 68 additions and 9 deletions

View File

@ -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 {

View File

@ -62,7 +62,7 @@ impl SettingsHashes {
}
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
pub const RENDER_CACHE_VERSION: u64 = 50;
pub const RENDER_CACHE_VERSION: u64 = 51;
/// 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];

View File

@ -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),

View File

@ -279,6 +279,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".to_vec();
bytes.extend(TiffWriter::new(ByteOrder::Little, false).write(&[mn]).unwrap());
exif.set(t::MAKER_NOTE, Value::Undefined(bytes));
}
let mut ifd0 = IfdBuilder::new();
ifd0.set(t::MAKE, Value::Ascii(make.into()));
if dng {
ifd0.set(t::DNG_VERSION, Value::Byte(vec![1, 4, 0, 0]));
}
ifd0.set_child(t::EXIF_IFD, exif);
TiffWriter::new(ByteOrder::Little, false).write(&[ifd0]).unwrap()
};
use crate::PreviewColorSpace::{AdobeRgb, Srgb};
let f = crate::embedded_preview_color_space;
assert_eq!(f(&file("SONY", 0xFFFF, Some("R03"), None, false)), Some(AdobeRgb));
assert_eq!(f(&file("SONY", 1, Some("R98"), None, false)), Some(Srgb));
assert_eq!(f(&file("SONY", 0xFFFF, Some("R98"), None, false)), None);
assert_eq!(f(&file("SONY", 0xFFFF, None, None, false)), None);
assert_eq!(f(&file("Canon", 0xFFFF, Some("R03"), None, false)), Some(AdobeRgb));
assert_eq!(f(&file("NIKON CORPORATION", 1, Some("R98"), Some(2), false)), Some(AdobeRgb));
assert_eq!(f(&file("NIKON CORPORATION", 0xFFFF, Some("R03"), None, false)), Some(AdobeRgb));
assert_eq!(f(&file("Canon", 0xFFFF, Some("R03"), None, true)), None);
}
#[test]
fn nikon_crop_area_preserves_sensor_origin_and_rejects_invalid_rectangles() {
let (w, h) = (32, 24);