diff --git a/crates/pptx/src/read/timing.rs b/crates/pptx/src/read/timing.rs index fcf01953..f331ecfe 100644 --- a/crates/pptx/src/read/timing.rs +++ b/crates/pptx/src/read/timing.rs @@ -33,11 +33,11 @@ pub fn transition(imp: &mut Imp, part: &Part, t: &El) -> Option { }, } let default_dur = deckcraft_model::anim::TRANSITIONS.iter().find(|x| x.0 == tr.kind).map(|x| x.3).unwrap_or(1000); + // `spd` defaults to "fast" in the schema; PowerPoint omits both attributes for 0.5 s. tr.duration_ms = ms_attr(t, "dur").unwrap_or(match t.attr("spd") { - Some("fast") => 500, Some("med") => 750, Some("slow") => default_dur.max(1000), - _ => default_dur, + _ => 500, }); if let Some(snd) = t.path(&["sndAc", "stSnd", "snd"]) { tr.sound = dml::media_ref(imp, part, snd); diff --git a/crates/pptx/tests/transition_xml.rs b/crates/pptx/tests/transition_xml.rs new file mode 100644 index 00000000..ffa5bcf4 --- /dev/null +++ b/crates/pptx/tests/transition_xml.rs @@ -0,0 +1,72 @@ +//! Transition XML as other applications write it, spliced into an exported deck and read back. + +use std::io::{Cursor, Read, Write}; + +use deckcraft_model::anim::Transition; +use deckcraft_model::defaults; + +const P159: &str = "http://schemas.microsoft.com/office/powerpoint/2015/09/main"; +const P14: &str = "http://schemas.microsoft.com/office/powerpoint/2010/main"; + +/// Exports a one-slide deck, replaces the slide's `mc:AlternateContent` transition block with +/// `xml` and returns the imported transition of that slide. +fn import_with(xml: &str) -> Transition { + let mut p = defaults::new_presentation(None); + std::sync::Arc::make_mut(&mut p.slides[0]).transition = Some(Transition { kind: "fade".into(), duration_ms: 3000, ..Default::default() }); + let bytes = deckcraft_pptx::export(&p).expect("export"); + let mut z = zip::ZipArchive::new(Cursor::new(&bytes)).expect("zip"); + let mut out = zip::ZipWriter::new(Cursor::new(Vec::new())); + let o = zip::write::SimpleFileOptions::default(); + for i in 0..z.len() { + let mut f = z.by_index(i).expect("entry"); + let mut d = vec![]; + f.read_to_end(&mut d).expect("read"); + if f.name() == "ppt/slides/slide1.xml" { + let s = String::from_utf8(d).expect("utf8"); + let t = s.find(" String { + format!( + r#"{choice}{fallback}"# + ) +} + +/// PowerPoint 2021/2024 write a 0.5 s Morph with neither `spd` nor `p14:dur` (issue #117). +#[test] +fn powerpoint_half_second_morph_without_spd() { + let xml = mc(P159, "p159", r#""#, ""); + let t = import_with(&xml); + assert_eq!((t.kind.as_str(), t.duration_ms), ("morph", 500)); +} + +#[test] +fn missing_spd_is_fast_for_a_plain_transition() { + let t = import_with(""); + assert_eq!((t.kind.as_str(), t.duration_ms), ("fade", 500)); +} + +#[test] +fn spd_and_dur_still_apply() { + let t = import_with(r#""#); + assert_eq!(t.duration_ms, 750); + let xml = mc( + P159, + "p159", + &format!(r#""#), + r#""#, + ); + let t = import_with(&xml); + assert_eq!((t.kind.as_str(), t.duration_ms), ("morph", 1250)); +}