Render Queue EXR sequences are scan-line, not tiled (#2805)

image's OpenEXR encoder writes 64x64 RLE tiles, which FilmCraft and other
scan-line readers refuse. Write the frames with the exr crate in scan lines
with ZIP, like comp.saveFrameAsExr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
heojunfoandClaude Opus 5.5 committed 2026-11-02 15:29:59 +09:00
1 parent ad7abfa507
commit 3b2e26ceef
4 files changed
+29 -3

No files matched your search

Generated
+1
View File
@@ -1555,6 +1555,7 @@ dependencies = [
"effectcraft-render",
"effectcraft-time",
"effectcraft-vp9enc",
"exr",
"filmcraft-aac",
"filmcraft-h264enc",
"filmcraft-isobmff",
+1
View File
@@ -23,6 +23,7 @@ effectcraft-vp9enc = { workspace = true }
effectcraft-hevcenc = { workspace = true }
effectcraft-av1enc = { workspace = true }
image = { workspace = true, features = ["png", "jpeg", "tiff", "exr", "tga"] }
exr = { workspace = true }
gif = { workspace = true }
rayon = { workspace = true }
web-time = { workspace = true }
+11 -3
View File
@@ -297,7 +297,7 @@ struct StillOutput {
fn write_still<W: std::io::Write + std::io::Seek>(cx: &Cx, file: &mut W, img: &Image, comp: &Comp, output: StillOutput) -> Result<()> {
let StillOutput { format: fmt, channels, size: (w, h), quality } = output;
use image::{ExtendedColorType, ImageEncoder, ImageFormat};
use image::{ExtendedColorType, ImageEncoder};
match fmt {
OutputFormat::ExrSequence => {
// Linear light, premultiplied (the EXR convention); RGB keeps alpha = 1 over the background.
@@ -326,8 +326,16 @@ fn write_still<W: std::io::Write + std::io::Seek>(cx: &Cx, file: &mut W, img: &I
data.extend_from_slice(&[c[0], c[1], c[2], a]);
}
}
let buf = image::Rgba32FImage::from_raw(w, h, data).ok_or_else(|| ExportError::Encode("exr buffer".into()))?;
image::DynamicImage::ImageRgba32F(buf).write_to(file, ImageFormat::OpenExr).map_err(|e| ExportError::Encode(e.to_string()))
// Scan lines with ZIP like `comp.saveFrameAsExr`: `image`'s encoder writes 64×64 RLE tiles,
// which FilmCraft and other scan-line readers refuse (#2805).
use exr::prelude::{Encoding, Layer, LayerAttributes, SpecificChannels, Vec2, WritableImage};
let row = w as usize * 4;
let pixels = SpecificChannels::rgba(|p: Vec2<usize>| {
let o = p.y() * row + p.x() * 4;
(data[o], data[o + 1], data[o + 2], data[o + 3])
});
let layer = Layer::new((w as usize, h as usize), LayerAttributes::default(), Encoding::SMALL_LOSSLESS, pixels);
exr::image::Image::from_layer(layer).write().to_buffered(file).map_err(|e| ExportError::Encode(e.to_string()))
}
OutputFormat::JpegSequence => {
let px = cx.pixels(img, comp, if channels == Channels::Alpha { Channels::Alpha } else { Channels::Rgb }, w, h);
+16
View File
@@ -302,6 +302,22 @@ fn exr_sequence_is_linear() {
assert!((c[2] - 0.1329).abs() < 0.01 && (c[3] - 1.0).abs() < 1e-6, "{c:?}");
}
/// Sequence frames are scan-line EXR, not tiled, so scan-line readers such as FilmCraft open
/// them (#2805).
#[test]
fn exr_sequence_writes_scan_lines() {
let (p, cid) = project(None);
let d = out_dir("seq-exr-scanline");
let om = OutputModule::for_format(OutputFormat::ExrSequence);
run(&p, cid, &NoFootage, &om, &d.join("f_[#####].exr"));
let bytes = std::fs::read(d.join("f_00000.exr")).expect("exr");
// Version field: 2, then the flags byte whose 0x02 bit marks a single-part tiled file.
assert_eq!(&bytes[4..6], &[2, 0], "{:02x?}", &bytes[..8]);
let meta = exr::meta::MetaData::read_from_buffered(std::io::Cursor::new(&bytes), false).expect("exr header");
assert_eq!(meta.headers[0].blocks, exr::meta::BlockDescription::ScanLines);
assert_eq!(meta.headers[0].compression, exr::compression::Compression::ZIP16);
}
/// A 32 bpc project whose only layer is a full-frame opaque solid of the given colour.
fn bright_project(color: [f32; 3]) -> (Project, ItemId) {
let mut p = Project::default();