diff --git a/ROADMAP.md b/ROADMAP.md index 0df09136..1c0d9958 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -136,6 +136,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-18 | Duplicate (Ctrl+D) repeats a move: duplicate a shape, move the copy, and the next Duplicate places its copy the same distance further on, as in PowerPoint (#966) | | 2026-10-22 | Slide Show: while animations play, each frame draws only the slide's shapes over the slide's backdrop (background, master and layout graphics), rendered once and ahead of time. A full-slide picture with a glow in the layout re-rendered at every frame held animations to 2–3 frames a second (~390 ms a frame at 1080p); now ~28 ms | | 2026-10-22 | Animation Pane ▸ Play From: with an effect selected, the pane's play button starts the canvas preview at that effect, with the effects before it already played. With nothing selected it is still Play All | +| 2026-10-21 | Opening a .pptx puts the text of shapes without an anchor at the top even when the theme's shape defaults (`spDef`) centre new shapes, as PowerPoint for Mac draws them; checked with a synthetic probe and on a private 166-deck corpus (51 slides closer to PowerPoint's PDF, none further) | | 2026-10-18 | Native chart sidecar bytes move from JSON arrays into referenced ZIP entries, with shared media byte/item limits and bounded reads; older inline sidecars still load. Synthetic format regressions cover large workbooks and malformed members | | 2026-10-18 | Slide thumbnails and the Slide Sorter render only the slides in view: on a deck of more than 600 slides they used to evict their own thumbnails, so the pane flashed white and never finished (#969) | | 2026-10-18 | Resizing with the mouse snaps the edge being dragged to the slide, guides and other shapes: a fixed edge that already lined up used to take the snap and leave the dragged edge free, and a fixed edge near a line shifted the dragged one (#961) | @@ -259,6 +260,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-16 | trivial | Progress log: a `.ppsx` from the system starts the slide show (#862) | | 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 | | 2026-10-14 | trivial | remote MCP checks reply IDs, envelopes and bounded newline framing (#718) | | 2026-10-14 | minor | Gray list markers on empty paragraphs while editing; caret keeps the list indent before typing | | 2026-10-14 | minor | Community PR wave folded: headline breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) and the feature-area breadths from `cargo xtask parity`; one row per area; Upcoming item 2 merged; per-PR count notes dropped from the progress log. Ready stays ~55%, stage alpha | diff --git a/crates/pptx/src/read/mod.rs b/crates/pptx/src/read/mod.rs index 979cdb70..644ffa8f 100644 --- a/crates/pptx/src/read/mod.rs +++ b/crates/pptx/src/read/mod.rs @@ -781,24 +781,14 @@ pub fn color_map(cm: &El) -> Vec<(String, String)> { cm.attrs.iter().filter(|(k, _)| !k.starts_with("xmlns")).map(|(k, v)| (k.clone(), v.clone())).collect() } -/// The anchor the theme's shape defaults (`a:objectDefaults/a:spDef/a:bodyPr`) give new shapes -/// text, if any. -fn theme_shape_anchor(t: &deckcraft_model::Theme) -> Option { - let raw = t.raw_extra.as_deref()?; - let sp = &raw[raw.find("spDef")?..]; - let sp = &sp[..sp.find("/a:spDef").or_else(|| sp.find("txDef")).unwrap_or(sp.len())]; - let b = &sp[sp.find("bodyPr")?..]; - let b = &b[..b.find('>').unwrap_or(b.len())]; - let v = &b[b.find("anchor=\"")? + 8..]; - deckcraft_model::text::Anchor::from_xml(&v[..v.find('"')?]) -} - -/// A shape's text without an anchor of its own sits at the top, as the standard has it (or where -/// the theme's shape defaults put it). The model's own default for shapes is the middle, so a -/// top or bottom anchor is made explicit here and a middle one left unset (placeholders and text -/// boxes keep inheriting). +/// A shape's text without an anchor of its own sits at the top, as the standard has it and as +/// PowerPoint draws it. The theme's shape defaults (`a:objectDefaults/a:spDef/a:bodyPr`, centred in +/// Office's themes) are for shapes inserted later, not for the file's own shapes: PowerPoint for +/// Mac draws `` at the top under an `spDef` with `anchor="ctr"`. The model's own default +/// for shapes is the middle, so a top or bottom anchor is made explicit here and a middle one left +/// unset (placeholders and text boxes keep inheriting). fn default_text_anchor(p: &mut Presentation) { - fn fix(shapes: &mut [deckcraft_model::Shape], a: deckcraft_model::text::Anchor) { + fn fix(shapes: &mut [deckcraft_model::Shape]) { walk_shapes_mut(shapes, &mut |s| { if s.ph.is_none() && !s.text_box @@ -806,25 +796,20 @@ fn default_text_anchor(p: &mut Presentation) { && let Some(t) = &mut s.text { // Middle is the model's own default for shapes, so it is kept as unset. - let got = t.body.anchor.unwrap_or(a); + let got = t.body.anchor.unwrap_or(deckcraft_model::text::Anchor::Top); t.body.anchor = (got != deckcraft_model::text::Anchor::Middle).then_some(got); } }); } - let top = deckcraft_model::text::Anchor::Top; - let anchors: Vec<_> = p.masters.iter().map(|m| (m.id, theme_shape_anchor(&m.theme).unwrap_or(top))).collect(); for m in &mut p.masters { - let a = anchors.iter().find(|(id, _)| *id == m.id).map(|x| x.1).unwrap_or(top); let m = Arc::make_mut(m); - fix(&mut m.shapes, a); + fix(&mut m.shapes); for l in &mut m.layouts { - fix(&mut l.shapes, a); + fix(&mut l.shapes); } } - let per_slide: Vec<_> = - p.slides.iter().map(|s| p.master_for(s).and_then(|(m, _)| anchors.iter().find(|(id, _)| *id == m.id)).map(|x| x.1).unwrap_or(top)).collect(); - for (s, a) in p.slides.iter_mut().zip(per_slide) { - fix(&mut Arc::make_mut(s).shapes, a); + for s in &mut p.slides { + fix(&mut Arc::make_mut(s).shapes); } } diff --git a/crates/pptx/tests/it/anchor.rs b/crates/pptx/tests/it/anchor.rs index 72af2436..18280e4a 100644 --- a/crates/pptx/tests/it/anchor.rs +++ b/crates/pptx/tests/it/anchor.rs @@ -1,5 +1,5 @@ //! Text anchoring of shapes without an `anchor` of their own: the model centres a shape's text, -//! a file without one puts it at the top (or where the theme's shape defaults say). +//! a file without one puts it at the top, whatever the theme's shape defaults say. use std::io::{Cursor, Read, Write}; @@ -57,7 +57,8 @@ fn shapes_keep_their_middle_anchor_and_files_without_one_anchor_at_the_top() { let a = anchors(&deckcraft_pptx::import(&bare).expect("import")); assert!(a.iter().all(|x| *x == Some(Anchor::Top)), "{a:?}"); - // ...unless the theme's shape defaults centre them, as Office's themes do. + // Also when the theme's shape defaults centre new shapes, as Office's themes do: PowerPoint + // for Mac draws `` at the top under such an `spDef` (synthetic probe exported as PDF). let themed = rewrite(&bare, |n, x| { if n.starts_with("ppt/theme/") { x.replace( @@ -69,6 +70,5 @@ fn shapes_keep_their_middle_anchor_and_files_without_one_anchor_at_the_top() { } }); let a = anchors(&deckcraft_pptx::import(&themed).expect("import")); - assert!(a.iter().all(|x| x.is_none() || *x == Some(Anchor::Middle)), "{a:?}"); - assert!(a.iter().filter(|x| x.is_none()).count() >= 3, "{a:?}"); + assert!(a.iter().all(|x| *x == Some(Anchor::Top)), "{a:?}"); }