mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 08:50:47 -06:00
Merge pull request #1723
This commit is contained in:
@@ -152,6 +152,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-11-02 | Native and PPTX saves retain optional table right-to-left direction, including explicit false and omission; two-save round-trip tests keep labeled columns in order |
|
||||
| 2026-11-02 | Text runs preserve alternate-language metadata independently from the main language through native and PPTX saves |
|
||||
| 2026-11-02 | File › About replaces the unused Account page, with version, project links, contributor/model credits and appearance preferences |
|
||||
| 2026-11-02 | PPTX reflection effects keep their authored start position, affine scale/skew, direction and fade, alignment, and rotation flag through native and PPTX saves |
|
||||
| 2026-11-02 | Tiled picture fills in groups: a tile (`a:blipFill` with `a:tile`) keeps its size, offset and anchor in slide points whatever the group's child-coordinate scale, uniform or stretched unevenly, nested or flipped (also a group's own tile fill passed to its members, and a picture shape's own tile, with its shadow and reflection), and tiles with `rotWithShape="0"` keep their size in points at any zoom, as PowerPoint draws them (measured on its own PNG export of a synthetic probe deck: every grouped case within ~3/255 of the ungrouped control's own difference from PowerPoint, which is ~13/255 from resampling). Before, a group whose child space was ~1800 times smaller than its box showed one flat texel of the texture (a Venn diagram with paper-textured circles and a hatched intersection) |
|
||||
| 2026-10-31 | No Fill shape interiors allow selection and editing of underlying objects, even when a quick style remains; hit-testing uses the renderer’s fill resolution. Outlines, rotation, text targets and actual fills keep their hit behaviour (#1621) |
|
||||
| 2026-10-30 | Print-preview tests use the URL emitted by egui and its exact file path; previews are explicitly removed before the macOS process exits |
|
||||
|
||||
@@ -135,7 +135,14 @@ pub fn apply(sh: &mut Shape, id: &str) -> bool {
|
||||
let e = Effects {
|
||||
outer_shadow: lk.shadow.map(shadow),
|
||||
soft_edge: lk.soft_edge,
|
||||
reflection: lk.reflection.then_some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.35, dist: 0.0 }),
|
||||
reflection: lk.reflection.then_some(Reflection {
|
||||
blur: 0.5,
|
||||
start_alpha: 0.5,
|
||||
end_alpha: 0.0,
|
||||
end_pos: 0.35,
|
||||
dist: 0.0,
|
||||
..Default::default()
|
||||
}),
|
||||
bevel: lk.bevel.map(str::to_string),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -371,9 +371,9 @@ pub(super) fn effects(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let soft = p.get("softEdges").map(|v| v.as_f64().filter(|x| *x > 0.0).map(|x| x.min(100.0)));
|
||||
let refl = str_param(p, "reflection").map(|r| match r {
|
||||
"none" => None,
|
||||
"half" => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.55, dist: 4.0 }),
|
||||
"full" => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.9, dist: 4.0 }),
|
||||
_ => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.35, dist: 0.0 }),
|
||||
"half" => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.55, dist: 4.0, ..Default::default() }),
|
||||
"full" => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.9, dist: 4.0, ..Default::default() }),
|
||||
_ => Some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.35, dist: 0.0, ..Default::default() }),
|
||||
});
|
||||
let three_d = p.get("threeD").filter(|v| v.is_object());
|
||||
let bevel = str_param(p, "bevel").map(|b| if b == "none" { None } else { Some(b.to_string()) });
|
||||
@@ -467,7 +467,14 @@ pub(super) fn effects(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
e.glow = (g.radius > 0.0).then_some(g);
|
||||
}
|
||||
if let Some(v) = &refl_props {
|
||||
let mut r = e.reflection.take().unwrap_or(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.35, dist: 0.0 });
|
||||
let mut r = e.reflection.take().unwrap_or(Reflection {
|
||||
blur: 0.5,
|
||||
start_alpha: 0.5,
|
||||
end_alpha: 0.0,
|
||||
end_pos: 0.35,
|
||||
dist: 0.0,
|
||||
..Default::default()
|
||||
});
|
||||
if let Some(t) = num(v, "transparency") {
|
||||
r.start_alpha = 1.0 - t.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -5362,7 +5362,8 @@ fn opening_a_file_caps_absurd_effect_sizes() {
|
||||
e.soft_edge = Some(f64::NAN);
|
||||
let sw = e.outer_shadow.as_mut().unwrap();
|
||||
(sw.blur, sw.dist, sw.sx) = (huge, huge, huge);
|
||||
*e.reflection.as_mut().unwrap() = Reflection { blur: huge, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.5, dist: huge };
|
||||
*e.reflection.as_mut().unwrap() =
|
||||
Reflection { blur: huge, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.5, dist: huge, ..Default::default() };
|
||||
}
|
||||
let glow = Some(Glow { color: deckcraft_model::style::ColorRef::rgb(deckcraft_color::Rgba::BLACK), radius: huge });
|
||||
let runs: Vec<&mut deckcraft_model::text::Run> = match &mut sh.kind {
|
||||
|
||||
@@ -495,6 +495,9 @@ fn sane_shadow(s: &mut deckcraft_model::style::Shadow) {
|
||||
fn sane_reflection(r: &mut deckcraft_model::style::Reflection) {
|
||||
r.blur = effect_size(r.blur, MAX_REFLECTION);
|
||||
r.dist = effect_size(r.dist, MAX_REFLECTION);
|
||||
if r.align.as_deref().is_some_and(|v| !matches!(v, "tl" | "t" | "tr" | "l" | "ctr" | "r" | "bl" | "b" | "br")) {
|
||||
r.align = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Run properties a list style hands down to the text that inherits it.
|
||||
|
||||
@@ -316,6 +316,54 @@ fn a_file_saved_with_non_finite_numbers_reopens() {
|
||||
assert_eq!(load(&save(&back).unwrap()).unwrap(), back);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflection_transform_survives_two_native_saves_and_absent_fields_stay_absent() {
|
||||
use deckcraft_model::style::Reflection;
|
||||
use deckcraft_model::{Effects, Shape};
|
||||
let mut p = Presentation::default();
|
||||
let reflection = Reflection {
|
||||
blur: 1.0,
|
||||
start_alpha: 0.5,
|
||||
end_alpha: 0.0,
|
||||
end_pos: 0.75,
|
||||
dist: 2.0,
|
||||
st_pos: Some(20_000),
|
||||
fade_dir: Some(0),
|
||||
sx: Some(75_000),
|
||||
sy: Some(-50_000),
|
||||
kx: Some(600_000),
|
||||
align: Some("bl".into()),
|
||||
rotate_with_shape: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
let shape = Shape { effects: Some(Effects { reflection: Some(reflection.clone()), ..Default::default() }), ..Default::default() };
|
||||
Arc::make_mut(&mut p.slides[0]).shapes.push(shape);
|
||||
let once = load(&save(&p).expect("save")).expect("load");
|
||||
let twice = load(&save(&once).expect("save again")).expect("load again");
|
||||
assert_eq!(twice.slides[0].shapes.last().and_then(|s| s.effects.as_ref()).and_then(|e| e.reflection.as_ref()), Some(&reflection));
|
||||
|
||||
let absent = Reflection {
|
||||
blur: 1.0,
|
||||
start_alpha: 1.0,
|
||||
end_alpha: 0.0,
|
||||
end_pos: 1.0,
|
||||
dist: 0.0,
|
||||
st_pos: None,
|
||||
dir: None,
|
||||
fade_dir: None,
|
||||
sx: None,
|
||||
sy: None,
|
||||
kx: None,
|
||||
ky: None,
|
||||
align: None,
|
||||
rotate_with_shape: None,
|
||||
};
|
||||
let shape = Shape { effects: Some(Effects { reflection: Some(absent.clone()), ..Default::default() }), ..Default::default() };
|
||||
Arc::make_mut(&mut p.slides[0]).shapes = vec![shape];
|
||||
let reopened = load(&save(&p).expect("save absent values")).expect("load absent values");
|
||||
assert_eq!(reopened.slides[0].shapes.last().and_then(|s| s.effects.as_ref()).and_then(|e| e.reflection.as_ref()), Some(&absent));
|
||||
}
|
||||
|
||||
/// Files written before [`finite::Finite`] (plain serde_json: NaN as `null`) open too,
|
||||
/// including a `null` inside a tagged enum, where serde reports only the enclosing object.
|
||||
#[test]
|
||||
|
||||
@@ -459,13 +459,44 @@ pub struct Glow {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Reflection {
|
||||
pub blur: f64,
|
||||
pub start_alpha: f64,
|
||||
pub end_alpha: f64,
|
||||
pub end_pos: f64,
|
||||
pub dist: f64,
|
||||
/// DrawingML reflection transform values retain their native integer units.
|
||||
pub st_pos: Option<i32>,
|
||||
pub dir: Option<i32>,
|
||||
pub fade_dir: Option<i32>,
|
||||
pub sx: Option<i32>,
|
||||
pub sy: Option<i32>,
|
||||
pub kx: Option<i32>,
|
||||
pub ky: Option<i32>,
|
||||
pub align: Option<String>,
|
||||
pub rotate_with_shape: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for Reflection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blur: 0.0,
|
||||
start_alpha: 0.0,
|
||||
end_alpha: 0.0,
|
||||
end_pos: 0.0,
|
||||
dist: 0.0,
|
||||
st_pos: None,
|
||||
dir: Some(5_400_000),
|
||||
fade_dir: None,
|
||||
sx: None,
|
||||
sy: Some(-100_000),
|
||||
kx: None,
|
||||
ky: None,
|
||||
align: Some("bl".into()),
|
||||
rotate_with_shape: Some(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -322,6 +322,15 @@ pub fn effects(el: &El) -> Effects {
|
||||
end_alpha: pct(e, "endA").unwrap_or(0.0),
|
||||
end_pos: pct(e, "endPos").unwrap_or(1.0),
|
||||
dist: pt(e, "dist").unwrap_or(0.0),
|
||||
st_pos: e.i64("stPos").and_then(|v| i32::try_from(v).ok()),
|
||||
dir: e.i64("dir").and_then(|v| i32::try_from(v).ok()),
|
||||
fade_dir: e.i64("fadeDir").and_then(|v| i32::try_from(v).ok()),
|
||||
sx: e.i64("sx").and_then(|v| i32::try_from(v).ok()),
|
||||
sy: e.i64("sy").and_then(|v| i32::try_from(v).ok()),
|
||||
kx: e.i64("kx").and_then(|v| i32::try_from(v).ok()),
|
||||
ky: e.i64("ky").and_then(|v| i32::try_from(v).ok()),
|
||||
align: e.attr("algn").filter(|v| matches!(*v, "tl" | "t" | "tr" | "l" | "ctr" | "r" | "bl" | "b" | "br")).map(str::to_owned),
|
||||
rotate_with_shape: e.bool("rotWithShape"),
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -357,10 +357,18 @@ pub fn effects(w: &mut W, fx: &Effects) {
|
||||
.a("endA", pct(r.end_alpha.clamp(0.0, 1.0)))
|
||||
.a("endPos", pct(r.end_pos.clamp(0.0, 1.0)))
|
||||
.a("dist", emu(r.dist).max(0))
|
||||
.a("dir", 5_400_000)
|
||||
.a("sy", -100_000)
|
||||
.a("algn", "bl")
|
||||
.a("rotWithShape", "0"),
|
||||
// Kept from a file as read: held to the schema's ranges so a stray value can't
|
||||
// make PowerPoint repair the file (ST_PositiveFixedAngle, ST_FixedAngle,
|
||||
// ST_PositiveFixedPercentage).
|
||||
.o("stPos", r.st_pos.map(|v| v.clamp(0, 100_000)))
|
||||
.o("dir", r.dir.map(|v| v.rem_euclid(21_600_000)))
|
||||
.o("fadeDir", r.fade_dir.map(|v| v.rem_euclid(21_600_000)))
|
||||
.o("sx", r.sx)
|
||||
.o("sy", r.sy)
|
||||
.o("kx", r.kx.map(|v| v.clamp(-5_399_999, 5_399_999)))
|
||||
.o("ky", r.ky.map(|v| v.clamp(-5_399_999, 5_399_999)))
|
||||
.o("algn", r.align.as_deref())
|
||||
.b("rotWithShape", r.rotate_with_shape),
|
||||
);
|
||||
}
|
||||
if let Some(r) = fx.soft_edge {
|
||||
|
||||
@@ -34,7 +34,7 @@ fn glow() -> Glow {
|
||||
}
|
||||
|
||||
fn reflection() -> Reflection {
|
||||
Reflection { blur: 2.0, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.75, dist: 6.0 }
|
||||
Reflection { blur: 2.0, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.75, dist: 6.0, ..Default::default() }
|
||||
}
|
||||
|
||||
fn gradient(angle: f64) -> Fill {
|
||||
|
||||
@@ -103,7 +103,7 @@ fn effects_and_links() -> Vec<u8> {
|
||||
"Reflected",
|
||||
"",
|
||||
8636000,
|
||||
r#"<a:reflection blurRad="6350" stA="50000" endA="300" endPos="55500" dist="50800" dir="5400000" sy="-100000" algn="bl" rotWithShape="0"/>"#,
|
||||
r#"<a:reflection blurRad="6350" stA="50000" endA="300" endPos="55500" dist="50800" stPos="20000" dir="5400000" fadeDir="0" sx="75000" sy="-50000" kx="600000" ky="-300000" algn="bl" rotWithShape="0"/>"#,
|
||||
r#"<a:p><a:r><a:rPr lang="en-US"><a:hlinkMouseOver r:id="rId90" tooltip="Preview"/></a:rPr><a:t>Hover me</a:t></a:r></a:p>"#,
|
||||
);
|
||||
let slide = format!(
|
||||
@@ -134,6 +134,11 @@ fn effects_and_links_survive_a_round_trip() {
|
||||
assert_eq!(fx(get("Soft edged")).soft_edge, Some(5.0));
|
||||
let r = fx(get("Reflected")).reflection.expect("reflection");
|
||||
assert!((r.end_pos - 0.555).abs() < 1e-6 && (r.dist - 4.0).abs() < 1e-6, "{r:?}");
|
||||
assert_eq!(
|
||||
(r.st_pos, r.dir, r.fade_dir, r.sx, r.sy, r.kx, r.ky),
|
||||
(Some(20_000), Some(5_400_000), Some(0), Some(75_000), Some(-50_000), Some(600_000), Some(-300_000))
|
||||
);
|
||||
assert_eq!((r.align.as_deref(), r.rotate_with_shape), (Some("bl"), Some(false)));
|
||||
let shape = get("Shadowed");
|
||||
assert!(matches!(shape.click.as_ref().map(|h| (&h.action, h.highlight_click)), Some((Action::EndShow, true))));
|
||||
assert!(
|
||||
@@ -173,7 +178,17 @@ fn effects_and_links_survive_resave(a: &Presentation) {
|
||||
assert_eq!(ha, hb, "text hover of {n}");
|
||||
}
|
||||
let out = deckcraft_pptx::export(&b).expect("export");
|
||||
assert!(part(&out, "ppt/slides/slide1.xml").contains("hlinkMouseOver"));
|
||||
let xml = part(&out, "ppt/slides/slide1.xml");
|
||||
assert!(xml.contains("hlinkMouseOver"));
|
||||
let c = deckcraft_pptx::import(&out).expect("second reopen");
|
||||
assert_eq!(fx(shapes_named(&b, 0)("Reflected")).reflection, fx(shapes_named(&c, 0)("Reflected")).reflection);
|
||||
assert!(
|
||||
xml.contains("stPos=\"20000\"")
|
||||
&& xml.contains("fadeDir=\"0\"")
|
||||
&& xml.contains("sx=\"75000\"")
|
||||
&& xml.contains("sy=\"-50000\"")
|
||||
&& xml.contains("kx=\"600000\"")
|
||||
);
|
||||
}
|
||||
|
||||
/// Two masters with their own themes (colours, fonts) and layouts, slides on each.
|
||||
|
||||
@@ -2138,3 +2138,26 @@ fn default_durations_match_powerpoint() {
|
||||
assert_eq!(a.4, ms, "{effect}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflection_angles_and_skews_out_of_range_are_written_inside_the_schema() {
|
||||
let mut p = Presentation::default();
|
||||
let mut slide = new_slide(&mut p, "Reflection ranges");
|
||||
let mut s = shape(&mut p, "Reflected", 10.0, 10.0, 200.0, 80.0);
|
||||
s.effects = Some(deckcraft_model::style::Effects {
|
||||
reflection: Some(deckcraft_model::style::Reflection {
|
||||
st_pos: Some(150_000),
|
||||
dir: Some(-5),
|
||||
fade_dir: Some(21_600_000),
|
||||
kx: Some(9_000_000),
|
||||
ky: Some(-9_000_000),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
slide.shapes.push(s);
|
||||
p.slides.push(Arc::new(slide));
|
||||
|
||||
let r = round(&p).slides.last().and_then(|sl| by_name(sl, "Reflected").effects.clone()).and_then(|fx| fx.reflection).expect("reflection");
|
||||
assert_eq!((r.st_pos, r.dir, r.fade_dir, r.kx, r.ky), (Some(100_000), Some(21_599_995), Some(0), Some(5_399_999), Some(-5_399_999)));
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ fn scene(shape: &str, effect: Effect) -> Presentation {
|
||||
})
|
||||
}
|
||||
Effect::Glow => effects.glow = Some(Glow { color: ColorRef::rgb(Rgba::rgb(255, 0, 0)), radius: 8.0 }),
|
||||
Effect::Reflection => effects.reflection = Some(Reflection { blur: 0.0, start_alpha: 1.0, end_alpha: 0.0, end_pos: 1.0, dist: 10.0 }),
|
||||
Effect::Reflection => {
|
||||
effects.reflection = Some(Reflection { blur: 0.0, start_alpha: 1.0, end_alpha: 0.0, end_pos: 1.0, dist: 10.0, ..Default::default() })
|
||||
}
|
||||
}
|
||||
let slide = std::sync::Arc::make_mut(&mut p.slides[0]);
|
||||
slide.background = Some(deckcraft_model::Background::Fill { fill: Fill::solid(ColorRef::rgb(Rgba::WHITE)) });
|
||||
|
||||
@@ -577,7 +577,7 @@ fn a_tiled_pictures_reflection_keeps_the_tile_size_inside_a_scaled_group() {
|
||||
let mut p = Presentation::default();
|
||||
let media = p.add_media("t.png", "image/png", half_red_half_blue()).expect("media");
|
||||
let mode = PictureMode::Tile { tx: 0.0, ty: 0.0, sx: 10.0, sy: 10.0, flip: "none".into(), align: "tl".into() };
|
||||
let rf = Reflection { blur: 0.0, start_alpha: 1.0, end_alpha: 0.0, end_pos: 0.5, dist: 0.0 };
|
||||
let rf = Reflection { blur: 0.0, start_alpha: 1.0, end_alpha: 0.0, end_pos: 0.5, dist: 0.0, ..Default::default() };
|
||||
let img = in_a_scaled_group(kx, ky, p, |w, h| Shape {
|
||||
id: ShapeId(22),
|
||||
xfrm: Some(Xfrm::new(0.0, 0.0, w, h)),
|
||||
@@ -935,7 +935,7 @@ fn a_picture_reflects_its_pixels() {
|
||||
let mut p = picture_deck(None);
|
||||
let s = std::sync::Arc::make_mut(&mut p.slides[0]);
|
||||
if let Some(sh) = s.shapes.first_mut() {
|
||||
let rf = deckcraft_model::style::Reflection { blur: 0.0, start_alpha: 1.0, end_alpha: 0.0, end_pos: 0.5, dist: 0.0 };
|
||||
let rf = deckcraft_model::style::Reflection { blur: 0.0, start_alpha: 1.0, end_alpha: 0.0, end_pos: 0.5, dist: 0.0, ..Default::default() };
|
||||
sh.effects = Some(deckcraft_model::Effects { reflection: Some(rf), ..Default::default() });
|
||||
}
|
||||
let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() });
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-02 · **Change:** minor (optional table RTL direction survives native and two PPTX saves; scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-02 · **Change:** minor (run alternate-language metadata round-trips through native and PPTX formats; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: seven owned PPTX corpus files, embedded fonts, EMF/WMF clip regions, threaded comments (#70), chart trendlines and workbooks (#335), SmartArt written as a diagram, tags and custom XML kept, picture export DPI; duplicated rows and headers folded; previous: trivial, lines in scaled groups, #233) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-11-02 · **Change:** trivial (PPTX reflection transforms and fade geometry survive resaves; renderer parity remains partial; previous: minor, native chart payloads stored in bounded ZIP members) · **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**
|
||||
@@ -123,6 +124,7 @@ the round trip even though DeckCraft doesn't model it.
|
||||
| 2026-11-02 | minor | Optional table RTL direction is read and written as `a:tblPr/@rtl`; native and two PPTX saves preserve true, false and absent values with distinguishable column text |
|
||||
| 2026-11-02 | minor | Text runs preserve alternate-language metadata in native and PPTX round trips |
|
||||
| 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged |
|
||||
| 2026-11-02 | trivial | Reflection start position, affine scale/skew, direction and fade, alignment and rotation attributes survive native and PPTX saves; rendering remains partial |
|
||||
| 2026-10-15 | trivial | Native `.deckcraft` files serialize a nonempty chart `kept` sidecar (relationships, part names, content types and bytes). Older files without it still load; empty sidecars are omitted. Headless PPTX → edit → native save/reopen → PPTX checks keep the workbook and drawing bytes; editing chart data still drops the stale workbook |
|
||||
| 2026-10-15 | trivial | PPTX interactive timing sequences keep later On Click steps and paragraph click builds (`trigger_steps.rs`); mixed starts and independent buttons survive import/edit/save/reopen |
|
||||
| 2026-10-15 | minor | A `.pptx` slide that can't be read is reported instead of dropped in silence: `file.open` and `file.openBytes` return `lostSlides`, the app shows a message and opens a repaired copy (Save asks for a new name), and `deckcraft-cli` warns on stderr (#203) |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Where DeckCraft falls short of PowerPoint
|
||||
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-02 · **Change:** trivial (text run alternate-language metadata now survives native and PPTX saves; per-run language editing and proofing gaps remain; scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-30 · **Last updated:** 2026-11-02 · **Change:** trivial (PPTX reflection transforms and fade geometry survive resaves; rendering remains partial) · **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
|
||||
@@ -369,6 +370,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | trivial | Run alternate-language metadata now survives native and PPTX saves; UI and proofing gaps remain; scores unchanged |
|
||||
| 2026-11-02 | trivial | PPTX reflection geometry is retained through native and PPTX resaves; no rendering parity claim |
|
||||
| 2026-10-31 | trivial | No Fill hit-testing follows resolved fill instead of the presence of a style (#1621); scores unchanged |
|
||||
| 2026-10-30 | minor | Slide-show click targets use each shape’s own animation state; scale and rotation test added |
|
||||
| 2026-10-28 | trivial | Automatic lists now run independently of Replace text as you type; proofing gap description updated |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **Generated:** 2026-10-14 (bullets/numbering remain D after empty-line marker review; summary by `cargo xtask parity`; rows re-scored by the 2026-10-14 community PR wave, duplicate Thesaurus, AutoCorrect, Artistic effects and Corrections rows folded; Preview in editor re-scored 2026-10-13; earlier rows re-scored 2026-10-10 against PowerPoint for Mac 16.113.4; video/animated GIF row re-scored 2026-10-11, #171) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
> **Last updated:** 2026-11-02 · Tiled picture fills keep their size in scaled groups and upright tiles (#1693); Picture and texture fill row re-assessed: remains P, since only how tiles are sized changed. Scores unchanged.
|
||||
> **Last updated:** 2026-11-02 · Reflection transforms and fade geometry now survive native and PPTX resaves; the row remains P because rendering is still partial. No score change.
|
||||
>
|
||||
> **Previously (2026-11-01):** Locked-shape and no-outline regressions added for #1621; fill row re-assessed: remains D; No Fill hit-testing now follows resolved appearance. Scores unchanged.
|
||||
|
||||
@@ -242,5 +242,6 @@ Open P0 items: Contextual tabs (Shape Format, Picture Format, Table Design/Layou
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | trivial | Reflection transform/fade serialization retained; its P score stays because rendering remains partial |
|
||||
| 2026-11-02 | trivial | Tiled picture fills keep their size in scaled groups and upright tiles (#1693); Picture and texture fill row remains P and summary scores are unchanged |
|
||||
| 2026-10-31 | trivial | No Fill interior hit-testing follows resolved fill (#1621); fill row remains D and summary scores are unchanged |
|
||||
|
||||
Reference in New Issue
Block a user