From 8f74c3790e814678942c990c16f98c8bf16fdc59 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sat, 17 Oct 2026 07:45:24 +0200 Subject: [PATCH] Replace non-ASCII characters in EXR layer names so ProEXR export no longer panics (#1702) --- crates/engine/src/commands/frame_export.rs | 5 +++-- crates/engine/src/tests_frame_export.rs | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/commands/frame_export.rs b/crates/engine/src/commands/frame_export.rs index c939d67e..baad9b6e 100644 --- a/crates/engine/src/commands/frame_export.rs +++ b/crates/engine/src/commands/frame_export.rs @@ -140,9 +140,10 @@ fn srgb_to_linear(c: f32) -> f32 { if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) } } -/// A unique EXR layer name (no dots: they separate the layer from the channel). +/// A unique EXR layer name (no dots: they separate the layer from the channel). Printable ASCII only: +/// the `exr` crate panics on characters it cannot store in a channel name (#1702). fn exr_name(name: &str, used: &mut Vec) -> String { - let base: String = name.chars().map(|c| if c == '.' || c.is_control() { '_' } else { c }).collect(); + let base: String = name.chars().map(|c| if c == '.' || !(c == ' ' || c.is_ascii_graphic()) { '_' } else { c }).collect(); let base = if base.trim().is_empty() { "Layer".to_string() } else { base }; let mut n = base.clone(); let mut k = 2; diff --git a/crates/engine/src/tests_frame_export.rs b/crates/engine/src/tests_frame_export.rs index 87b0864a..a45c5d44 100644 --- a/crates/engine/src/tests_frame_export.rs +++ b/crates/engine/src/tests_frame_export.rs @@ -77,6 +77,19 @@ fn proexr_writes_layer_prefixed_channels_and_the_composite() { assert!(at("R", 16, 16) < 0.5, "multiplied by 50% blue (linear)"); } +#[test] +fn proexr_names_a_non_ascii_layer_with_underscores_instead_of_panicking() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Frame", "width": 8, "height": 8, "frameRate": 24, "duration": 1})).unwrap(); + s.execute("layer.newSolid", json!({"name": "你好", "color": "#ff0000"})).unwrap(); + let dir = out_dir("frame-exr-non-ascii"); + let path = dir.join("frame.exr").to_string_lossy().to_string(); + let r = s.execute("comp.saveFrameAsExr", json!({"path": path})).unwrap(); + let names: Vec<&str> = r["channels"].as_array().unwrap().iter().map(|c| c.as_str().unwrap()).collect(); + assert!(names.contains(&"__.R") && names.contains(&"__.A"), "{names:?}"); + assert!(std::path::Path::new(&path).exists()); +} + /// Save a 32×32 comp holding one opaque solid of `color` (an sRGB-encoded triple) as ProEXR and /// return the centre pixel of the composite and of the solid's own layer: `[R, G, B]` each. fn proexr_centre(color: [f64; 3], bit_depth: u64) -> ([f32; 3], [f32; 3]) {