From aece0970969259510d98bfc4d22d4fd45d2b1018 Mon Sep 17 00:00:00 2001 From: Dominik Strebel Date: Sat, 24 Oct 2026 14:07:43 +0200 Subject: [PATCH 1/4] Render: a picture's fill shows behind its transparent pixels PowerPoint draws a picture's own fill (p:pic/p:spPr) behind the picture, so it shows through its transparent pixels: a synthetic deck with one half-transparent PNG three times (no fill, an orange fill, a yellow fill with a blue outline), exported as PDF, shows the orange and yellow boxes behind the image. DeckCraft drew the outline but never the fill. Old equation objects use this: an OLE object (p:oleObj, Equation.3) has a fallback picture whose spPr gives it a coloured box behind the equation's transparent metafile. The reader took only the picture's image as the object's preview and dropped its fill and line. - The shape-fill loop moves into Renderer::fill_geometry; a picture and an embedded object's preview paint their own explicit fill with it before the image (a style fill alone still draws nothing, as before). - An embedded object without a fill or line of its own takes them from its fallback picture's spPr; its frame keeps its own position. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 + crates/pptx/src/read/shapes.rs | 11 +++++ crates/pptx/tests/it/ole.rs | 40 +++++++++++++++++ crates/render/src/lib.rs | 82 +++++++++++++++++++++++----------- crates/render/src/tests.rs | 32 +++++++++++++ 5 files changed, 140 insertions(+), 27 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d54c160f..a4062400 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -134,6 +134,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-24 | A picture's own fill shows behind its transparent pixels, and an embedded object (an old equation, `p:oleObj`) takes the fill and outline of its fallback picture, as PowerPoint for Mac draws them (synthetic probe: a half-transparent PNG on orange and on yellow with an outline); on a private 166-deck corpus 51 slides of equations on coloured boxes move closer to PowerPoint's PDF, none further | | 2026-10-23 | Format Background ▸ Apply to All also sets the background of the slide masters the slides use (and clears their layouts' overrides), so new slides keep it (#891) | | 2026-10-22 | Table text takes the master's other-text style (`otherStyle`), shapes and text boxes the presentation's default text style, as PowerPoint for Mac draws them (synthetic probe: 14 pt cells under a 14 pt `otherStyle` and a 26 pt `defaultTextStyle`); a private deck's table had been drawn 20 pt instead of 18 | | 2026-10-18 | Find (Ctrl+F): "Find what:" has the keyboard as soon as the dialog opens, and keeps it after Enter finds a match, so the search can be typed and changed without clicking the field (#951) | @@ -272,6 +273,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-15 | trivial | Progress log: PPTX trigger click-step round trip. Feature scores and milestone names unchanged | | 2026-10-16 | trivial | Progress log: PNG export with a transparent background (#784) | | 2026-10-16 | trivial | Progress log: a `.ppsx` from the system starts the slide show (#862) | +| 2026-10-24 | trivial | Progress log: picture and embedded object fills | | 2026-10-22 | trivial | Progress log: table text takes the master's other-text style | | 2026-10-16 | trivial | Progress log: responsive mouse-wheel navigation on the fitted canvas | | 2026-10-21 | trivial | Progress log: theme `spDef` anchor no longer applied to a file's own shapes | diff --git a/crates/pptx/src/read/shapes.rs b/crates/pptx/src/read/shapes.rs index 0d6c8f8c..3bca8220 100644 --- a/crates/pptx/src/read/shapes.rs +++ b/crates/pptx/src/read/shapes.rs @@ -447,6 +447,17 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S pics.into_iter().find_map(|b| dml::media_ref(imp, part, b)) } .or_else(|| gd.and_then(|g| g.find("oleObj")).and_then(|o| vml_preview(imp, part, o.attr("spid")?))); + // An embedded object's fallback picture (`p:oleObj/p:pic`) can carry a fill and an outline, + // which PowerPoint draws behind and around the preview (old equation objects on coloured + // boxes). The frame's own position stays. + if let Some(pr) = gd.and_then(|g| g.find("oleObj")).and_then(|o| o.find("pic")).and_then(|p| p.child("spPr")) { + if s.fill.is_none() { + s.fill = dml::fill_of(imp, part, pr); + } + if s.line.is_none() { + s.line = pr.child("ln").map(|ln| dml::line(imp, part, ln)); + } + } let label = if let Some(o) = gd.and_then(|g| g.find("oleObj")) { o.attr("progId").or(o.attr("name")).unwrap_or("Object").to_string() } else if uri.ends_with("/diagram") { diff --git a/crates/pptx/tests/it/ole.rs b/crates/pptx/tests/it/ole.rs index 5c0bc1db..1b0ef779 100644 --- a/crates/pptx/tests/it/ole.rs +++ b/crates/pptx/tests/it/ole.rs @@ -96,3 +96,43 @@ fn an_old_ole_object_shows_its_vml_preview() { fn an_ole_object_without_a_matching_vml_shape_has_no_preview() { assert_eq!(object_preview(&ole_deck("_x0000_s1")), None); } + +/// PowerPoint 2010 and later: `p:oleObj` with a fallback `p:pic` inside `mc:AlternateContent`, whose +/// `spPr` can give the object a fill and an outline (old equation objects on coloured boxes). +#[test] +fn an_ole_objects_fallback_picture_gives_it_its_fill_and_outline() { + let pic = |spr: &str| { + format!( + r#"{spr}"# + ) + }; + let deck = |frame: String| { + with_parts(&[("ppt/media/eq.png", png()), ("ppt/embeddings/oleObject1.bin", vec![0; 16])], move |name, s| { + if name == "ppt/slides/slide1.xml" { + s.replacen("", &format!("{frame}"), 1) + } else if name == "ppt/slides/_rels/slide1.xml.rels" { + s.replacen( + "", + r#""#, + 1, + ) + } else { + s + } + }) + }; + let object = |bytes: Vec| { + let p = deckcraft_pptx::import(&bytes).expect("import"); + let s = p.slides[0].shapes.iter().find(|s| s.name == "Object 3").expect("object").clone(); + assert!(matches!(&s.kind, ShapeKind::Opaque { preview: Some(_), label, .. } if label == "Equation.3"), "{:?}", s.kind); + s + }; + let filled = object(deck(pic( + r#""#, + ))); + let orange = deckcraft_model::style::Fill::solid(deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::rgb(0xFF, 0x99, 0))); + assert_eq!(filled.fill, Some(orange)); + assert_eq!(filled.line.as_ref().and_then(|l| l.width), Some(1.0)); + // Without a fill of its own the object stays unfilled. + assert_eq!(object(deck(pic(""))).fill, None); +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index c47d2b6b..37102d9b 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1024,6 +1024,51 @@ impl Renderer { } } + /// Fill the filled sub-paths of `geo` (a `w` × `h` shape placed by `m`) with `fl`, faded per + /// sub-path by `fade`: `useBgFill` shows the slide background through the shape and `grpFill` + /// the group's fill. + #[allow(clippy::too_many_arguments)] + fn fill_geometry( + &self, + ctx: &mut RenderContext, + f: &Frame, + rctx: &Ctx, + fl: &Fill, + fill_ph: Option, + geo: &Geometry, + (w, h): (f64, f64), + m: Affine, + fade: &dyn Fn(usize) -> deckcraft_geom::morph::Fade, + ) { + for (i, sp) in geo.paths.iter().enumerate() { + if sp.fill == FillMode::None || fade(i).fill <= 0.0 { + continue; + } + ctx.set_transform(m); + ctx.set_fill_rule(if sp.even_odd { peniko::Fill::EvenOdd } else { peniko::Fill::NonZero }); + match (fl, &self.bg) { + // `useBgFill`: the slide's background, laid out on the slide (not the + // shape), shows through the shape. + (Fill::Background, Some((bg, ph, view))) => { + let slide = Rect::new(0.0, 0.0, f.pres.slide_size.width, f.pres.slide_size.height); + let path = (view.inverse() * m) * sp.path.clone(); + paint::fill_path(ctx, rctx, bg, *ph, &path, slide, *view, sp.fill, fade(i).fill, f.pres); + } + // `grpFill`: the group's fill, laid out over the whole group. + (Fill::Group, _) => { + if let Some((gf, gm, gbox)) = &self.group_fill + && gm.determinant().abs() > 1e-12 + { + let path = (gm.inverse() * m) * sp.path.clone(); + paint::fill_path(ctx, rctx, gf, None, &path, *gbox, *gm, sp.fill, fade(i).fill, f.pres); + } + } + _ => paint::fill_path(ctx, rctx, fl, fill_ph, &sp.path, Rect::new(0.0, 0.0, w, h), m, sp.fill, fade(i).fill, f.pres), + } + ctx.set_fill_rule(peniko::Fill::NonZero); + } + } + #[allow(clippy::too_many_arguments)] fn geometry_shape( &mut self, @@ -1120,6 +1165,10 @@ impl Renderer { // Fill. match &s.kind { ShapeKind::Picture { fill: pf } => { + // The picture's own fill shows behind its transparent pixels, as in PowerPoint. + if let Some(fl) = s.fill.as_ref().filter(|f| !f.is_none()) { + self.fill_geometry(ctx, f, rctx, fl, None, &geo, (w, h), m, &fade); + } ctx.set_transform(m); let bounds = Rect::new(0.0, 0.0, w, h); ctx.push_clip_layer(&outline); @@ -1130,6 +1179,11 @@ impl Renderer { // An object DeckCraft doesn't model (an embedded OLE object, an unknown graphic): its // preview picture, stretched over the frame, as PowerPoint draws it. ShapeKind::Opaque { preview: Some(media), .. } => { + // An embedded object's fallback picture can carry a fill (old equation objects + // drawn on coloured boxes): behind the preview, as for a picture. + if let Some(fl) = s.fill.as_ref().filter(|f| !f.is_none()) { + self.fill_geometry(ctx, f, rctx, fl, None, &geo, (w, h), m, &fade); + } ctx.set_transform(m); let pf = deckcraft_model::style::PictureFill { media: *media, ..Default::default() }; paint::picture(ctx, f.pres, &pf, Rect::new(0.0, 0.0, w, h), m, None); @@ -1154,33 +1208,7 @@ impl Renderer { if let Some(fl) = &fill && !(empty_ph && f.opts.edit && s.fill.is_none()) { - for (i, sp) in geo.paths.iter().enumerate() { - if sp.fill == FillMode::None || fade(i).fill <= 0.0 { - continue; - } - ctx.set_transform(m); - ctx.set_fill_rule(if sp.even_odd { peniko::Fill::EvenOdd } else { peniko::Fill::NonZero }); - match (fl, &self.bg) { - // `useBgFill`: the slide's background, laid out on the slide (not the - // shape), shows through the shape. - (Fill::Background, Some((bg, ph, view))) => { - let slide = Rect::new(0.0, 0.0, f.pres.slide_size.width, f.pres.slide_size.height); - let path = (view.inverse() * m) * sp.path.clone(); - paint::fill_path(ctx, rctx, bg, *ph, &path, slide, *view, sp.fill, fade(i).fill, f.pres); - } - // `grpFill`: the group's fill, laid out over the whole group. - (Fill::Group, _) => { - if let Some((gf, gm, gbox)) = &self.group_fill - && gm.determinant().abs() > 1e-12 - { - let path = (gm.inverse() * m) * sp.path.clone(); - paint::fill_path(ctx, rctx, gf, None, &path, *gbox, *gm, sp.fill, fade(i).fill, f.pres); - } - } - _ => paint::fill_path(ctx, rctx, fl, fill_ph, &sp.path, Rect::new(0.0, 0.0, w, h), m, sp.fill, fade(i).fill, f.pres), - } - ctx.set_fill_rule(peniko::Fill::NonZero); - } + self.fill_geometry(ctx, f, rctx, fl, fill_ph, &geo, (w, h), m, &fade); } } } diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index b65098b0..7d1f78a1 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1910,6 +1910,38 @@ fn group_child_scale_does_not_scale_line_widths() { } } +#[test] +fn a_picture_and_an_embedded_objects_preview_draw_their_fill_behind_transparent_pixels() { + // PowerPoint draws a picture's own fill behind its transparent pixels (and an old equation + // object's fallback picture on its coloured box). Here the left half of the image is clear. + let png = { + let img = image::RgbaImage::from_fn(8, 4, |x, _| if x < 4 { image::Rgba([0, 0, 0, 0]) } else { image::Rgba([20, 20, 160, 255]) }); + let mut out = std::io::Cursor::new(vec![]); + image::DynamicImage::ImageRgba8(img).write_to(&mut out, image::ImageFormat::Png).unwrap(); + out.into_inner() + }; + let orange = Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 153, 0))); + for (object, fill) in [(false, None), (false, Some(orange.clone())), (true, Some(orange.clone()))] { + let mut p = Presentation::default(); + let media = p.add_media("half.png", "image/png", png.clone()).expect("media"); + let kind = if object { + ShapeKind::Opaque { xml: String::new(), preview: Some(media), label: "Equation.3".into(), parts: vec![] } + } else { + ShapeKind::Picture { fill: deckcraft_model::style::PictureFill { media, ..Default::default() } } + }; + let sh = Shape { id: ShapeId(630), xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 200.0)), kind, fill: fill.clone(), ..Default::default() }; + std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh]; + let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }); + let (clear, opaque) = (img.pixel(200, 200), img.pixel(400, 200)); + assert!(opaque[2] > 140 && opaque[0] < 40, "the picture's own pixels ({object}): {opaque:?}"); + if fill.is_some() { + assert_eq!(clear[..3], [255, 153, 0], "the fill behind the clear half ({object})"); + } else { + assert!(clear[..3].iter().all(|v| *v > 240), "no fill: the slide shows through: {clear:?}"); + } + } +} + #[test] fn a_picture_casts_its_shadow_from_its_pixels() { // PowerPoint shades only where a picture paints: a logo on a transparent background casts the From a8f1573fbaf5dee9a52429d38b2d055bca482deb Mon Sep 17 00:00:00 2001 From: Dominik Strebel Date: Sat, 24 Oct 2026 14:29:30 +0200 Subject: [PATCH 2/4] fix(pptx): an object saved as its preview picture keeps the fallback picture's fill --- crates/pptx/src/write/shapes.rs | 6 +++++- crates/pptx/tests/it/ole.rs | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/pptx/src/write/shapes.rs b/crates/pptx/src/write/shapes.rs index 5ef269f3..87200a89 100644 --- a/crates/pptx/src/write/shapes.rs +++ b/crates/pptx/src/write/shapes.rs @@ -909,8 +909,12 @@ fn shape_with_id(w: &mut W, x: &mut Exp, o: &mut Out, cx: &mut Ctx, s: &Shape, i w.empty0("p:nvPr"); w.close("p:nvPicPr"); dml::blip_fill(w, x, o, "p:blipFill", fill); + // As for any picture, a colour or pattern fill shows behind the image (an object's preview + // keeps its fallback picture's fill). let mut s2 = s.clone(); - s2.fill = None; + if matches!(s2.fill, Some(Fill::Picture(_) | Fill::Background | Fill::Group)) { + s2.fill = None; + } sp_pr(w, x, o, &s2, true); w.close("p:pic"); } else { diff --git a/crates/pptx/tests/it/ole.rs b/crates/pptx/tests/it/ole.rs index 1b0ef779..f24d0ac6 100644 --- a/crates/pptx/tests/it/ole.rs +++ b/crates/pptx/tests/it/ole.rs @@ -131,8 +131,19 @@ fn an_ole_objects_fallback_picture_gives_it_its_fill_and_outline() { r#""#, ))); let orange = deckcraft_model::style::Fill::solid(deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::rgb(0xFF, 0x99, 0))); - assert_eq!(filled.fill, Some(orange)); + assert_eq!(filled.fill, Some(orange.clone())); assert_eq!(filled.line.as_ref().and_then(|l| l.width), Some(1.0)); + // Saved as its preview picture (its kept XML can't be written back), it keeps the fill. + let mut p = deckcraft_pptx::import(&deck(pic(r#""#))).expect("import"); + for s in &mut std::sync::Arc::make_mut(&mut p.slides[0]).shapes { + if let ShapeKind::Opaque { xml, .. } = &mut s.kind { + xml.clear(); + } + } + let back = deckcraft_pptx::import(&deckcraft_pptx::export(&p).expect("export")).expect("reimport"); + let saved = back.slides[0].shapes.iter().find(|s| s.name == "Object 3").expect("object"); + assert!(matches!(saved.kind, ShapeKind::Picture { .. }), "{:?}", saved.kind); + assert_eq!(saved.fill, Some(orange)); // Without a fill of its own the object stays unfilled. assert_eq!(object(deck(pic(""))).fill, None); } From c15e260794e8ab153fda90eed895b90198125570 Mon Sep 17 00:00:00 2001 From: Dominik Strebel Date: Sat, 24 Oct 2026 16:48:24 +0200 Subject: [PATCH 3/4] fix(render): an emphasis tint recolours the fill behind a picture's and an object preview's transparent pixels --- crates/render/src/lib.rs | 15 ++++++++++----- crates/render/src/tests.rs | 4 ++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 37102d9b..ee79b385 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1165,9 +1165,12 @@ impl Renderer { // Fill. match &s.kind { ShapeKind::Picture { fill: pf } => { - // The picture's own fill shows behind its transparent pixels, as in PowerPoint. - if let Some(fl) = s.fill.as_ref().filter(|f| !f.is_none()) { - self.fill_geometry(ctx, f, rctx, fl, None, &geo, (w, h), m, &fade); + // The picture's own fill shows behind its transparent pixels, as in PowerPoint (an + // emphasis tint recolours it, as it does any shape's fill). + if s.fill.as_ref().is_some_and(|f| !f.is_none()) + && let Some(fl) = &fill + { + self.fill_geometry(ctx, f, rctx, fl, fill_ph, &geo, (w, h), m, &fade); } ctx.set_transform(m); let bounds = Rect::new(0.0, 0.0, w, h); @@ -1181,8 +1184,10 @@ impl Renderer { ShapeKind::Opaque { preview: Some(media), .. } => { // An embedded object's fallback picture can carry a fill (old equation objects // drawn on coloured boxes): behind the preview, as for a picture. - if let Some(fl) = s.fill.as_ref().filter(|f| !f.is_none()) { - self.fill_geometry(ctx, f, rctx, fl, None, &geo, (w, h), m, &fade); + if s.fill.as_ref().is_some_and(|f| !f.is_none()) + && let Some(fl) = &fill + { + self.fill_geometry(ctx, f, rctx, fl, fill_ph, &geo, (w, h), m, &fade); } ctx.set_transform(m); let pf = deckcraft_model::style::PictureFill { media: *media, ..Default::default() }; diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 7d1f78a1..686992c3 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1936,6 +1936,10 @@ fn a_picture_and_an_embedded_objects_preview_draw_their_fill_behind_transparent_ assert!(opaque[2] > 140 && opaque[0] < 40, "the picture's own pixels ({object}): {opaque:?}"); if fill.is_some() { assert_eq!(clear[..3], [255, 153, 0], "the fill behind the clear half ({object})"); + // A fill-colour emphasis recolours that fill, as it does any shape's. + let tint = |_: ShapeId| ShapeState { tint: Some(deckcraft_color::Rgba::rgb(0, 160, 60)), ..Default::default() }; + let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, state: Some(&tint), ..Default::default() }); + assert_eq!(img.pixel(200, 200)[..3], [0, 160, 60], "the tinted fill behind the clear half ({object})"); } else { assert!(clear[..3].iter().all(|v| *v > 240), "no fill: the slide shows through: {clear:?}"); } From 8236a9441bea6ae4e4cd8aceac2427f3f89ddc47 Mon Sep 17 00:00:00 2001 From: Dominik Strebel Date: Sat, 24 Oct 2026 17:16:58 +0200 Subject: [PATCH 4/4] docs(roadmap): status line names the 2026-10-24 progress-log entry --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index a4062400..cb6c5dfc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~20 points and ~120–210 h away -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-23 · **Change:** trivial (progress log: scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-24 · **Change:** trivial (progress log: picture and embedded object fills; previous: trivial, scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (fill and outline edits reach group members, #1079; stage and scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-20 · **Change:** trivial (empty chart number-format sections hide values; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-18 · **Change:** minor (Symbol-font characters (U+F0xx) remap to the installed Symbol face's real Unicode on macOS, so they draw from Symbol instead of a look-alike; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)