mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
PPTX: a theme's spDef anchor is for new shapes, not the file's own
A shape whose text has no anchor of its own (`<a:bodyPr/>`) was given the anchor of the theme's shape defaults (`a:objectDefaults/a:spDef/a:bodyPr`), which Office's themes set to `ctr`, so its text was centred in the box. PowerPoint for Mac draws such text at the top: a synthetic deck with an spDef anchor="ctr" and three rectangles (`<a:bodyPr/>`, anchor="t", anchor="ctr"), exported as PDF, puts the first two at the top. The spDef defaults are for shapes inserted later. Opening a file now anchors every shape without an anchor at the top, as the standard has it; the model's middle default is still written as ctr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5a76518ddd
commit
30d00468e2
@@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 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-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-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape |
|
||||
@@ -245,6 +246,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-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 |
|
||||
|
||||
+12
-27
@@ -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<deckcraft_model::text::Anchor> {
|
||||
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 `<a:bodyPr/>` 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 `<a:bodyPr/>` 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:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user