mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 01:58:19 -06:00
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 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <brandon@storyteller.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
ebe15de141
commit
9722d77aa5
@@ -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<u8>) {
|
||||
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<u8>) {
|
||||
// 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<u8>)> {
|
||||
static CACHE: OnceLock<Mutex<std::collections::HashMap<(String, u64, u32), Arc<(u32, u32, Vec<u8>)>>>> = OnceLock::new();
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user