mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-08 17:41:22 +00:00
UI: ScriptUI images decoded within a size limit; effect detection answers each comp once
ScriptUI decoded any image and converted it to full RGBA without a size check; images are decoded through image's limits (4096 px per side and a matching allocation cap). comp_has_effects walked a nested comp again for every layer using it; each comp is now answered once.
This commit is contained in:
@@ -438,14 +438,22 @@ pub trait RemoteFrames: Send + Sync {
|
||||
|
||||
/// Whether a comp (or a comp it nests) has a layer with enabled effects.
|
||||
pub fn comp_has_effects(project: &Project, comp: ItemId) -> bool {
|
||||
fn walk(p: &Project, comp: ItemId, depth: usize) -> bool {
|
||||
// Each comp is answered once: a comp nested by many layers at each of several levels was
|
||||
// walked again for every path to it.
|
||||
fn walk(p: &Project, comp: ItemId, depth: usize, seen: &mut std::collections::HashMap<ItemId, bool>) -> bool {
|
||||
if let Some(&r) = seen.get(&comp) {
|
||||
return r;
|
||||
}
|
||||
seen.insert(comp, false);
|
||||
let Some(c) = p.comp(comp) else { return false };
|
||||
c.layers.iter().any(|l| {
|
||||
let r = c.layers.iter().any(|l| {
|
||||
l.switches.effects && l.effects().is_some_and(|fx| fx.groups().any(|g| g.enabled))
|
||||
|| depth < 16 && matches!(l.source, effectcraft_engine::project::LayerSource::Comp { item } if walk(p, item, depth + 1))
|
||||
})
|
||||
|| depth < 16 && matches!(l.source, effectcraft_engine::project::LayerSource::Comp { item } if walk(p, item, depth + 1, seen))
|
||||
});
|
||||
seen.insert(comp, r);
|
||||
r
|
||||
}
|
||||
walk(project, comp, 0)
|
||||
walk(project, comp, 0, &mut std::collections::HashMap::new())
|
||||
}
|
||||
|
||||
pub struct Frames {
|
||||
|
||||
@@ -1084,3 +1084,23 @@ fn timing_markers_and_expressions_still_change_the_frames() {
|
||||
s.execute("layer.addMarker", json!({"comp": outer.0, "layers": [solid], "time": 0.75})).unwrap();
|
||||
assert_ne!(at(&s), o2, "an expression can read the marker, so the frames change");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effect_detection_answers_each_comp_once() {
|
||||
// Sixteen levels, each comp holding eight layers of the one below and no effects: 8^16 walks
|
||||
// without a memo. Answered at once.
|
||||
use effectcraft_engine::project::{Comp, ItemKind, LayerSource, build};
|
||||
use effectcraft_engine::time::{FrameRate, Tick};
|
||||
let mut p = Project::default();
|
||||
let base = Comp::new(8, 8, FrameRate::new(25, 1), Tick::from_seconds_f64(1.0));
|
||||
let mut below = p.add_item("C", effectcraft_engine::color::Label::Sandstone, None, ItemKind::Comp(base.clone().into()));
|
||||
for _ in 0..16 {
|
||||
let layers: Vec<_> = (0..8).map(|_| build::layer(&mut p, &base, "a", LayerSource::Comp { item: below }, (0, 0), None)).collect();
|
||||
let mut c = base.clone();
|
||||
c.layers = layers.into();
|
||||
below = p.add_item("C", effectcraft_engine::color::Label::Sandstone, None, ItemKind::Comp(c.into()));
|
||||
}
|
||||
let start = std::time::Instant::now();
|
||||
assert!(!comp_has_effects(&p, below));
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(5));
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ fn image_texture(ctx: &egui::Context, img: &ImageRef, services: &dyn Services) -
|
||||
if let Some(cached) = ctx.data(|d| d.get_temp::<Option<egui::TextureHandle>>(id)) {
|
||||
return cached;
|
||||
}
|
||||
let tex = img.bytes(services).and_then(|b| image::load_from_memory(&b).ok()).map(|d| {
|
||||
let tex = img.bytes(services).and_then(|b| decode_ui_image(&b)).map(|d| {
|
||||
let rgba = d.to_rgba8();
|
||||
let ci = egui::ColorImage::from_rgba_unmultiplied([rgba.width() as usize, rgba.height() as usize], rgba.as_raw());
|
||||
ctx.load_texture(format!("scriptui-image-{}", img.name), ci, egui::TextureOptions::LINEAR)
|
||||
@@ -338,6 +338,21 @@ fn image_texture(ctx: &egui::Context, img: &ImageRef, services: &dyn Services) -
|
||||
tex
|
||||
}
|
||||
|
||||
/// The largest side of a ScriptUI image (icons and banners are far smaller).
|
||||
const MAX_UI_IMAGE_SIDE: u32 = 4096;
|
||||
|
||||
/// A ScriptUI image decoded within [`MAX_UI_IMAGE_SIDE`] per side and a matching allocation
|
||||
/// limit, checked from the header before the pixels (and their RGBA copy) are allocated.
|
||||
fn decode_ui_image(bytes: &[u8]) -> Option<image::DynamicImage> {
|
||||
let mut r = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_image_width = Some(MAX_UI_IMAGE_SIDE);
|
||||
limits.max_image_height = Some(MAX_UI_IMAGE_SIDE);
|
||||
limits.max_alloc = Some(u64::from(MAX_UI_IMAGE_SIDE) * u64::from(MAX_UI_IMAGE_SIDE) * 16);
|
||||
r.limits(limits);
|
||||
r.decode().ok()
|
||||
}
|
||||
|
||||
/// An image control's or icon button's image, fitted (aspect kept) and centred in `r`.
|
||||
fn paint_image(ui: &egui::Ui, r: Rect, img: &ImageRef, services: &dyn Services) -> bool {
|
||||
let Some(tex) = image_texture(ui.ctx(), img, services) else { return false };
|
||||
|
||||
Reference in New Issue
Block a user