From 7b424a463bce78aee300c2dde23ed7ba42f40a38 Mon Sep 17 00:00:00 2001 From: Zach Berry <1112351+zberry@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:46:42 -0500 Subject: [PATCH 1/3] [HH9] picture.reset: document and pin what it keeps (frame shape, size, position) PowerPoint: Reset Picture discards the picture's formatting changes (corrections, recolor, artistic effect, picture-style effects and outline); Reset Picture & Size also restores the size. The frame shape set by Change Shape is not touched. Ours already left the geometry alone (the reported geometry wipe does not reproduce); effects, outline and crop are picture formatting that PowerPoint discards too, so they stay reset (declined: parity). Added the regression test and the command doc. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/shape.rs | 10 +++++++++- crates/engine/src/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/shape.rs b/crates/engine/src/cmd/shape.rs index f3d41640..c8d1d014 100644 --- a/crates/engine/src/cmd/shape.rs +++ b/crates/engine/src/cmd/shape.rs @@ -165,7 +165,15 @@ pub fn specs() -> Vec { has_selection, picture_adjust ), - cmd!("picture.reset", "Reset Picture", ["Picture Format", "Adjust"], None, "{ids?}", has_selection, picture_reset), + cmd!( + "picture.reset", + "Reset Picture", + ["Picture Format", "Adjust"], + None, + "{ids?} (discards the picture's formatting: adjustments, artistic effect, crop, transparency, picture-style effects and outline; the frame shape from Change Shape or Crop to Shape, the size and the position stay)", + has_selection, + picture_reset + ), cmd!( "picture.change", "Change Picture", diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 4dfc21f4..deeca260 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -5646,3 +5646,29 @@ fn invalid_ids_array_never_falls_back_to_the_selection() { assert!(s.doc().unwrap().shape(deckcraft_model::ShapeId(a as u32)).is_some(), "shape deleted for {ids}"); } } + +/// HH9: Reset Picture discards the picture's formatting (adjustments, artistic effect, picture +/// style effects and outline) but not the frame's shape (Change Shape / Crop to Shape). +#[test] +fn picture_reset_keeps_the_frame_shape() { + let mut img = image::RgbaImage::new(40, 30); + img.pixels_mut().for_each(|p| *p = image::Rgba([200, 30, 30, 255])); + let mut png = Vec::new(); + image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); + let mut s = Session::with_new(); + s.execute("slide.new", &json!({"layout": "blank"})).unwrap(); + s.execute("insert.picture", &json!({"name": "a.png", "data": cmd::base64_encode(&png)})).unwrap(); + s.execute("shape.change", &json!({"preset": "roundRect"})).unwrap(); + s.execute("picture.adjust", &json!({"brightness": 0.3})).unwrap(); + let geom = s.doc().unwrap().selected_shapes()[0].geom.clone(); + assert_ne!(geom, deckcraft_model::Geom::preset("rect")); + s.execute("picture.reset", &json!({})).unwrap(); + let sh = s.doc().unwrap().selected_shapes()[0].clone(); + assert_eq!(sh.geom, geom, "the frame shape stays"); + match &sh.kind { + deckcraft_model::ShapeKind::Picture { fill } => assert_eq!(fill.adjust, Default::default()), + _ => panic!("not a picture"), + } + let doc = command_specs().iter().find(|c| c.id == "picture.reset").unwrap().params; + assert!(doc.contains("frame shape"), "{doc}"); +} From 0efd6f1b853395ee753dd2848aab96db08643bba Mon Sep 17 00:00:00 2001 From: Zach Berry <1112351+zberry@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:14:43 -0500 Subject: [PATCH 2/3] [HH12] Pin that exporting a slide again overwrites the PNG Not a bug: file.export writes the second PNG over the first with the edit in it. The test pins it (export, edit, export to the same path, files differ). PowerPoint: Save As / Export to an existing file name replaces the file. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/tests.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index deeca260..ac9183c3 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -5672,3 +5672,33 @@ fn picture_reset_keeps_the_frame_shape() { let doc = command_specs().iter().find(|c| c.id == "picture.reset").unwrap().params; assert!(doc.contains("frame shape"), "{doc}"); } + +/// HH12: exporting the same slide twice to one PNG path after an edit overwrites the file with +/// the new picture (not a bug: the second file differs). +#[test] +fn exporting_a_slide_again_overwrites_the_png_with_the_edit() { + /// Removes the folder when the test ends, a failed assertion included. + struct Cleanup(std::path::PathBuf); + impl Drop for Cleanup { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + // A folder of this run's own: created exclusively (never one an earlier or parallel run left). + let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos()); + let dir = (0..100) + .map(|k| std::env::temp_dir().join(format!("deckcraft-hh12-{}-{stamp}-{k}", std::process::id()))) + .find(|d| std::fs::create_dir(d).is_ok()) + .expect("a fresh temporary folder"); + let _cleanup = Cleanup(dir.clone()); + let png = dir.join("slide.png"); + let mut s = session(); + let path = png.to_string_lossy().to_string(); + s.execute("file.export", &json!({"path": path, "format": "png", "slide": 0, "scale": 0.5})).unwrap(); + let first = std::fs::read(&png).unwrap(); + s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 400, 300]})).unwrap(); + s.execute("shape.fill", &json!({"color": "#FF0000"})).unwrap(); + s.execute("file.export", &json!({"path": path, "format": "png", "slide": 0, "scale": 0.5})).unwrap(); + let second = std::fs::read(&png).unwrap(); + assert_ne!(first, second, "the second export replaced the file with the edited slide"); +} From 3356bc071c048420fab0ed526046f037bdbe1aab Mon Sep 17 00:00:00 2001 From: Zach Berry <1112351+zberry@users.noreply.github.com> Date: Mon, 19 Oct 2026 03:12:13 -0500 Subject: [PATCH 3/3] [SS1] Pin that an inserted action button's fill follows the theme It already does: insert.actionButton gives the shape the accent1 style with no explicit fill, as PowerPoint's gallery button has, so a theme change recolours it. No code change; a test pins it. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/tests.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ac9183c3..f70213ae 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -5702,3 +5702,21 @@ fn exporting_a_slide_again_overwrites_the_png_with_the_edit() { let second = std::fs::read(&png).unwrap(); assert_ne!(first, second, "the second export replaced the file with the edited slide"); } + +#[test] +fn action_button_fill_follows_the_theme() { + let mut s = session(); + let id = s.execute("insert.actionButton", &json!({"kind": "next"})).unwrap()["id"].clone(); + let fill = |s: &mut Session| { + let r = s.execute("slide.inspect", &json!({})).unwrap(); + r["shapes"].as_array().unwrap().iter().find(|x| x["id"] == id).unwrap()["fill"].clone() + }; + let before = fill(&mut s); + assert!(before.as_str().is_some_and(|f| f.starts_with('#')), "{before}"); + s.execute("design.theme", &json!({"name": "Ember"})).unwrap(); + let after = fill(&mut s); + assert_ne!(before, after, "the button takes the new theme's accent1"); + let st = s.doc().unwrap(); + let sh = st.doc.slides[0].shapes.iter().find(|x| x.fill.is_none() && x.style.is_some()).unwrap(); + assert!(sh.fill.is_none(), "no explicit fill: the style binds it to accent1"); +}