fix(pptx): gate inferred non-click paragraph builds on 500ms/0ms delay when bldP is absent

This commit is contained in:
Neil Cawse committed 2026-11-08 13:36:49 +00:00
1 parent a570031909
commit 3def50c98a
2 files changed
+16 -7

No files matched your search

+6 -2
View File
@@ -330,9 +330,10 @@ pub fn animations(imp: &mut Imp, part: &Part, ctx: &IdCtx, timing: &El, shapes:
for run in out.chunk_by(same_build) {
let effects: Vec<&Animation> = run.iter().map(|(a, ..)| a).collect();
let build = run.first().is_some_and(|r| r.3);
let explicit_bld = run.first().is_some_and(|r| bld_p.get(&(r.1, r.2.clone())) == Some(&true));
let text = effects.first().and_then(|a| find_shape(shapes, a.shape)).and_then(|s| s.text.as_ref());
match (effects.first(), text) {
(Some(first), Some(text)) if build && plays_as_build(&effects, text) => {
(Some(first), Some(text)) if build && plays_as_build(&effects, text, explicit_bld) => {
anims.push(Animation { text_build: TextBuild::ByParagraph, paragraph: None, ..(*first).clone() });
}
_ => anims.extend(effects.into_iter().cloned()),
@@ -345,7 +346,7 @@ pub fn animations(imp: &mut Imp, part: &Part, ctx: &IdCtx, timing: &El, shapes:
/// first one plays: one effect per paragraph the build animates, in paragraph order, later top
/// paragraphs on click (after previous when the build doesn't start on click), deeper ones with
/// their parent, all with the same settings?
fn plays_as_build(effects: &[&Animation], text: &TextBody) -> bool {
fn plays_as_build(effects: &[&Animation], text: &TextBody, explicit_bld: bool) -> bool {
let roles = paragraph_build_roles(text);
let expected = roles.iter().enumerate().filter(|(i, r)| match r {
BuildRole::Top => true,
@@ -353,6 +354,9 @@ fn plays_as_build(effects: &[&Animation], text: &TextBody) -> bool {
BuildRole::Unowned => false,
});
let Some(first) = effects.first() else { return false };
if !explicit_bld && first.start != AnimStart::OnClick && (span_ms(first) != 500 || first.delay_ms != 0) {
return false;
}
let settings = |a: &Animation| Animation { start: AnimStart::OnClick, paragraph: None, ..a.clone() };
let base = settings(first);
let mut matched = 0;
+10 -5
View File
@@ -317,6 +317,7 @@ fn right_of(p: &Presentation) -> ShapeId {
by_name(p, "Right").id
}
/// Return a copy of the `.pptx` archive `bytes` with `entry_name` rewritten by `f`.
fn rewrite_zip_entry(bytes: &[u8], entry_name: &str, f: impl Fn(&str) -> String) -> Vec<u8> {
use std::io::{Cursor, Read, Write};
let mut r = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
@@ -370,22 +371,26 @@ fn paragraph_build_without_bld_lst_still_folds_into_by_paragraph() {
assert_eq!(back.slides[0].animations.len(), 1);
assert_eq!(back.slides[0].animations[0].text_build, TextBuild::ByParagraph);
// Non-500ms After Previous paragraph build without bldLst is also inferred.
// Explicit bldP build="p" folds non-500ms After Previous builds, while without bldLst an 800ms
// After Previous run stays separate so playback does not alter its 800ms stagger to 500ms.
let mut pres_after = pres.clone();
let s_after = Arc::make_mut(&mut pres_after.slides[0]);
s_after.animations[0].start = AnimStart::AfterPrevious;
s_after.animations[0].duration_ms = 800;
let bytes_after = deckcraft_pptx::export(&pres_after).unwrap();
let back_explicit_after = deckcraft_pptx::import(&bytes_after).unwrap();
assert_eq!(back_explicit_after.slides[0].animations.len(), 1);
assert_eq!(back_explicit_after.slides[0].animations[0].text_build, TextBuild::ByParagraph);
assert_eq!(back_explicit_after.slides[0].animations[0].duration_ms, 800);
let without_bld_after = rewrite_zip_entry(&bytes_after, "ppt/slides/slide1.xml", |xml| {
let start = xml.find("<p:bldLst>").expect("bldLst");
let end = xml.find("</p:bldLst>").expect("/bldLst") + "</p:bldLst>".len();
format!("{}{}", &xml[..start], &xml[end..])
});
let back_after = deckcraft_pptx::import(&without_bld_after).unwrap();
assert_eq!(back_after.slides[0].animations.len(), 1);
assert_eq!(back_after.slides[0].animations[0].text_build, TextBuild::ByParagraph);
assert_eq!(back_after.slides[0].animations[0].duration_ms, 800);
assert_eq!(back_after.slides[0].animations.len(), 2);
assert!(back_after.slides[0].animations.iter().all(|a| a.text_build == TextBuild::default()));
// Explicit bldP with build!="p" (e.g. build="cust") opts out of folding.
let custom_bld = rewrite_zip_entry(&bytes, "ppt/slides/slide1.xml", |xml| xml.replace(r#"build="p""#, r#"build="cust""#));
let back_cust = deckcraft_pptx::import(&custom_bld).unwrap();