mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-13 13:43:50 +00:00
fix(pptx): drop timing gate in plays_as_build, merge duplicate bldP entries, and expand build tests
This commit is contained in:
1 parent
9d9610a893
commit
a570031909
2 files changed
+82
-7
No files matched your search
@@ -245,7 +245,10 @@ pub fn animations(imp: &mut Imp, part: &Part, ctx: &IdCtx, timing: &El, shapes:
|
||||
.map(|b| {
|
||||
b.children_named("bldP")
|
||||
.filter_map(|p| Some(((p.u32("spid")?, p.attr("grpId").unwrap_or("0").to_string()), p.attr("build") == Some("p"))))
|
||||
.collect()
|
||||
.fold(std::collections::HashMap::new(), |mut m, (k, v)| {
|
||||
m.entry(k).and_modify(|e| *e |= v).or_insert(v);
|
||||
m
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut out: Vec<(Animation, u32, String, bool)> = vec![];
|
||||
@@ -327,10 +330,9 @@ 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, explicit_bld) => {
|
||||
(Some(first), Some(text)) if build && plays_as_build(&effects, text) => {
|
||||
anims.push(Animation { text_build: TextBuild::ByParagraph, paragraph: None, ..(*first).clone() });
|
||||
}
|
||||
_ => anims.extend(effects.into_iter().cloned()),
|
||||
@@ -343,7 +345,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, explicit_bld: bool) -> bool {
|
||||
fn plays_as_build(effects: &[&Animation], text: &TextBody) -> bool {
|
||||
let roles = paragraph_build_roles(text);
|
||||
let expected = roles.iter().enumerate().filter(|(i, r)| match r {
|
||||
BuildRole::Top => true,
|
||||
@@ -351,9 +353,6 @@ fn plays_as_build(effects: &[&Animation], text: &TextBody, explicit_bld: 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;
|
||||
|
||||
@@ -316,3 +316,79 @@ fn left_of(p: &Presentation) -> ShapeId {
|
||||
fn right_of(p: &Presentation) -> ShapeId {
|
||||
by_name(p, "Right").id
|
||||
}
|
||||
|
||||
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();
|
||||
let mut out = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut w = zip::ZipWriter::new(&mut out);
|
||||
let opts = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
|
||||
for i in 0..r.len() {
|
||||
let mut file = r.by_index(i).unwrap();
|
||||
let name = file.name().to_string();
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
w.start_file(&name, opts).unwrap();
|
||||
if name == entry_name {
|
||||
let text = String::from_utf8(buf).unwrap();
|
||||
w.write_all(f(&text).as_bytes()).unwrap();
|
||||
} else {
|
||||
w.write_all(&buf).unwrap();
|
||||
}
|
||||
}
|
||||
w.finish().unwrap();
|
||||
}
|
||||
out.into_inner()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_build_without_bld_lst_still_folds_into_by_paragraph() {
|
||||
use deckcraft_model::anim::{AnimClass, AnimStart, Animation, TextBuild};
|
||||
let mut pres = defaults::new_presentation(None);
|
||||
let mut sh = shape(&mut pres, "Title 1", 50.0, 50.0, 400.0, 200.0);
|
||||
let shape_id = sh.id;
|
||||
sh.text = Some(TextBody { paragraphs: vec![Paragraph::new("First"), Paragraph::new("Second")], ..Default::default() });
|
||||
let s0 = Arc::make_mut(&mut pres.slides[0]);
|
||||
s0.shapes.push(sh);
|
||||
s0.animations.push(Animation {
|
||||
shape: shape_id,
|
||||
class: AnimClass::Entrance,
|
||||
effect: "fade".into(),
|
||||
start: AnimStart::OnClick,
|
||||
duration_ms: 500,
|
||||
text_build: TextBuild::ByParagraph,
|
||||
..Default::default()
|
||||
});
|
||||
let bytes = deckcraft_pptx::export(&pres).unwrap();
|
||||
let without_bld = rewrite_zip_entry(&bytes, "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 = deckcraft_pptx::import(&without_bld).unwrap();
|
||||
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.
|
||||
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 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);
|
||||
|
||||
// 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();
|
||||
assert_eq!(back_cust.slides[0].animations.len(), 2);
|
||||
assert!(back_cust.slides[0].animations.iter().all(|a| a.text_build == TextBuild::default()));
|
||||
}
|
||||
Reference in new issue
Block a user