From 2dea9109d8c78c51cca64950958ebd852830d583 Mon Sep 17 00:00:00 2001 From: radialmonster Date: Fri, 9 Oct 2026 06:44:20 -0400 Subject: [PATCH] LR-IMP-FORMATS: name GoPro GPR (VC-5 DNG) in the unsupported-raw error instead of 'TIFF compression 9' --- crates/engine/src/files.rs | 24 ++++++++++++++++++++++++ crates/raw/src/dng.rs | 9 +++++++++ crates/raw/src/lib.rs | 26 ++++++++++++++++++++++++++ crates/tiff/src/tags.rs | 2 ++ 4 files changed, 61 insertions(+) diff --git a/crates/engine/src/files.rs b/crates/engine/src/files.rs index 95481a4ff..f9ee11515 100644 --- a/crates/engine/src/files.rs +++ b/crates/engine/src/files.rs @@ -1090,6 +1090,30 @@ mod tests { assert!(e.contains("without an embedded preview"), "{e}"); } + /// A GoPro GPR (DNG with VC-5 coded data, no preview anywhere) still imports, described from its + /// headers; loading it says what is wrong instead of naming a bare TIFF compression number. + #[test] + fn gopro_vc5_dng_imports_and_fails_to_load_with_a_clear_reason() { + use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value, tags as t}; + let mut ifd = IfdBuilder::new(); + ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0])); + ifd.set(t::IMAGE_WIDTH, Value::Long(vec![32])); + ifd.set(t::IMAGE_LENGTH, Value::Long(vec![16])); + ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16])); + ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1])); + ifd.set(t::PHOTOMETRIC, Value::Short(vec![32803])); + ifd.set(t::COMPRESSION, Value::Short(vec![9])); + ifd.set(t::DNG_VERSION, Value::Byte(vec![1, 4, 0, 0])); + ifd.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2])); + ifd.set(t::CFA_PATTERN_EP, Value::Byte(vec![0, 1, 1, 2])); + ifd.set_image(ImageData::Tiles { tile_width: 32, tile_height: 16, tiles: vec![vec![0x5au8; 400]] }); + let f = TiffWriter::default().write(&[ifd]).unwrap(); + let p = probe_bytes("GOPR0001.GPR", &f).unwrap(); + assert_eq!((p.kind, p.preview_only, p.width, p.height), (MediaKind::Raw, None, 32, 16)); + let e = load_bytes(&f, 16).unwrap_err(); + assert!(e.contains("GoPro VC-5") && e.contains("not decoded yet"), "{e}"); + } + /// Any failure of a recognised raw (not only an unsupported variant) can fall back to its preview; /// CR3 keeps its own wording. #[test] diff --git a/crates/raw/src/dng.rs b/crates/raw/src/dng.rs index 63ddf91a6..126e135ff 100644 --- a/crates/raw/src/dng.rs +++ b/crates/raw/src/dng.rs @@ -92,6 +92,15 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { let tiff = Tiff::parse(bytes)?; let ifd0 = &tiff.ifds[0]; let raw = raw_ifd(&tiff).ok_or_else(|| RawError::Corrupt("DNG without a raw image IFD".into()))?; + // GoPro's GPR: a DNG whose raw data is VC-5 coded, which is not decoded (yet). Named here, before + // the generic "TIFF compression 9" of the chunk decoder. Header mode still describes the file + // (a GPR has no embedded preview, and a library keeps the photo with this reason shown, as it + // does for any file whose pixels fail to decode, rather than refusing to import it). + if mode == Mode::Full + && let Some(c) = raw.u16(t::COMPRESSION).filter(|&c| c == t::compression::VC5) + { + return Err(RawError::Unsupported(format!("DNG compression {c} (GoPro VC-5) is not decoded yet"))); + } let info = raw.image()?; let (w, h, cpp) = (info.width as usize, info.height as usize, info.samples_per_pixel as usize); if !(1..=4).contains(&cpp) { diff --git a/crates/raw/src/lib.rs b/crates/raw/src/lib.rs index d38b2fc68..1c7a865f1 100644 --- a/crates/raw/src/lib.rs +++ b/crates/raw/src/lib.rs @@ -911,6 +911,32 @@ mod tests { assert_eq!(probe(&g), None); } + /// A GoPro GPR (a DNG with VC-5 coded raw data, Compression 9) is refused by the full decode with + /// a reason that names the format, whatever its tile bytes hold; the header probe still + /// describes it (so it imports). + #[test] + fn gopro_vc5_dng_is_unsupported_with_a_clear_reason() { + let mut ifd = IfdBuilder::new(); + ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0])); + ifd.set(t::IMAGE_WIDTH, Value::Long(vec![32])); + ifd.set(t::IMAGE_LENGTH, Value::Long(vec![16])); + ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16])); + ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1])); + ifd.set(t::PHOTOMETRIC, Value::Short(vec![32803])); + ifd.set(t::COMPRESSION, Value::Short(vec![9])); + ifd.set(t::DNG_VERSION, Value::Byte(vec![1, 4, 0, 0])); + ifd.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2])); + ifd.set(t::CFA_PATTERN_EP, Value::Byte(vec![0, 1, 1, 2])); + ifd.set_image(ImageData::Tiles { tile_width: 32, tile_height: 16, tiles: vec![vec![0x5au8; 400]] }); + let bytes = write(&[ifd]); + assert_eq!(probe(&bytes), Some(RawFormat::Dng)); + let want = "DNG compression 9 (GoPro VC-5) is not decoded yet"; + let Err(RawError::Unsupported(why)) = decode(&bytes) else { panic!("expected Unsupported from decode") }; + assert_eq!(why, want); + let info = probe_info(&bytes).expect("header probe describes the file"); + assert_eq!((info.width, info.height), (32, 16)); + } + /// Compression 99 (not a registered TIFF value; Leaf MOS tiles) marks a raw even without a CFA tag. #[test] fn private_compression_99_is_a_raw() { diff --git a/crates/tiff/src/tags.rs b/crates/tiff/src/tags.rs index 5771316a5..6ee854d88 100644 --- a/crates/tiff/src/tags.rs +++ b/crates/tiff/src/tags.rs @@ -200,6 +200,8 @@ pub mod compression { /// DNG lossy JPEG (baseline DCT, 8-bit). pub const LOSSY_JPEG: u16 = 34892; pub const JPEG_XL: u16 = 52546; + /// SMPTE ST 2073 (VC-5) as written by GoPro's GPR files (the value measured in them). + pub const VC5: u16 = 9; /// Nikon NEF Huffman compression. pub const NIKON: u16 = 34713; /// Sony ARW compressed (the value used by ARW files).