From 184b76e2a72416b73c4e35313def163ddba90456 Mon Sep 17 00:00:00 2001 From: Nikita Rakitin Date: Wed, 14 Oct 2026 00:04:22 +0300 Subject: [PATCH] Round-trip text effects, layout, fills, bevels and connectors An owned corpus of those kinds survives open, save and reopen. A run's inner shadow, reflection and soft edge were dropped, and a bevel preset with no kept 3-D XML was not written back, so import and export now keep them. --- ROADMAP.md | 9 +- crates/model/src/text.rs | 8 + crates/pptx/src/read/dml.rs | 3 + crates/pptx/src/read/theme.rs | 3 + crates/pptx/src/write/dml.rs | 28 +++- crates/pptx/src/write/shapes.rs | 2 + crates/pptx/src/write/theme.rs | 2 + crates/pptx/tests/corpus_style.rs | 250 ++++++++++++++++++++++++++++++ docs/file-format-parity.md | 11 +- docs/gaps.md | 14 +- docs/roadmap.md | 12 +- 11 files changed, 322 insertions(+), 20 deletions(-) create mode 100644 crates/pptx/tests/corpus_style.rs diff --git a/ROADMAP.md b/ROADMAP.md index 46a87add..7d8ab840 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-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **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 @@ -111,8 +111,9 @@ Ranked; detail and the full beta plan in [docs/roadmap.md](docs/roadmap.md). 1. **No data loss, no startup crashes:** Close's save prompt (#60), Windows GPU crashes (#40, #59), image paste (#53), editor soak tests. 10–20 h. -2. **PPTX on real decks:** owned corpus, round-trip checks, fixes; #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, @@ -128,6 +129,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 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 | @@ -150,6 +152,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 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-12 | trivial | Progress log: File tab (Backstage, #76) | | 2026-10-12 | trivial | Progress log: lines in scaled groups (#233) | diff --git a/crates/model/src/text.rs b/crates/model/src/text.rs index 6a1a7527..473215b5 100644 --- a/crates/model/src/text.rs +++ b/crates/model/src/text.rs @@ -347,7 +347,12 @@ pub struct RunProps { pub link: Option, pub lang: Option, pub shadow: Option, + /// `a:innerShdw` on the run. Outer shadow stays in `shadow`. + pub inner_shadow: Option, pub glow: Option, + pub reflection: Option, + /// Soft-edge radius in points (`a:softEdge`). + pub soft_edge: Option, /// Spelling/grammar errors are not flagged. pub no_proof: Option, } @@ -378,7 +383,10 @@ impl RunProps { link, lang, shadow, + inner_shadow, glow, + reflection, + soft_edge, no_proof ); } diff --git a/crates/pptx/src/read/dml.rs b/crates/pptx/src/read/dml.rs index 0aee935c..209d05e0 100644 --- a/crates/pptx/src/read/dml.rs +++ b/crates/pptx/src/read/dml.rs @@ -503,7 +503,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), diff --git a/crates/pptx/src/read/theme.rs b/crates/pptx/src/read/theme.rs index 7d6e614c..a94c6b0b 100644 --- a/crates/pptx/src/read/theme.rs +++ b/crates/pptx/src/read/theme.rs @@ -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); diff --git a/crates/pptx/src/write/dml.rs b/crates/pptx/src/write/dml.rs index 16a069fe..0e83768a 100644 --- a/crates/pptx/src/write/dml.rs +++ b/crates/pptx/src/write/dml.rs @@ -339,6 +339,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 { + if preset.is_empty() || preset.len() > 32 || !preset.chars().all(|c| c.is_ascii_alphanumeric()) { + return None; + } + Some(format!("")) +} + 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); @@ -579,10 +588,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() @@ -604,8 +613,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"); diff --git a/crates/pptx/src/write/shapes.rs b/crates/pptx/src/write/shapes.rs index 8b2ea35d..794836bc 100644 --- a/crates/pptx/src/write/shapes.rs +++ b/crates/pptx/src/write/shapes.rs @@ -242,6 +242,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"); diff --git a/crates/pptx/src/write/theme.rs b/crates/pptx/src/write/theme.rs index d83e90f9..48039420 100644 --- a/crates/pptx/src/write/theme.rs +++ b/crates/pptx/src/write/theme.rs @@ -83,6 +83,8 @@ pub fn theme_xml(x: &mut Exp, o: &mut Out, t: &Theme) -> Vec { 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"); } diff --git a/crates/pptx/tests/corpus_style.rs b/crates/pptx/tests/corpus_style.rs new file mode 100644 index 00000000..9f02c641 --- /dev/null +++ b/crates/pptx/tests/corpus_style.rs @@ -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( + "" + .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 +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 1e888c7a..a1e64c7c 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity with PowerPoint -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (Groups with a small legacy child coordinate space draw outlines, dashes, arrowheads and ink at their own width (#233); previously major: created; formats taken from PowerPoint 16.113.4's Info.plist, PPTX coverage measured from `crates/pptx`) · **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** @@ -16,7 +16,7 @@ PowerPoint opens and saves; Viewer = opens only). | Format | PowerPoint | DeckCraft read | DeckCraft write | Fidelity / notes | Tested by | |---|---|---|---|---|---| -| `.pptx` presentation | Editor | yes | yes | Broad (see below); never measured on real decks | `crates/pptx/tests/roundtrip.rs`, `sample.rs`, `malformed.rs`, `sniff.rs` (30 tests); 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 | @@ -66,9 +66,9 @@ the round trip even though DeckCraft doesn't model it. | Shapes: preset geometry (~180 presets), custom geometry, groups, connectors | yes | yes | Presets re-derived in `crates/geom/src/preset.rs` (and `preset/extra.rs`). Outlines, dashes, arrowheads and ink inside a group, and text, keep their size in points whatever the group's child-coordinate scale (#233). | | Fills: solid, gradient, picture (stretch/tile/crop), pattern, group | yes | yes | | | Lines: dash, compound, caps, joins, arrowheads | yes | yes | | -| 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` and date fields | +| 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 | | @@ -99,6 +99,7 @@ the round trip even though DeckCraft doesn't model it. | Date | Change | Summary | |---|---|---| +| 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) | diff --git a/docs/gaps.md b/docs/gaps.md index a2894b42..3a6e1fe0 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where DeckCraft falls short of PowerPoint -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101; previous: minor, core-workflow alpha gate added; stage stays alpha; before that: major, first itemized gap list; full re-measure against PowerPoint for Mac 16.113.4 and the open GitHub issues) · **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 @@ -59,9 +59,14 @@ revision history. Detail in [file-format-parity.md](file-format-parity.md). ### PPTX has never met a corpus of real decks -- **Evidence:** every PPTX test (`crates/pptx/tests`, 30 tests) uses decks DeckCraft generates; - the 2026-10-07 alpha checklist already listed this as open. 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 unknown. - **Estimate:** 20–35 h: build an owned corpus in `storytold/photocraft-corpus` style (craftrules @@ -256,6 +261,7 @@ Detail in [localization-parity.md](localization-parity.md). | Date | Change | Summary | |---|---|---| +| 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-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 | | 2026-10-12 | minor | EMF/WMF text, pattern and hatch brushes drawn; legacy OLE and ActiveX previews shown; the SVG gap keeps only EMF clip regions | diff --git a/docs/roadmap.md b/docs/roadmap.md index 97953034..687a2adc 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # DeckCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (core-workflow alpha gate added; stage stays alpha; previous: major, created; milestones carried from the local execution plan, the beta plan built from gaps.md) · **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 @@ -26,8 +26,11 @@ points and lead the beta plan. 1. **Stop losing work and crashing:** land Close's save prompt (#60, PR #75), the Windows GPU startup crashes (#40, #59), image paste on Windows/Linux (#53). 6–12 h. -2. **PPTX on real decks:** build an owned corpus, round-trip checks in CI, fix what breaks; - comment threads, media options and rewind on save (#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. @@ -96,7 +99,7 @@ on common paths. On 2026-10-07 it was ~80% there with these blockers: | Blocker (2026-10-07) | Status 2026-10-10 | |---|---| | 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 | Still open; now 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 | @@ -109,5 +112,6 @@ plan above. | Date | Change | Summary | |---|---|---| +| 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 |