diff --git a/crates/engine/src/commands/render_queue.rs b/crates/engine/src/commands/render_queue.rs index 66fa0e2d..8abc855c 100644 --- a/crates/engine/src/commands/render_queue.rs +++ b/crates/engine/src/commands/render_queue.rs @@ -219,6 +219,9 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, FORMATS))?; om.set_format(f); } + // Before Channels: the WebM codec decides whether alpha can be written, so an explicit + // `channels: rgba` with `webmCodec: av1` is refused rather than dropped (#166). + any |= apply_codec_options(om, p, cmd)?; if let Some(c) = str_p(p, "channels") { om.channels = match c.to_ascii_lowercase().replace([' ', '+'], "").as_str() { "rgb" => Channels::Rgb, @@ -227,7 +230,11 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< _ => return Err(bad(cmd, "channels: rgb|rgba|alpha")), }; if om.channels == Channels::Rgba && !om.supports_alpha() { - return Err(bad(cmd, format!("{} has no alpha channel", om.format.label()))); + let what = match om.format { + OutputFormat::WebM => format!("{} WebM has no alpha channel (VP9 WebM has)", om.webm_codec.label()), + f => format!("{} has no alpha channel", f.label()), + }; + return Err(bad(cmd, what)); } } if let Some(c) = str_p(p, "color").or(str_p(p, "alphaMode")) { @@ -237,7 +244,6 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< _ => return Err(bad(cmd, "color: straight|premultiplied")), }; } - any |= apply_codec_options(om, p, cmd)?; if let Some(q) = f_p(p, "quality") { om.quality = q.clamp(1.0, 100.0) as u8; } diff --git a/crates/engine/src/tests_codec_options.rs b/crates/engine/src/tests_codec_options.rs index 80703063..8adabba5 100644 --- a/crates/engine/src/tests_codec_options.rs +++ b/crates/engine/src/tests_codec_options.rs @@ -110,6 +110,25 @@ fn output_to_keeps_a_format_that_writes_the_extension() { assert_eq!((&r["output"]["format"], &r["outputModuleSummary"]), (&json!("Hevc"), &a["outputModuleSummary"]), "{r}"); } +/// An explicit RGB + Alpha request with the AV1 WebM codec (which has no alpha) is refused, not +/// silently rendered opaque (#166). Without `channels`, AV1 WebM renders RGB as before. +#[test] +fn av1_webm_refuses_requested_alpha() { + let mut s = session(); + let n = s.project.render_queue.len(); + let e = s.execute("renderQueue.add", json!({"format": "webm", "webmCodec": "av1", "channels": "rgba"})).unwrap_err().to_string(); + assert!(e.contains("AV1 WebM has no alpha channel"), "{e}"); + assert_eq!(s.project.render_queue.len(), n, "nothing queued"); + let a = s.execute("renderQueue.add", json!({"format": "webm", "channels": "rgba"})).unwrap(); + let e = s.execute("renderQueue.setOutputModule", json!({"item": a["item"], "webmCodec": "av1", "channels": "rgba"})).unwrap_err().to_string(); + assert!(e.contains("AV1 WebM has no alpha channel"), "{e}"); + let om = &s.project.render_queue.last().unwrap().output; + assert_eq!((om.webm_codec, om.channels), (WebmVideoCodec::Vp9, Channels::Rgba), "unchanged"); + s.execute("renderQueue.add", json!({"format": "webm", "webmCodec": "av1"})).unwrap(); + let om = &s.project.render_queue.last().unwrap().output; + assert_eq!((om.webm_codec, om.channels), (WebmVideoCodec::Av1, Channels::Rgb)); +} + /// A `comp` that names no composition says so, rather than "no active composition" (#155). #[test] fn an_unknown_comp_is_named_in_the_error() {