mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 18:46:57 -06:00
Open: cap absurd effect sizes from a file at what Format Shape allows
A .pptx or .deckcraft could give a glow, soft edge, shadow or reflection any size. A glow of ~10^16 pt made vello panic while flattening, one of 10^18 pt made rendering ask for 34 GB and abort. sane_numbers (run by repair on every opened file) now caps them like line widths and font sizes: glow 150, soft edges 100, shadows 200, reflections 100 pt; shadow scale and skew are kept sane and non-numbers become 0, on shapes and on text runs, table cells included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
22db9261f9
commit
daae74b57e
@@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort |
|
||||
| 2026-10-15 | PPTX trigger sequences keep separate On Click steps, including paragraph builds; synthetic import/edit/save/reopen and headless show regressions |
|
||||
| 2026-10-15 | CLI: .txt and .md export includes table cell text, which it used to skip silently (#814) |
|
||||
| 2026-10-15 | PDF export and rendering: `anchorCtr` centres the drawn text block — right-aligned text no longer ran past the box edge (#736) |
|
||||
|
||||
@@ -3623,3 +3623,76 @@ fn table_column_width_of_a_huge_column_is_ignored() {
|
||||
assert_eq!(table_of(&s, id).cols, vec![100.0, 100.0], "no column changes");
|
||||
}
|
||||
}
|
||||
|
||||
/// A file with absurd effect sizes opens with them capped at what Format Shape allows: drawn as
|
||||
/// they were, a glow of 10^18 pt made the renderer ask for 34 GB and abort.
|
||||
#[test]
|
||||
fn opening_a_file_caps_absurd_effect_sizes() {
|
||||
use deckcraft_model::style::{Glow, Reflection};
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 200, 100], "text": "Glow"})).unwrap();
|
||||
s.execute("shape.effects", &json!({"shadow": "outer", "reflection": "tight", "softEdges": 4, "glow": {"radius": 8}})).unwrap();
|
||||
s.execute("insert.table", &json!({"rows": 1, "cols": 1, "rect": [400, 300, 100, 40]})).unwrap();
|
||||
let huge = 1e18;
|
||||
s.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).unwrap();
|
||||
for sh in list.iter_mut() {
|
||||
if let Some(e) = sh.effects.as_mut() {
|
||||
e.glow.as_mut().unwrap().radius = huge;
|
||||
e.soft_edge = Some(f64::NAN);
|
||||
let sw = e.outer_shadow.as_mut().unwrap();
|
||||
(sw.blur, sw.dist, sw.sx) = (huge, huge, huge);
|
||||
*e.reflection.as_mut().unwrap() = Reflection { blur: huge, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.5, dist: huge };
|
||||
}
|
||||
let glow = Some(Glow { color: deckcraft_model::style::ColorRef::rgb(deckcraft_color::Rgba::BLACK), radius: huge });
|
||||
let runs: Vec<&mut deckcraft_model::text::Run> = match &mut sh.kind {
|
||||
deckcraft_model::ShapeKind::Table(t) => t
|
||||
.rows
|
||||
.iter_mut()
|
||||
.flat_map(|r| r.cells.iter_mut())
|
||||
.flat_map(|c| c.text.paragraphs.iter_mut())
|
||||
.flat_map(|p| p.runs.iter_mut())
|
||||
.collect(),
|
||||
_ => sh.text.iter_mut().flat_map(|t| t.paragraphs.iter_mut()).flat_map(|p| p.runs.iter_mut()).collect(),
|
||||
};
|
||||
for r in runs {
|
||||
r.props.glow = glow.clone();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
for format in ["deckcraft", "pptx"] {
|
||||
let bytes = cmd::file::save_bytes(&s.doc().unwrap().doc, format).unwrap();
|
||||
let mut t = Session::new();
|
||||
t.execute("file.openBytes", &json!({"name": format!("fx.{format}"), "data": cmd::base64_encode(&bytes)})).unwrap();
|
||||
let st = t.doc().unwrap();
|
||||
let slide = st.doc.slides.last().unwrap();
|
||||
let mut glows = vec![];
|
||||
deckcraft_model::walk(&slide.shapes, &mut |sh, _| {
|
||||
if let Some(e) = &sh.effects {
|
||||
glows.extend(e.glow.as_ref().map(|g| g.radius));
|
||||
assert!(e.soft_edge.is_none_or(|v| (0.0..=100.0).contains(&v)), "{format}: {:?}", e.soft_edge);
|
||||
if let Some(sw) = &e.outer_shadow {
|
||||
assert!(sw.blur <= 200.0 && sw.dist <= 200.0 && sw.sx <= 10.0, "{format}: {sw:?}");
|
||||
}
|
||||
if let Some(r) = &e.reflection {
|
||||
assert!(r.blur <= 100.0 && r.dist <= 100.0, "{format}: {r:?}");
|
||||
}
|
||||
}
|
||||
for p in sh.text.iter().flat_map(|t| t.paragraphs.iter()) {
|
||||
glows.extend(p.runs.iter().filter_map(|r| r.props.glow.as_ref().map(|g| g.radius)));
|
||||
}
|
||||
if let deckcraft_model::ShapeKind::Table(tb) = &sh.kind {
|
||||
let cells = tb.rows.iter().flat_map(|r| r.cells.iter()).flat_map(|c| c.text.paragraphs.iter());
|
||||
glows.extend(cells.flat_map(|p| p.runs.iter()).filter_map(|r| r.props.glow.as_ref().map(|g| g.radius)));
|
||||
}
|
||||
});
|
||||
assert!(!glows.is_empty(), "{format}: the glows were kept");
|
||||
assert!(glows.iter().all(|g| (0.0..=150.0).contains(g)), "{format}: {glows:?}");
|
||||
// Capped, the slide renders (checked last: uncapped, this is what ran out of memory).
|
||||
let i = st.doc.slides.len() - 1;
|
||||
t.execute("file.render", &json!({"slide": i, "scale": 0.1})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +350,59 @@ fn sane_xfrm(x: &mut deckcraft_model::Xfrm) {
|
||||
|
||||
fn sane_run(p: &mut deckcraft_model::text::RunProps) {
|
||||
p.size = p.size.filter(|v| v.is_finite() && *v > 0.0).map(|v| v.clamp(1.0, 4000.0));
|
||||
for s in p.shadow.iter_mut().chain(p.inner_shadow.iter_mut()) {
|
||||
sane_shadow(s);
|
||||
}
|
||||
if let Some(g) = p.glow.as_mut() {
|
||||
g.radius = effect_size(g.radius, MAX_GLOW);
|
||||
}
|
||||
if let Some(r) = p.reflection.as_mut() {
|
||||
sane_reflection(r);
|
||||
}
|
||||
p.soft_edge = p.soft_edge.map(|v| effect_size(v, MAX_SOFT_EDGE));
|
||||
}
|
||||
|
||||
/// The largest effect sizes, in points: what Format Shape and `shape.effects` allow. A file can
|
||||
/// say anything, and the renderer's memory grows with these (a glow of 10^18 pt asked for 34 GB).
|
||||
const MAX_GLOW: f64 = 150.0;
|
||||
const MAX_SOFT_EDGE: f64 = 100.0;
|
||||
const MAX_SHADOW: f64 = 200.0;
|
||||
const MAX_REFLECTION: f64 = 100.0;
|
||||
|
||||
/// `v` within 0..=`max`; not a number is 0.
|
||||
fn effect_size(v: f64, max: f64) -> f64 {
|
||||
if v.is_finite() { v.clamp(0.0, max) } else { 0.0 }
|
||||
}
|
||||
|
||||
fn sane_shadow(s: &mut deckcraft_model::style::Shadow) {
|
||||
s.blur = effect_size(s.blur, MAX_SHADOW);
|
||||
s.dist = effect_size(s.dist, MAX_SHADOW);
|
||||
s.dir = if s.dir.is_finite() { s.dir.rem_euclid(360.0) } else { 0.0 };
|
||||
// Perspective scale (1 = 100%) and skew (degrees).
|
||||
for v in [&mut s.sx, &mut s.sy] {
|
||||
*v = if v.is_finite() { v.clamp(-10.0, 10.0) } else { 1.0 };
|
||||
}
|
||||
for v in [&mut s.kx, &mut s.ky] {
|
||||
*v = if v.is_finite() { v.clamp(-89.0, 89.0) } else { 0.0 };
|
||||
}
|
||||
}
|
||||
|
||||
fn sane_reflection(r: &mut deckcraft_model::style::Reflection) {
|
||||
r.blur = effect_size(r.blur, MAX_REFLECTION);
|
||||
r.dist = effect_size(r.dist, MAX_REFLECTION);
|
||||
}
|
||||
|
||||
fn sane_effects(e: &mut deckcraft_model::Effects) {
|
||||
for s in e.outer_shadow.iter_mut().chain(e.inner_shadow.iter_mut()) {
|
||||
sane_shadow(s);
|
||||
}
|
||||
if let Some(g) = e.glow.as_mut() {
|
||||
g.radius = effect_size(g.radius, MAX_GLOW);
|
||||
}
|
||||
e.soft_edge = e.soft_edge.map(|v| effect_size(v, MAX_SOFT_EDGE));
|
||||
if let Some(r) = e.reflection.as_mut() {
|
||||
sane_reflection(r);
|
||||
}
|
||||
}
|
||||
|
||||
fn sane_shapes(shapes: &mut [deckcraft_model::Shape], depth: usize) {
|
||||
@@ -363,6 +416,9 @@ fn sane_shapes(shapes: &mut [deckcraft_model::Shape], depth: usize) {
|
||||
if let Some(l) = sh.line.as_mut() {
|
||||
l.width = l.width.filter(|v| v.is_finite() && *v >= 0.0).map(|v| v.min(1584.0));
|
||||
}
|
||||
if let Some(e) = sh.effects.as_mut() {
|
||||
sane_effects(e);
|
||||
}
|
||||
if let Some(t) = sh.text.as_mut() {
|
||||
for para in &mut t.paragraphs {
|
||||
sane_run(&mut para.end_props);
|
||||
@@ -381,6 +437,14 @@ fn sane_shapes(shapes: &mut [deckcraft_model::Shape], depth: usize) {
|
||||
sane_xfrm(child);
|
||||
sane_shapes(children, depth + 1);
|
||||
}
|
||||
deckcraft_model::ShapeKind::Table(t) => {
|
||||
for para in t.rows.iter_mut().flat_map(|r| r.cells.iter_mut()).flat_map(|c| c.text.paragraphs.iter_mut()) {
|
||||
sane_run(&mut para.end_props);
|
||||
for r in &mut para.runs {
|
||||
sane_run(&mut r.props);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user