From 9722d77aa552acdeb449bc47940fdab725fc5be0 Mon Sep 17 00:00:00 2001 From: Earl Co Date: Fri, 23 Oct 2026 16:52:30 +0200 Subject: [PATCH] security(engine): bound graphic template thumbnails (#2887) * security(engine): bound graphic template thumbnails * graphic templates: scale a capped thumbnail to fit both sides of the canvas Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/engine/src/graphic_templates.rs | 20 +++++++++++++++++--- crates/engine/src/graphic_templates_tests.rs | 7 +++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/graphic_templates.rs b/crates/engine/src/graphic_templates.rs index 00b66c72f..01fa146a4 100644 --- a/crates/engine/src/graphic_templates.rs +++ b/crates/engine/src/graphic_templates.rs @@ -44,6 +44,7 @@ fn query(id: &'static str, label: &'static str, params: &'static str, run: Run) } const G: &[&str] = &["Graphics and Titles"]; +const MAX_THUMBNAIL_DIMENSION: u32 = 1920; fn has_graphic(s: &Session) -> std::result::Result<(), String> { has_seq(s)?; @@ -149,8 +150,7 @@ fn entry_json(e: &LibraryEntry) -> Value { /// Render a template's thumbnail (static: rolls are shown at rest) as straight sRGB RGBA8, /// `w` pixels wide with the template's aspect ratio. Returns (width, height, pixels). pub fn thumbnail(t: &GraphicsTemplate, w: u32) -> (u32, u32, Vec) { - let w = w.clamp(16, 1920); - let h = ((w as f64 * t.canvas[1] as f64 / t.canvas[0].max(1) as f64).round() as u32).max(1); + let (w, h) = thumbnail_dimensions(t.canvas, w); register_fonts(t); let mut meta = t.graphic.clone(); meta.roll.mode = RollMode::Off; @@ -171,11 +171,25 @@ pub fn thumbnail(t: &GraphicsTemplate, w: u32) -> (u32, u32, Vec) { // a moment into the clip, so intro animations have settled a little let at = Tick((t.duration.0 / 2).min(filmcraft_time::TICKS_PER_SECOND)); let mut img = filmcraft_render::image::Image::new(w as usize, h as usize); - let k = w as f64 / t.canvas[0].max(1) as f64; + // fit both sides: a capped height leaves `w / canvas width` too large for a tall canvas + let k = (w as f64 / t.canvas[0].max(1) as f64).min(h as f64 / t.canvas[1].max(1) as f64); filmcraft_render::graphic_clip::render_graphic(&it, at, (t.canvas[0], t.canvas[1]), &Affine::scale(k, k), &mut img); (w, h, img.to_rgba8()) } +/// Fit a requested thumbnail width within a bounded rectangle without trusting the template's +/// aspect ratio to derive an unbounded second dimension. +fn thumbnail_dimensions(canvas: [u32; 2], requested_width: u32) -> (u32, u32) { + let mut w = requested_width.clamp(16, MAX_THUMBNAIL_DIMENSION); + let aspect = canvas[0].max(1) as f64 / canvas[1].max(1) as f64; + let mut h = (w as f64 / aspect).round().clamp(1.0, u32::MAX as f64) as u32; + if h > MAX_THUMBNAIL_DIMENSION { + h = MAX_THUMBNAIL_DIMENSION; + w = (h as f64 * aspect).round().clamp(1.0, MAX_THUMBNAIL_DIMENSION as f64) as u32; + } + (w, h) +} + /// Cached thumbnails by template id + content + width (the UI asks every frame). pub fn thumbnail_cached(t: &GraphicsTemplate, w: u32) -> Arc<(u32, u32, Vec)> { static CACHE: OnceLock)>>>> = OnceLock::new(); diff --git a/crates/engine/src/graphic_templates_tests.rs b/crates/engine/src/graphic_templates_tests.rs index e49c0ec8f..47a7c1528 100644 --- a/crates/engine/src/graphic_templates_tests.rs +++ b/crates/engine/src/graphic_templates_tests.rs @@ -143,6 +143,13 @@ fn builtin_templates_apply_and_thumbnail() { } } +#[test] +fn template_thumbnail_dimensions_fit_a_bounded_rectangle() { + assert_eq!(super::thumbnail_dimensions([1920, 1080], 160), (160, 90)); + assert_eq!(super::thumbnail_dimensions([1, 16_384], 1920), (1, 1920)); + assert_eq!(super::thumbnail_dimensions([16_384, 1], 1920), (1920, 1)); +} + #[test] fn responsive_time_protects_intro_and_outro_when_trimmed() { let mut s = demo("gt-time");