diff --git a/crates/media/src/exr_channels.rs b/crates/media/src/exr_channels.rs index 06bda925..732cd3c4 100644 --- a/crates/media/src/exr_channels.rs +++ b/crates/media/src/exr_channels.rs @@ -37,6 +37,18 @@ pub fn parse_manifest(json: &str) -> Vec<(String, u32)> { v } +/// Why an OpenEXR file's pixels can't be decoded when its headers name a compression this build +/// can't decompress (HTJ2K, OpenEXR 3.4's High-Throughput JPEG 2000): the decoders only said +/// "no non-deep rgb channels" or showed a transparent frame (#482). `None` for other files. +pub fn unsupported_compression(bytes: &[u8]) -> Option<&'static str> { + use exr::compression::Compression; + let meta = exr::meta::MetaData::read_from_buffered(std::io::Cursor::new(bytes), false).ok()?; + meta.headers + .iter() + .any(|h| matches!(h.compression, Compression::HTJ2K32 | Compression::HTJ2K256)) + .then_some("OpenEXR HTJ2K compression isn't supported yet: re-save the file with ZIP, PIZ or DWAA compression") +} + /// Read all channels of an EXR file's bytes. `None` when it is not a readable EXR. pub fn read_exr_channels(bytes: &[u8]) -> Option { use exr::prelude::*; diff --git a/crates/media/src/pool.rs b/crates/media/src/pool.rs index db3e19a4..9db2f225 100644 --- a/crates/media/src/pool.rs +++ b/crates/media/src/pool.rs @@ -731,6 +731,13 @@ impl Inner { if let Some(img) = crate::layered::decode(path, &bytes, footage, op)? { return Ok(img); } + // (an OpenEXR compression the decoder lacks came out transparent or as a missing + // channel error) + if path.to_ascii_lowercase().ends_with(".exr") + && let Some(why) = crate::exr_channels::unsupported_compression(&bytes) + { + return Err(MediaError::Decode(format!("{path}: {why}"))); + } let img = match image::load_from_memory(&bytes) { Ok(img) => img, // A multi-layer OpenEXR file without an unnamed RGB layer: its colour layer. @@ -963,6 +970,32 @@ mod tests { assert!(c.take_spare(17).is_empty()); } + /// #482: an OpenEXR file with HTJ2K compression (which the decoder lacks) says so on import + /// and when its frame is read, instead of "no non-deep rgb channels" or a transparent frame. + #[test] + fn htj2k_exr_is_reported_as_unsupported() { + use exr::prelude::*; + let channels = + AnyChannels::sort(vec![AnyChannel::new("R", FlatSamples::F32(vec![0.5; 4])), AnyChannel::new("G", FlatSamples::F32(vec![0.5; 4]))].into()); + let image = exr::image::Image::from_layer(Layer::new((2, 2), LayerAttributes::default(), Encoding::UNCOMPRESSED, channels)); + let mut bytes = std::io::Cursor::new(Vec::new()); + image.write().to_buffered(&mut bytes).unwrap(); + let mut bytes = bytes.into_inner(); + assert_eq!(crate::exr_channels::unsupported_compression(&bytes), None); + // The header's compression attribute (name, type, size 1, value) says HTJ2K32 (11). + let tag = b"compression\0compression\0\x01\0\0\0"; + let at = bytes.windows(tag.len()).position(|w| w == tag).unwrap() + tag.len(); + bytes[at] = 11; + let b: Arc<[u8]> = bytes.into(); + let e = crate::probe_bytes("/shot.exr", b.clone()).unwrap_err().to_string(); + assert!(e.contains("/shot.exr: OpenEXR HTJ2K compression isn't supported yet"), "{e}"); + let pool = MediaPool::new(); + pool.add_bytes("/shot.exr", b); + let f = Footage { path: "/shot.exr".into(), kind: FootageKind::Still, width: 2, height: 2, has_video: true, ..Default::default() }; + let e = pool.frame_at(&f, Tick::ZERO).unwrap_err().to_string(); + assert!(e.contains("HTJ2K compression isn't supported yet"), "{e}"); + } + /// #411: Interpret Footage ▸ Preserve RGB reads a float OpenEXR's values as the file stores /// them (no automatic linear → sRGB encode), so an OCIO effect is the only conversion; the /// default interpretation still encodes. Both interpretations of one file decode separately. diff --git a/crates/media/src/probe.rs b/crates/media/src/probe.rs index 4e986151..6e8718fd 100644 --- a/crates/media/src/probe.rs +++ b/crates/media/src/probe.rs @@ -158,6 +158,11 @@ pub(crate) fn still_footage(path: &str, w: u32, h: u32, fmt: image::ImageFormat, fn probe_still_bytes(path: &str, bytes: &[u8], fmt: image::ImageFormat) -> Result { use image::ImageDecoder; + if fmt == image::ImageFormat::OpenExr + && let Some(why) = crate::exr_channels::unsupported_compression(bytes) + { + return Err(MediaError::Decode(format!("{path}: {why}"))); + } let reader = image::ImageReader::with_format(std::io::Cursor::new(bytes), fmt); let (w, h, has_alpha) = match reader.into_decoder() { Ok(dec) => (dec.dimensions().0, dec.dimensions().1, dec.color_type().has_alpha()),