mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge PR #562 from NikitkaRa: Pptx: round-trip text effects, layout, fills, bevels and connectors
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (WordArt Transform gallery; previous: hours to ~95% per audience added; ready 56% → 55%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: owned PPTX corpus round-trip, notes-master header and footer; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (owned PPTX corpus for comments, links, custom shows, groups and inherited placeholders; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (owned PPTX corpus for text effects, body layout, theme effects, fills, bevels and connectors; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON
|
||||
format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere
|
||||
@@ -124,6 +125,9 @@ Ranked; detail and the full beta plan in [docs/roadmap.md](docs/roadmap.md).
|
||||
2. **PPTX on real decks:** owned corpus now covers comments, links and actions, custom shows,
|
||||
rotated groups and placeholders that inherit from the layout. Still open: a larger corpus,
|
||||
embedded fonts, #70, #74, #77, EMF/WMF and SVG. 36–64 h.
|
||||
2. **PPTX on real decks:** owned corpus now covers text effects, autofit, columns, vertical and
|
||||
RTL text, tabs, line spacing, theme effects, fills, bevels and connectors. Still open: a larger
|
||||
corpus, embedded fonts, #70, #74, #77, EMF/WMF and SVG. 36–64 h.
|
||||
3. **Localization:** catalog, system language (#15), zh-Hans, es, fr, de, ja with IME. 27–46 h.
|
||||
4. **Output:** vector PDF, printing. 10–16 h.
|
||||
5. **Depth that users hit:** font weights (#69), chart data and elements, Animation Pane timeline,
|
||||
@@ -150,6 +154,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-13 | WordArt Transform: Shape Format ▸ Text Effects shows the 40 text warps and No Transform; `format.textWarp` applies one to the selected text. Adjust handles are still open |
|
||||
| 2026-10-13 | PPTX corpus round-trip for decks we author (open → save → reopen, package parts and text). A notes master's header and footer are kept instead of being replaced by an empty notes master |
|
||||
| 2026-10-13 | PPTX round trip of comments, hyperlinks and actions, custom shows, rotated groups, and placeholders that inherit from the layout. Paragraph default formatting (`defRPr`, including a link) and action sounds (the clip, stop-previous, and a custom show's "Show and return") are kept. Embedded fonts and comment threads stay open |
|
||||
| 2026-10-13 | PPTX round trip of text effects, autofit, columns, vertical and RTL text, tab stops, line spacing, theme effects, gradient and pattern fills, 3-D bevels and connectors. A run keeps its inner shadow, reflection and soft edge, and a bevel preset with no kept 3-D XML is written back. Embedded fonts and #70, #74, #77 stay open |
|
||||
| 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless |
|
||||
| 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline |
|
||||
| 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting |
|
||||
@@ -185,6 +190,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-13 | trivial | Progress log: codec parsers reject overflowing numeric fields (#426) |
|
||||
| 2026-10-13 | trivial | Progress log: owned PPTX corpus round-trip; notes-master header and footer |
|
||||
| 2026-10-13 | trivial | Progress log: PPTX corpus for comments, links, custom shows, groups and inherited placeholders |
|
||||
| 2026-10-13 | trivial | Progress log: PPTX corpus for text effects, body layout, theme effects, fills, bevels and connectors |
|
||||
| 2026-10-13 | trivial | Progress log: wild text cases |
|
||||
| 2026-10-13 | trivial | Progress log: slide show for interactive decks |
|
||||
| 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) |
|
||||
|
||||
@@ -372,7 +372,12 @@ pub struct RunProps {
|
||||
pub link: Option<Hyperlink>,
|
||||
pub lang: Option<String>,
|
||||
pub shadow: Option<crate::style::Shadow>,
|
||||
/// `a:innerShdw` on the run. Outer shadow stays in `shadow`.
|
||||
pub inner_shadow: Option<crate::style::Shadow>,
|
||||
pub glow: Option<crate::style::Glow>,
|
||||
pub reflection: Option<crate::style::Reflection>,
|
||||
/// Soft-edge radius in points (`a:softEdge`).
|
||||
pub soft_edge: Option<f64>,
|
||||
/// Spelling/grammar errors are not flagged.
|
||||
pub no_proof: Option<bool>,
|
||||
}
|
||||
@@ -403,7 +408,10 @@ impl RunProps {
|
||||
link,
|
||||
lang,
|
||||
shadow,
|
||||
inner_shadow,
|
||||
glow,
|
||||
reflection,
|
||||
soft_edge,
|
||||
no_proof
|
||||
);
|
||||
}
|
||||
|
||||
@@ -510,7 +510,10 @@ pub fn rpr(imp: &mut Imp, part: &Part, r: &El) -> RunProps {
|
||||
"effectLst" => {
|
||||
let fx = effects(e);
|
||||
p.shadow = fx.outer_shadow;
|
||||
p.inner_shadow = fx.inner_shadow;
|
||||
p.glow = fx.glow;
|
||||
p.reflection = fx.reflection;
|
||||
p.soft_edge = fx.soft_edge;
|
||||
}
|
||||
"highlight" => p.highlight = color(e),
|
||||
"uFill" => p.underline_color = e.child("solidFill").and_then(color),
|
||||
|
||||
@@ -60,6 +60,9 @@ pub fn read_theme(imp: &mut Imp, part: &Part, root: &El) -> Theme {
|
||||
.take(16)
|
||||
.map(|e| {
|
||||
let mut fx = e.child("effectLst").map(dml::effects).unwrap_or_default();
|
||||
if let Some(b) = e.path(&["sp3d", "bevelT"]) {
|
||||
fx.bevel = Some(b.attr("prst").unwrap_or("circle").to_string());
|
||||
}
|
||||
let raw: String = e.elements().filter(|c| c.is("scene3d") || c.is("sp3d")).map(|c| part.keep(c)).collect();
|
||||
if !raw.is_empty() {
|
||||
fx.raw3d = Some(raw);
|
||||
|
||||
@@ -342,6 +342,15 @@ pub fn effects(w: &mut W, fx: &Effects) {
|
||||
w.close("a:effectLst");
|
||||
}
|
||||
|
||||
/// `a:sp3d` for a bevel preset when the shape or theme style has no kept 3-D XML.
|
||||
/// The preset name is limited to ASCII letters and digits so it cannot break the markup.
|
||||
pub fn bevel_sp3d(preset: &str) -> Option<String> {
|
||||
if preset.is_empty() || preset.len() > 32 || !preset.chars().all(|c| c.is_ascii_alphanumeric()) {
|
||||
return None;
|
||||
}
|
||||
Some(format!("<a:sp3d><a:bevelT prst=\"{preset}\"/></a:sp3d>"))
|
||||
}
|
||||
|
||||
pub fn xfrm(w: &mut W, tag: &str, x: &Xfrm, child: Option<&Xfrm>) {
|
||||
let x = if x.is_finite() { *x } else { Xfrm::default() };
|
||||
let rot = ang(x.rot);
|
||||
@@ -602,10 +611,10 @@ pub fn rpr(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, r: &RunProps) {
|
||||
.o("spc", r.spacing.map(|s| (s.clamp(-4000.0, 4000.0) * 100.0).round() as i64))
|
||||
.o("baseline", r.baseline.map(|b| pct(b.clamp(-10.0, 10.0))))
|
||||
.b("noProof", r.no_proof);
|
||||
let has_fx = r.shadow.is_some() || r.inner_shadow.is_some() || r.glow.is_some() || r.reflection.is_some() || r.soft_edge.is_some();
|
||||
let has_kids = r.outline.is_some()
|
||||
|| r.fill.is_some()
|
||||
|| r.shadow.is_some()
|
||||
|| r.glow.is_some()
|
||||
|| has_fx
|
||||
|| r.highlight.is_some()
|
||||
|| r.underline_color.is_some()
|
||||
|| r.font.is_some()
|
||||
@@ -627,8 +636,19 @@ pub fn rpr(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, r: &RunProps) {
|
||||
f => fill(w, x, o, f),
|
||||
}
|
||||
}
|
||||
if r.shadow.is_some() || r.glow.is_some() {
|
||||
effects(w, &Effects { outer_shadow: r.shadow.clone(), glow: r.glow.clone(), ..Default::default() });
|
||||
if has_fx {
|
||||
// Same child order as a shape's effect list: glow, inner shadow, outer shadow, reflection, soft edge.
|
||||
effects(
|
||||
w,
|
||||
&Effects {
|
||||
outer_shadow: r.shadow.clone(),
|
||||
inner_shadow: r.inner_shadow.clone(),
|
||||
glow: r.glow.clone(),
|
||||
reflection: r.reflection.clone(),
|
||||
soft_edge: r.soft_edge,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some(h) = &r.highlight {
|
||||
w.open0("a:highlight");
|
||||
|
||||
@@ -245,6 +245,8 @@ fn sp_pr(w: &mut W, x: &mut Exp, o: &mut Out, s: &Shape, geom: bool) {
|
||||
dml::effects(w, fx);
|
||||
if let Some(raw) = &fx.raw3d {
|
||||
w.raw(raw);
|
||||
} else if let Some(xml) = fx.bevel.as_deref().and_then(dml::bevel_sp3d) {
|
||||
w.raw(&xml);
|
||||
}
|
||||
}
|
||||
w.close("p:spPr");
|
||||
|
||||
@@ -83,6 +83,8 @@ pub fn theme_xml(x: &mut Exp, o: &mut Out, t: &Theme) -> Vec<u8> {
|
||||
dml::effects(&mut w, e);
|
||||
if let Some(raw) = &e.raw3d {
|
||||
w.raw(raw);
|
||||
} else if let Some(xml) = e.bevel.as_deref().and_then(dml::bevel_sp3d) {
|
||||
w.raw(&xml);
|
||||
}
|
||||
w.close("a:effectStyle");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
//! Owned corpus: text effects, body layout, theme effects, fills, bevels and connectors.
|
||||
//! Open → save → reopen, twice, and compare the model.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
|
||||
use deckcraft_geom::Xfrm;
|
||||
use deckcraft_model::style::{ColorRef, Dash, Effects, Fill, Glow, Gradient, GradientShape, GradientStop, Line, PatternFill, Reflection, Shadow};
|
||||
use deckcraft_model::text::{Align, AutoFit, BodyProps, ParaProps, Paragraph, Run, RunProps, Spacing, TabStop, TextBody, TextDir};
|
||||
use deckcraft_model::{Geom, Presentation, Shape, ShapeId, ShapeKind, defaults};
|
||||
|
||||
fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> Shape {
|
||||
Shape { id: ShapeId(p.alloc_id()), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), ..Default::default() }
|
||||
}
|
||||
|
||||
fn shadow(inner: bool, dir: f64) -> Shadow {
|
||||
Shadow {
|
||||
color: ColorRef::rgb(if inner { Rgba::rgb(0, 0, 80) } else { Rgba::BLACK }).with(ColorTransform::Alpha(50_000)),
|
||||
blur: if inner { 4.0 } else { 8.0 },
|
||||
dist: if inner { 2.0 } else { 5.0 },
|
||||
dir,
|
||||
inner,
|
||||
sx: if inner { 1.0 } else { 0.5 },
|
||||
sy: 1.0,
|
||||
kx: 0.0,
|
||||
ky: if inner { 0.0 } else { 15.0 },
|
||||
align: if inner { String::new() } else { "ctr".into() },
|
||||
rotate_with_shape: inner,
|
||||
}
|
||||
}
|
||||
|
||||
fn glow() -> Glow {
|
||||
Glow { color: ColorRef::scheme(SchemeSlot::Accent3), radius: 10.0 }
|
||||
}
|
||||
|
||||
fn reflection() -> Reflection {
|
||||
Reflection { blur: 2.0, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.75, dist: 6.0 }
|
||||
}
|
||||
|
||||
fn gradient(angle: f64) -> Fill {
|
||||
Fill::Gradient(Gradient {
|
||||
stops: vec![
|
||||
GradientStop { pos: 0.0, color: ColorRef::rgb(Rgba::rgb(255, 80, 0)) },
|
||||
GradientStop { pos: 1.0, color: ColorRef::scheme(SchemeSlot::Accent1) },
|
||||
],
|
||||
shape: GradientShape::Linear { angle, scaled: true },
|
||||
rotate_with_shape: true,
|
||||
tile: [0.0; 4],
|
||||
})
|
||||
}
|
||||
|
||||
fn styled_run() -> RunProps {
|
||||
RunProps {
|
||||
fill: Some(gradient(20.0)),
|
||||
outline: Some(Line { dash: Some(Dash::Preset { name: "dash".into() }), ..Line::solid(ColorRef::rgb(Rgba::rgb(0, 40, 80)), 1.5) }),
|
||||
shadow: Some(shadow(false, 135.0)),
|
||||
inner_shadow: Some(shadow(true, 45.0)),
|
||||
glow: Some(glow()),
|
||||
reflection: Some(reflection()),
|
||||
soft_edge: Some(4.0),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn style_deck() -> Presentation {
|
||||
let mut p = defaults::new_presentation(None);
|
||||
let master = Arc::make_mut(p.masters.first_mut().expect("master"));
|
||||
master.theme.format.effects[0] = Effects {
|
||||
glow: Some(glow()),
|
||||
reflection: Some(reflection()),
|
||||
// Preset only: no kept `scene3d`/`sp3d`. A save must still write the bevel.
|
||||
bevel: Some("circle".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let layout = defaults::layout_of_kind(&p, deckcraft_model::LayoutType::Blank).unwrap_or_default();
|
||||
let mut s = defaults::new_slide(&mut p, layout);
|
||||
|
||||
let mut word = shape(&mut p, "Styled text", 40.0, 40.0, 400.0, 180.0);
|
||||
word.text = Some(TextBody {
|
||||
body: BodyProps {
|
||||
autofit: Some(AutoFit::Shrink { font_scale: 0.8, line_reduction: 0.2 }),
|
||||
columns: Some(2),
|
||||
col_spacing: Some(12.0),
|
||||
vert: Some(TextDir::Vertical270),
|
||||
rtl_col: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
paragraphs: vec![Paragraph {
|
||||
props: ParaProps {
|
||||
align: Some(Align::Right),
|
||||
rtl: Some(true),
|
||||
line_spacing: Some(Spacing::Pct(1.5)),
|
||||
space_before: Some(Spacing::Pts(6.0)),
|
||||
tabs: Some(vec![
|
||||
TabStop { pos: 36.0, align: "l".into() },
|
||||
TabStop { pos: 72.0, align: "ctr".into() },
|
||||
TabStop { pos: 108.0, align: "r".into() },
|
||||
TabStop { pos: 144.0, align: "dec".into() },
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
runs: vec![Run::with("Gradient glow", styled_run())],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut path = shape(&mut p, "Path gradient", 40.0, 260.0, 140.0, 100.0);
|
||||
path.fill = Some(Fill::Gradient(Gradient {
|
||||
stops: vec![
|
||||
GradientStop { pos: 0.0, color: ColorRef::rgb(Rgba::rgb(10, 20, 30)) },
|
||||
GradientStop { pos: 0.5, color: ColorRef::scheme(SchemeSlot::Accent5) },
|
||||
GradientStop { pos: 1.0, color: ColorRef::rgb(Rgba::rgb(240, 240, 240)) },
|
||||
],
|
||||
shape: GradientShape::Path { path: "circle".into(), focus: [0.2, 0.2, 0.2, 0.2] },
|
||||
rotate_with_shape: false,
|
||||
tile: [0.0; 4],
|
||||
}));
|
||||
|
||||
let mut patt = shape(&mut p, "Pattern", 200.0, 260.0, 120.0, 80.0);
|
||||
patt.fill =
|
||||
Some(Fill::Pattern(PatternFill { preset: "smCheck".into(), fg: ColorRef::scheme(SchemeSlot::Accent4), bg: ColorRef::rgb(Rgba::WHITE) }));
|
||||
|
||||
let mut named = shape(&mut p, "Bevel preset", 460.0, 260.0, 80.0, 60.0);
|
||||
named.effects = Some(Effects { bevel: Some("relaxedInset".into()), ..Default::default() });
|
||||
|
||||
let mut bevel = shape(&mut p, "Bevel", 340.0, 260.0, 100.0, 80.0);
|
||||
bevel.effects = Some(Effects {
|
||||
raw3d: Some(
|
||||
"<a:scene3d><a:camera prst=\"orthographicFront\"><a:rot lat=\"0\" lon=\"0\" rev=\"0\"/></a:camera><a:lightRig rig=\"threePt\" dir=\"t\"/></a:scene3d><a:sp3d><a:bevelT w=\"63500\" h=\"25400\" prst=\"angle\"/></a:sp3d>"
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let left = shape(&mut p, "Left", 40.0, 400.0, 80.0, 40.0);
|
||||
let right = shape(&mut p, "Right", 240.0, 400.0, 80.0, 40.0);
|
||||
let (left_id, right_id) = (left.id, right.id);
|
||||
let mut cx = shape(&mut p, "Connector", 120.0, 420.0, 120.0, 0.0);
|
||||
cx.kind = ShapeKind::Connector { start: Some((left_id, 3)), end: Some((right_id, 1)) };
|
||||
cx.geom = Geom::preset("straightConnector1");
|
||||
cx.line = Some(Line::solid(ColorRef::scheme(SchemeSlot::Tx1), 1.5));
|
||||
|
||||
s.shapes.extend([word, path, patt, named, bevel, left, right, cx]);
|
||||
p.slides.push(Arc::new(s));
|
||||
p
|
||||
}
|
||||
|
||||
fn round(p: &Presentation) -> Presentation {
|
||||
let bytes = deckcraft_pptx::export(p).expect("export");
|
||||
deckcraft_pptx::import(&bytes).expect("import")
|
||||
}
|
||||
|
||||
fn by_name<'a>(p: &'a Presentation, name: &str) -> &'a Shape {
|
||||
p.slides.iter().flat_map(|s| &s.shapes).find(|s| s.name == name).unwrap_or_else(|| panic!("missing {name}"))
|
||||
}
|
||||
|
||||
fn run_of(p: &Presentation) -> &RunProps {
|
||||
&by_name(p, "Styled text").text.as_ref().expect("text").paragraphs[0].runs[0].props
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_kinds_round_trip() {
|
||||
let p = style_deck();
|
||||
let once = round(&p);
|
||||
let twice = round(&once);
|
||||
|
||||
let body = &by_name(&once, "Styled text").text.as_ref().expect("text").body;
|
||||
assert_eq!(body.autofit, Some(AutoFit::Shrink { font_scale: 0.8, line_reduction: 0.2 }));
|
||||
assert_eq!(body.columns, Some(2));
|
||||
assert_eq!(body.col_spacing, Some(12.0));
|
||||
assert_eq!(body.vert, Some(TextDir::Vertical270));
|
||||
assert_eq!(body.rtl_col, Some(true));
|
||||
let para = &by_name(&once, "Styled text").text.as_ref().expect("text").paragraphs[0].props;
|
||||
assert_eq!(para.rtl, Some(true));
|
||||
assert_eq!(para.align, Some(Align::Right));
|
||||
assert_eq!(para.line_spacing, Some(Spacing::Pct(1.5)));
|
||||
assert_eq!(para.space_before, Some(Spacing::Pts(6.0)));
|
||||
assert_eq!(
|
||||
para.tabs.as_deref(),
|
||||
Some(
|
||||
[
|
||||
TabStop { pos: 36.0, align: "l".into() },
|
||||
TabStop { pos: 72.0, align: "ctr".into() },
|
||||
TabStop { pos: 108.0, align: "r".into() },
|
||||
TabStop { pos: 144.0, align: "dec".into() },
|
||||
]
|
||||
.as_slice()
|
||||
)
|
||||
);
|
||||
|
||||
let got = run_of(&once);
|
||||
let expect = run_of(&p);
|
||||
assert_eq!(got.fill, expect.fill, "text gradient");
|
||||
assert_eq!(got.outline, expect.outline, "text outline");
|
||||
assert_eq!(got.shadow, expect.shadow, "text shadow");
|
||||
assert_eq!(got.glow, expect.glow, "text glow");
|
||||
assert_eq!(got.inner_shadow, expect.inner_shadow, "text inner shadow");
|
||||
assert_eq!(got.reflection, expect.reflection, "text reflection");
|
||||
assert_eq!(got.soft_edge, expect.soft_edge, "text soft edge");
|
||||
assert_eq!(run_of(&twice), run_of(&once));
|
||||
assert_eq!(
|
||||
by_name(&twice, "Styled text").text.as_ref().map(|t| (&t.body, &t.paragraphs[0].props)),
|
||||
by_name(&once, "Styled text").text.as_ref().map(|t| (&t.body, &t.paragraphs[0].props))
|
||||
);
|
||||
|
||||
assert_eq!(by_name(&once, "Path gradient").fill, by_name(&p, "Path gradient").fill);
|
||||
assert_eq!(by_name(&once, "Pattern").fill, by_name(&p, "Pattern").fill);
|
||||
assert_eq!(by_name(&twice, "Path gradient").fill, by_name(&once, "Path gradient").fill);
|
||||
assert_eq!(by_name(&twice, "Pattern").fill, by_name(&once, "Pattern").fill);
|
||||
|
||||
let theme = &once.masters[0].theme.format.effects[0];
|
||||
assert_eq!(theme.glow, Some(glow()));
|
||||
assert_eq!(theme.reflection, Some(reflection()));
|
||||
assert_eq!(theme.bevel.as_deref(), Some("circle"), "theme bevel preset");
|
||||
let theme2 = &twice.masters[0].theme.format.effects[0];
|
||||
assert_eq!(theme2.glow, theme.glow);
|
||||
assert_eq!(theme2.reflection, theme.reflection);
|
||||
assert_eq!(theme2.bevel, theme.bevel);
|
||||
assert_eq!(theme2.raw3d, theme.raw3d);
|
||||
|
||||
let named = by_name(&once, "Bevel preset").effects.as_ref().expect("effects");
|
||||
assert_eq!(named.bevel.as_deref(), Some("relaxedInset"));
|
||||
assert!(named.raw3d.as_deref().unwrap_or("").contains("relaxedInset"));
|
||||
assert_eq!(by_name(&twice, "Bevel preset").effects.as_ref().and_then(|e| e.bevel.as_deref()), Some("relaxedInset"));
|
||||
|
||||
let bevel = by_name(&once, "Bevel").effects.as_ref().expect("effects");
|
||||
assert_eq!(bevel.bevel.as_deref(), Some("angle"));
|
||||
let raw = bevel.raw3d.as_deref().unwrap_or("");
|
||||
assert!(raw.contains("bevelT") && raw.contains("angle") && raw.contains("scene3d"), "{raw}");
|
||||
assert_eq!(by_name(&twice, "Bevel").effects.as_ref().and_then(|e| e.raw3d.as_deref()), Some(raw));
|
||||
|
||||
let c = by_name(&once, "Connector");
|
||||
assert!(
|
||||
matches!(c.kind, ShapeKind::Connector { start: Some((s, 3)), end: Some((e, 1)) } if s == left_of(&p) && e == right_of(&p)),
|
||||
"{:?}",
|
||||
c.kind
|
||||
);
|
||||
assert_eq!(c.line, by_name(&p, "Connector").line);
|
||||
assert_eq!(c.geom, by_name(&p, "Connector").geom);
|
||||
assert_eq!(by_name(&twice, "Connector").kind, c.kind);
|
||||
}
|
||||
|
||||
fn left_of(p: &Presentation) -> ShapeId {
|
||||
by_name(p, "Left").id
|
||||
}
|
||||
fn right_of(p: &Presentation) -> ShapeId {
|
||||
by_name(p, "Right").id
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (embedded fonts read, written and drawn; MicroType Express subsets kept but not drawn; previously trivial: groups with a small legacy child coordinate space, #233) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (owned corpus round-trip; notes-master header and footer text kept; previous: trivial, groups with a small legacy child coordinate space) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (owned corpus for comments, links and actions, custom shows, rotated groups and inherited placeholders; paragraph `defRPr` and action sounds kept; previous: trivial, groups with a small legacy child coordinate space) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (owned corpus for text effects, body layout, theme effects, fills, bevels and connectors; a run's inner shadow, reflection and soft edge, and a bevel preset with no kept 3-D XML, survive a save; previous: trivial, groups with a small legacy child coordinate space) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
Every format PowerPoint reads or writes, DeckCraft's support, fidelity and how it is tested.
|
||||
Gaps are itemized in [gaps.md](gaps.md#file-formats). Dimension score: **~58% ready**
|
||||
@@ -23,6 +24,7 @@ PowerPoint opens and saves; Viewer = opens only).
|
||||
|---|---|---|---|---|---|
|
||||
| `.pptx` presentation | Editor | yes | yes | Broad (see below); corpus is decks we author, not measured against PowerPoint renders | `crates/pptx/tests/corpus.rs`, `roundtrip.rs`, `sample.rs`, `malformed.rs`, `sniff.rs`; opened in PowerPoint by hand 2026-10-06 |
|
||||
| `.pptx` presentation | Editor | yes | yes | Broad (see below); an owned corpus covers comments, links and actions, custom shows, rotated groups and placeholders that inherit from the layout. Embedded fonts and a corpus of decks from outside this repo are still open | `crates/pptx/tests/roundtrip.rs`, `corpus_elements.rs`, `sample.rs`, `malformed.rs`, `sniff.rs`; opened in PowerPoint by hand 2026-10-06 |
|
||||
| `.pptx` presentation | Editor | yes | yes | Broad (see below); an owned corpus covers text effects, autofit, columns, vertical and RTL text, tabs, line spacing, theme effects, gradient and pattern fills, bevels and connectors. A corpus of decks from outside this repo is still open | `crates/pptx/tests/roundtrip.rs`, `corpus_style.rs`, `sample.rs`, `malformed.rs`, `sniff.rs`; opened in PowerPoint by hand 2026-10-06 |
|
||||
| `.potx` template | Editor | yes (as a presentation) | no: written with the `.pptx` content type | `file.saveTemplate` is Save As under another name | none |
|
||||
| `.ppsx` slide show | Editor | yes (as a presentation) | no: written as `.pptx` content | Doesn't start in show mode | none |
|
||||
| `.pptm` / `.potm` / `.ppsm` macro-enabled | Editor | no | no | Should open and keep `vbaProject.bin` without running it | none |
|
||||
@@ -75,6 +77,9 @@ the round trip even though DeckCraft doesn't model it.
|
||||
| Effects: shadow, glow, soft edges, reflection | yes | yes | |
|
||||
| 3-D: `scene3d`, `sp3d` | kept | kept | Not rendered |
|
||||
| Text: runs, paragraphs, bullets, numbering, levels, autofit, columns, vertical, RTL, script fonts, fields, hyperlinks | yes | yes | Round-trip tests incl. `justLow`, date fields and paragraph `defRPr` (the defaults a placeholder's runs inherit, including a link). A click or hover action keeps its sound, `endSnd`, and a custom show's "Show and return" |
|
||||
| Effects: shadow, glow, soft edges, reflection | yes | yes | On a run, inner shadow, reflection and soft edges are kept along with the outer shadow and glow |
|
||||
| 3-D: `scene3d`, `sp3d` | kept | kept | A bevel preset with no `sp3d` of its own is written back on shapes and theme effect styles. Bevels, cameras and face lighting draw approximately; extrusion does not |
|
||||
| Text: runs, paragraphs, bullets, numbering, levels, autofit, columns, vertical, RTL, script fonts, fields, hyperlinks | yes | yes | Round-trip tests incl. `justLow`, date fields, gradient and outline on a run, autofit, columns, vertical and RTL text, tab stops and line spacing (`corpus_style.rs`) |
|
||||
| Text warp (`prstTxWarp`) | yes | yes | All 40 presets rendered (`crates/geom/src/warp.rs`); adjust values (`avLst`) round-trip |
|
||||
| Pictures with crop, recolour, duotone, alpha, luminance | yes | yes | |
|
||||
| Tables with styles | yes | yes | |
|
||||
@@ -111,6 +116,7 @@ the round trip even though DeckCraft doesn't model it.
|
||||
| 2026-10-13 | minor | Embedded fonts read and written; TrueType, OpenType and Embedded OpenType faces drawn |
|
||||
| 2026-10-13 | minor | Owned corpus round-trip (parts and text). Notes-master header, footer, date and slide-number text is written back |
|
||||
| 2026-10-13 | minor | Owned corpus for comments, links and actions, custom shows, rotated groups and inherited placeholders. Paragraph `defRPr` and action sounds (including custom-show return) survive a save |
|
||||
| 2026-10-13 | minor | Owned corpus for text effects, body layout, theme effects, fills, bevels and connectors. A run's inner shadow, reflection and soft edge, and a bevel preset with no kept 3-D XML, survive a save |
|
||||
| 2026-10-12 | minor | EMF/WMF text and pattern brushes drawn; OLE (VML) and ActiveX previews read |
|
||||
| 2026-10-12 | minor | Text warps rendered; their adjust values read and written |
|
||||
| 2026-10-12 | trivial | Groups with a small legacy child coordinate space draw outlines, dashes, arrowheads and ink at their own width (#233) |
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (WordArt Transform gallery applies a text warp; previous: minor, 3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (owned PPTX corpus round-trip; notes-master header and footer kept; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (owned PPTX corpus for comments, links, custom shows, groups and inherited placeholders; paragraph defaults and action sounds kept; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (owned PPTX corpus for text effects, body layout, theme effects, fills, bevels and connectors; run effect lists and bevel presets kept; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
Every known shortfall, one entry each, ranked by how much it stops someone who presents with
|
||||
PowerPoint from switching. This is the work list: pick from the top. Numbers are summarized in
|
||||
@@ -91,6 +92,14 @@ Detail in [file-format-parity.md](file-format-parity.md).
|
||||
return") were dropped on save; they are kept now. Embedded fonts, comment threads (#70), media
|
||||
options (#74) and animation rewind (#77) are still open, and there is no corpus of decks from
|
||||
outside this repo. No reference renders from PowerPoint exist to compare against.
|
||||
- **Evidence:** PPTX tests use decks DeckCraft generates. An owned check
|
||||
(`crates/pptx/tests/corpus_style.rs`) now opens, saves and reopens text effects (gradient, outline,
|
||||
shadow, glow, inner shadow, reflection and soft edge), autofit, columns, vertical and RTL text,
|
||||
tab stops, line spacing, theme effects, gradient and pattern fills, 3-D bevels and connectors.
|
||||
That check found a run's inner shadow, reflection and soft edge were dropped, and a bevel preset
|
||||
with no kept `sp3d` was not written back; both are kept now. Comment threads (#70), media options
|
||||
(#74), animation rewind (#77) and embedded fonts are still open, and there is no corpus of decks
|
||||
from outside this repo. No reference renders from PowerPoint exist to compare against.
|
||||
- **Impact:** the main reason to switch is "open my PowerPoint decks". Fidelity on real decks
|
||||
(corporate templates, SmartArt, `chartex` charts, embedded fonts, EMF) is still mostly unknown.
|
||||
- **Estimate:** 20–35 h: grow the owned corpus in `storytold/photocraft-corpus` style (craftrules
|
||||
@@ -314,6 +323,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
| 2026-10-13 | minor | Edit Points: convert to freeform, drag, add and delete vertices (`shape.editPoints`, `shape.movePoint`, `shape.addPoint`, `shape.deletePoint`); left: point types and control handles |
|
||||
| 2026-10-13 | minor | Owned PPTX corpus round-trip (parts and text). Notes-master header and footer text is kept; the corpus gap is now a larger set and PowerPoint reference renders |
|
||||
| 2026-10-13 | minor | Owned PPTX corpus for comments, links and actions, custom shows, rotated groups and inherited placeholders. Paragraph `defRPr` and action sounds kept. Embedded fonts and #70/#74/#77 stay open |
|
||||
| 2026-10-13 | minor | Owned PPTX corpus for text effects, body layout, theme effects, fills, bevels and connectors. A run's inner shadow, reflection and soft edge, and a bevel preset with no kept 3-D XML, are kept. #70, #74, #77 and embedded fonts stay open |
|
||||
| 2026-10-13 | minor | Shape, picture and table rendering checked against PowerPoint on ~150 synthetic edge-case slides; 3-D bevels, cameras and face lighting now render approximately (gap narrowed to extrusion, rigs and the 3-D pane) |
|
||||
| 2026-10-13 | minor | Interactive decks: triggers, return-to-slide state and click rules landed in the slide show; the remaining pieces are one new entry under Features |
|
||||
| 2026-10-12 | minor | IME composition in slide text and notes, and CJK interface text with real glyphs (#101): the IME gap is now Korean on the web and table-cell underlines |
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M9 text warps narrowed to adjust handles; the Transform gallery applies a preset) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (owned PPTX corpus check started; previous: minor, core-workflow alpha gate) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (owned PPTX corpus covers comments, links, custom shows, groups and inherited placeholders; previous: minor, core-workflow alpha gate) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (owned PPTX corpus covers text effects, body layout, theme effects, fills, bevels and connectors; previous: minor, core-workflow alpha gate) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
Forward-looking: where DeckCraft is going and in what order. The one-page summary is
|
||||
[ROADMAP.md](../ROADMAP.md); every shortfall is in [gaps.md](gaps.md); numbers are in
|
||||
@@ -37,6 +38,11 @@ points and lead the beta plan.
|
||||
custom shows, grouped rotated objects, and placeholders that inherit from the layout, and keeps
|
||||
paragraph default formatting and action sounds. Still open: a larger corpus, embedded fonts,
|
||||
comment threads, media options and rewind (#70, #74, #77), EMF/WMF and SVG. 31–55 h.
|
||||
2. **PPTX on real decks:** the owned corpus now round-trips text effects, autofit, columns,
|
||||
vertical and RTL text, tab stops, line spacing, theme effects, gradient and pattern fills, 3-D
|
||||
bevels and connectors, and keeps a run's inner shadow, reflection and soft edge plus a bevel
|
||||
preset that had no 3-D XML. Still open: a larger corpus, embedded fonts, comment threads, media
|
||||
options and rewind (#70, #74, #77), EMF/WMF and SVG. 31–55 h.
|
||||
3. **Review the open PRs:** equations (#55), browser print (#66), Morph alignment (#67), macOS
|
||||
open-with (#68), font lookup (#80), Ukrainian (#34, after the catalog exists). 4–8 h.
|
||||
|
||||
@@ -107,6 +113,7 @@ on common paths. On 2026-10-07 it was ~80% there with these blockers:
|
||||
| First real release run: signing, notarization, installers verified on each OS | Signed releases v0.1.0–v0.4.0 published; installers not hand-checked on each OS; Windows startup crashes on some GPUs (#40, #59) |
|
||||
| PPTX fidelity on a corpus of real decks | First owned check in CI; notes-master header and footer kept. A larger corpus is still beta item 2 |
|
||||
| PPTX fidelity on a corpus of real decks | Owned check for comments, links, custom shows, groups and inherited placeholders. A larger corpus, embedded fonts and #70/#74/#77 stay open; beta item 2 |
|
||||
| PPTX fidelity on a corpus of real decks | Owned check for text effects, body layout, theme effects, fills, bevels and connectors. A larger corpus, embedded fonts and #70/#74/#77 stay open; beta item 2 |
|
||||
| Presenter view and Animation Pane polish | Still open; beta items 9 and 10 |
|
||||
| Soak and fuzz the editor | Still open; beta item 1 |
|
||||
| UI fidelity pass on the most-used ribbon groups and dialogs; first run | Partly (start-screen fixes #20, #49); beta item 11 |
|
||||
@@ -123,5 +130,6 @@ plan above.
|
||||
| 2026-10-13 | minor | M9: the WordArt Transform gallery applies a text warp; adjust handles stay open |
|
||||
| 2026-10-13 | trivial | Owned PPTX corpus check started; notes-master header and footer kept. Beta item 2 stays open for a larger corpus |
|
||||
| 2026-10-13 | trivial | Owned PPTX corpus covers comments, links and actions, custom shows, rotated groups and inherited placeholders. Beta item 2 stays open for a larger corpus, embedded fonts and #70/#74/#77 |
|
||||
| 2026-10-13 | trivial | Owned PPTX corpus covers text effects, body layout, theme effects, fills, bevels and connectors. Beta item 2 stays open for a larger corpus, embedded fonts and #70/#74/#77 |
|
||||
| 2026-10-10 | minor | Added the alpha gate (six core workflows): all pass, two with partial depth; stage stays alpha |
|
||||
| 2026-10-10 | major | Created: current focus, ranked beta plan with hours, milestones M0–M14 with status, the old alpha checklist as history |
|
||||
|
||||
Reference in New Issue
Block a user