From 1705712a41fb28b8ad1dd254a015684d7b21cb42 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Sun, 25 Oct 2026 16:42:22 -0300 Subject: [PATCH] PPTX: a picture in a picture placeholder takes its layout's shape A layout's round picture placeholder keeps a slide's photo in a circle in PowerPoint. The reader marked only p:sp placeholders as inheriting their geometry, so a p:pic placeholder with just a frame in its spPr came out as a rectangle (with the layout's border). Mark p:pic placeholders too; the renderer already clips pictures to their geometry. Co-Authored-By: Claude Sonnet 5.5 --- ROADMAP.md | 8 +++-- crates/pptx/src/read/shapes.rs | 2 ++ crates/pptx/tests/it/placeholders.rs | 48 +++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b52abe28..56fc162d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,9 +2,9 @@ **Stage: alpha** · next: beta, ~20 points and ~120–210 h away -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-24 · **Change:** trivial (progress log: Edit Points handle drags with Shift, Ctrl/Cmd or Alt/Option set the point type; previous: 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: Ctrl+wheel zoom during the slide show, #1286; 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-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-25 · **Change:** trivial (progress log: a picture in a picture placeholder is cut into the layout's shape; previous: trivial, progress log: Edit Points handle drags with Shift, Ctrl/Cmd or Alt/Option set the point type; previous: 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-25 · **Change:** trivial (progress log: a picture in a picture placeholder is cut into the layout's shape; previous: trivial, progress log: Ctrl+wheel zoom during the slide show, #1286; 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-25 · **Change:** trivial (progress log: a picture in a picture placeholder is cut into the layout's shape; previous: 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) @@ -137,6 +137,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-25 | A photo in a picture placeholder is cut into the shape its layout gives the placeholder, as PowerPoint draws it: a layout's round picture placeholder keeps a slide's photo in a circle with the layout's white border, where it came out as a rectangle (the reader only passed `p:sp` placeholders to `inherit_placeholder_geometry`, not `p:pic`) | | 2026-10-24 | AutoFormat as you type: a paragraph that starts with `*`, `-`, `--`, `>`, `<>`, `1.`, `1)`, `I.`, `i.`, `A.`, `a.` or `a)` and a space or tab becomes a bulleted or numbered list in that style; Backspace right after puts the typed text back, and Tools ▸ AutoCorrect and `text.autocorrect {lists}` turn it off | | 2026-10-24 | Tables: while the caret is in a cell, dragging into another cell selects that range of cells (shaded, merged cells taken in whole), and Shift+arrow grows or shrinks it; formatting applies to the selected cells only, and Delete clears their text and keeps the table | | 2026-10-24 | Preferences ▸ Measurement units: inches or centimetres. The Format pane's Size and Position fields, Shape Format ▸ Size on the ribbon, the Slide Size dialog and the ruler show and take the chosen unit; the deck still stores points (#1326) | @@ -293,6 +294,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 2026-10-25 | trivial | Progress log: a picture in a picture placeholder takes the layout's shape. Scores and milestone names unchanged | | 2026-10-24 | trivial | Progress log: selecting a range of table cells by dragging. Scores and milestone names unchanged | | 2026-10-24 | trivial | Progress log: measurement units in centimetres (#1326). Scores unchanged | | 2026-10-22 | trivial | Progress log: Animation Pane ▸ Play From. Scores unchanged | diff --git a/crates/pptx/src/read/shapes.rs b/crates/pptx/src/read/shapes.rs index 3bca8220..6d8acc4a 100644 --- a/crates/pptx/src/read/shapes.rs +++ b/crates/pptx/src/read/shapes.rs @@ -338,6 +338,8 @@ fn pic(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El) -> Shape { if let Some(pr) = e.child("spPr") { sp_pr(imp, part, &mut s, pr); } + // A picture in a picture placeholder is cut into the layout's shape (a circle, say). + mark_inherited_geometry(&mut s, e.child("spPr")); s.style = e.child("style").map(style); let pf = e.child("blipFill").and_then(|b| dml::blip_fill(imp, part, b)); let nvpr = e.path(&["nvPicPr", "nvPr"]); diff --git a/crates/pptx/tests/it/placeholders.rs b/crates/pptx/tests/it/placeholders.rs index b2b46100..be4e9969 100644 --- a/crates/pptx/tests/it/placeholders.rs +++ b/crates/pptx/tests/it/placeholders.rs @@ -2,7 +2,9 @@ use std::io::{Cursor, Read, Write}; -use deckcraft_model::{Geom, defaults}; +use deckcraft_geom::Xfrm; +use deckcraft_model::style::PictureFill; +use deckcraft_model::{Geom, PhType, Placeholder, Shape, ShapeId, ShapeKind, defaults}; /// `bytes` with `edit` applied to every slide part. fn edit_slides(bytes: &[u8], edit: impl Fn(String) -> String) -> Vec { @@ -62,3 +64,47 @@ fn a_placeholder_without_geometry_takes_its_layouts() { let q = deckcraft_pptx::import(&deckcraft_pptx::export(&p).expect("export")).expect("import"); assert!(q.slides[0].shapes.iter().filter(|s| s.ph.is_some()).all(|s| matches!(&s.geom, Geom::Preset { name, .. } if name == "triangle"))); } + +#[test] +fn a_picture_placeholder_without_geometry_takes_its_layouts() { + // The slide's picture sits in a layout's picture placeholder drawn as an ellipse; PowerPoint + // writes its spPr with a frame only and shows the picture cut into the circle. + let mut p = defaults::new_presentation(None); + let img = { + let mut out = Cursor::new(vec![]); + let px = image::RgbImage::from_pixel(4, 4, image::Rgb([10, 200, 30])); + image::DynamicImage::ImageRgb8(px).write_to(&mut out, image::ImageFormat::Png).expect("png"); + p.add_media("image1.png", "image/png", out.into_inner()).expect("media") + }; + let ph = Placeholder { kind: PhType::Picture, idx: 15, ..Default::default() }; + let layout = p.slides[0].layout; + let slot = Shape { + id: ShapeId(p.alloc_id().expect("ids")), + name: "Picture Placeholder".into(), + xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 200.0)), + geom: Geom::Preset { name: "ellipse".into(), adj: vec![] }, + ph: Some(ph.clone()), + ..Default::default() + }; + std::sync::Arc::make_mut(&mut p.masters[0]).layouts.iter_mut().find(|l| l.id == layout).expect("layout").shapes.push(slot); + let pic = Shape { + id: ShapeId(p.alloc_id().expect("ids")), + name: "Photo".into(), + xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 200.0)), + kind: ShapeKind::Picture { fill: PictureFill { media: img, ..Default::default() } }, + ph: Some(ph), + ..Default::default() + }; + std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![pic]; + let bytes = deckcraft_pptx::export(&p).expect("export"); + let bare = edit_slides(&bytes, |mut s| { + while let Some(i) = s.find("").map(|j| j + i + "".len()) else { break }; + s.replace_range(i..j, ""); + } + s + }); + let q = deckcraft_pptx::import(&bare).expect("import"); + let photo = q.slides[0].shapes.iter().find(|s| matches!(s.kind, ShapeKind::Picture { .. })).expect("picture"); + assert!(matches!(&photo.geom, Geom::Preset { name, .. } if name == "ellipse"), "{:?}", photo.geom); +}