diff --git a/crates/pptx/src/write/mod.rs b/crates/pptx/src/write/mod.rs index 7fb6a713..225ce0c8 100644 --- a/crates/pptx/src/write/mod.rs +++ b/crates/pptx/src/write/mod.rs @@ -434,8 +434,8 @@ pub fn export(p: &Presentation) -> Result, PptxError> { master_rids.push((master_id, pres.rels.add("slideMaster", &opc::relative("ppt/presentation.xml", &mname), false))); } - // Notes master when any slide has notes. - let has_notes = p.slides.iter().any(|s| !s.notes.is_empty()); + // Notes master when the deck has one or any slide has notes. + let has_notes = p.notes_master.is_some() || p.slides.iter().any(|s| !s.notes.is_empty()); let notes_master = "ppt/notesMasters/notesMaster1.xml"; let mut notes_master_rid = None; if has_notes { @@ -449,9 +449,22 @@ pub fn export(p: &Presentation) -> Result, PptxError> { x.pkg.rels(&tname, &to.rels); let mut w = W::new(); root_open(&mut w, "p:notesMaster", A::new()); - let bg = Background::Ref { idx: 1001, color: deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg1) }; - csld(&mut w, &mut x, &mut no, "", Some(&bg), ¬es_master_shapes(p.notes_size)); - clr_map(&mut w, &[]); + let nm = p.notes_master.as_deref(); + let bg = nm + .and_then(|m| m.background.clone()) + .unwrap_or(Background::Ref { idx: 1001, color: deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg1) }); + // The file's own placeholders are kept; PowerPoint's defaults only when it has none. + let own = nm.map(|m| m.shapes.as_slice()).filter(|s| !s.is_empty()); + let defaults; + let list = match own { + Some(s) => s, + None => { + defaults = notes_master_shapes(p.notes_size); + defaults.as_slice() + } + }; + csld(&mut w, &mut x, &mut no, "", Some(&bg), list); + clr_map(&mut w, nm.map(|m| m.color_map.as_slice()).unwrap_or(&[])); dml::list_style(&mut w, &mut x, &mut no, "p:notesStyle", ¬es_style()); w.close("p:notesMaster"); x.pkg.xml_part(notes_master, CT_NOTES_MASTER, w.finish()); diff --git a/crates/pptx/tests/roundtrip.rs b/crates/pptx/tests/roundtrip.rs index db6d0beb..6729ddf4 100644 --- a/crates/pptx/tests/roundtrip.rs +++ b/crates/pptx/tests/roundtrip.rs @@ -991,3 +991,37 @@ fn transition_defaults_read_like_powerpoint() { assert_eq!((t.kind.as_str(), t.option.as_str(), t.duration_ms), (*kind, *option, 2000), "{el}"); } } + +#[test] +fn notes_master_placeholders_and_colour_map_survive_save() { + let mut p = defaults::new_presentation(None); + let mut box1 = shape(&mut p, "Confidential", 100000.0, 200000.0, 3000000.0, 400000.0); + box1.text = Some(TextBody::from_text("Company confidential")); + let map: Vec<(String, String)> = [("bg1", "dk1"), ("tx1", "lt1"), ("bg2", "dk2"), ("tx2", "lt2")] + .iter() + .chain( + [ + ("accent1", "accent1"), + ("accent2", "accent2"), + ("accent3", "accent3"), + ("accent4", "accent4"), + ("accent5", "accent5"), + ("accent6", "accent6"), + ("hlink", "hlink"), + ("folHlink", "folHlink"), + ] + .iter(), + ) + .map(|(a, b)| (a.to_string(), b.to_string())) + .collect(); + let m = deckcraft_model::Master { name: "Notes Master".into(), shapes: vec![box1], color_map: map.clone(), ..Default::default() }; + p.notes_master = Some(Arc::new(m)); + // No slide has notes: the notes master must still be written. + let q = round(&p); + let nm = q.notes_master.as_deref().expect("notes master kept"); + assert_eq!(nm.shapes.len(), 1, "own shapes, not the defaults"); + let sh = &nm.shapes[0]; + assert_eq!(sh.name, "Confidential"); + assert_eq!(sh.text.as_ref().map(|t| t.text()), Some("Company confidential".to_string())); + assert_eq!(nm.color_map, map); +}