mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Media: OpenEXR files with HTJ2K compression say it isn't supported (#482)
The exr crate reads HTJ2K (OpenEXR 3.4's High-Throughput JPEG 2000) headers but can't decompress the pixels, and the image paths turned that into a misleading "no non-deep rgb channels" (multi-layer files) or imported the file and showed a transparent frame (single-layer files). `exr_channels::unsupported_compression` reads the headers; import (probe) and frame decoding fail with "<file>: OpenEXR HTJ2K compression isn't supported yet: re-save the file with ZIP, PIZ or DWAA compression". Decoding HTJ2K itself needs a pure-Rust JPEG 2000 Part 15 decoder and is left for later. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
3 files changed
+50
No files matched your search
@@ -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<AuxChannels> {
|
||||
use exr::prelude::*;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Footage> {
|
||||
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()),
|
||||
|
||||
Reference in new issue
Block a user