diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index c81d540a..7fd7c7fb 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -129,10 +129,10 @@ fn open_bytes(s: &mut Session, p: &Value) -> Result { Ok(json!({"document": i, "missingFonts": s.doc()?.missing_fonts})) } -/// Encode the presentation in `format` (`deckcraft`, `pptx`, `outline`). +/// Encode the presentation in `format` (`deckcraft`, `pptx`, `potx`, `ppsx`, `outline`, `pdf`). pub fn save_bytes(doc: &Presentation, format: &str) -> Result> { match format { - "pptx" | "potx" | "ppsx" => deckcraft_pptx::export(doc).map_err(|e| EngineError::Other(e.to_string())), + f @ ("pptx" | "potx" | "ppsx") => deckcraft_pptx::export_as(doc, f).map_err(|e| EngineError::Other(e.to_string())), "outline" | "txt" => Ok(deckcraft_format::slides_to_outline(doc).into_bytes()), "pdf" => deckcraft_pdf::export(doc, &Default::default()).map_err(|e| EngineError::Other(e.to_string())), _ => deckcraft_format::save(doc).map_err(|e| EngineError::Other(e.to_string())), @@ -207,8 +207,12 @@ pub fn writable_format_for_path(path: &str) -> Option<&'static str> { pub fn format_for_path(path: &str) -> &'static str { let l = path.to_ascii_lowercase(); - if l.ends_with(".pptx") || l.ends_with(".potx") || l.ends_with(".ppsx") { + if l.ends_with(".pptx") { "pptx" + } else if l.ends_with(".potx") { + "potx" + } else if l.ends_with(".ppsx") { + "ppsx" } else if l.ends_with(".png") { "png" } else if l.ends_with(".jpg") || l.ends_with(".jpeg") { diff --git a/crates/pptx/src/lib.rs b/crates/pptx/src/lib.rs index b15b2aa6..f0609f6c 100644 --- a/crates/pptx/src/lib.rs +++ b/crates/pptx/src/lib.rs @@ -60,7 +60,18 @@ pub fn import(bytes: &[u8]) -> Result { /// Write a `.pptx` package. pub fn export(p: &Presentation) -> Result, PptxError> { - write::export(p) + write::export(p, write::CT_PRES) +} + +/// Write a package for `ext`: `potx` (template) and `ppsx` (slide show) differ from `pptx` only in +/// the main part's content type. +pub fn export_as(p: &Presentation, ext: &str) -> Result, PptxError> { + let ct = match ext { + "potx" => write::CT_TEMPLATE, + "ppsx" => write::CT_SHOW, + _ => write::CT_PRES, + }; + write::export(p, ct) } /// Content type for a file name's extension. diff --git a/crates/pptx/src/write/mod.rs b/crates/pptx/src/write/mod.rs index 7b8acac5..ddb77d86 100644 --- a/crates/pptx/src/write/mod.rs +++ b/crates/pptx/src/write/mod.rs @@ -19,7 +19,9 @@ use crate::PptxError; use crate::opc::{self, NS_A, NS_P, NS_P14, NS_P188, NS_R, PackageOut, RelsOut}; use crate::xml::{A, W}; -const CT_PRES: &str = "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"; +pub(crate) const CT_PRES: &str = "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"; +pub(crate) const CT_TEMPLATE: &str = "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml"; +pub(crate) const CT_SHOW: &str = "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml"; const CT_SLIDE: &str = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml"; const CT_LAYOUT: &str = "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"; const CT_MASTER: &str = "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"; @@ -418,7 +420,7 @@ fn sanitized(p: &Presentation) -> std::borrow::Cow<'_, Presentation> { std::borrow::Cow::Owned(q) } -pub fn export(p: &Presentation) -> Result, PptxError> { +pub fn export(p: &Presentation, main_ct: &str) -> Result, PptxError> { let p = sanitized(p); let p: &Presentation = &p; let mut x = Exp { @@ -575,7 +577,7 @@ pub fn export(p: &Presentation) -> Result, PptxError> { let ts = crate::tables::table_styles_xml(&x.table_styles, &p.table_styles); x.pkg.xml_part("ppt/tableStyles.xml", CT_TABLE_STYLES, ts); let pxml = presentation_xml(&mut x, &mut pres, &master_rids, notes_master_rid.as_deref(), &slide_rids); - x.pkg.xml_part("ppt/presentation.xml", CT_PRES, pxml); + x.pkg.xml_part("ppt/presentation.xml", main_ct, pxml); x.pkg.rels("ppt/presentation.xml", &pres.rels); // Document properties and package relationships. diff --git a/crates/pptx/tests/roundtrip.rs b/crates/pptx/tests/roundtrip.rs index 12929cb3..de2436a9 100644 --- a/crates/pptx/tests/roundtrip.rs +++ b/crates/pptx/tests/roundtrip.rs @@ -1036,3 +1036,16 @@ fn linked_pictures_are_written_with_r_link() { std::io::Read::read_to_string(&mut zip.by_name("ppt/slides/_rels/slide1.xml.rels").expect("rels"), &mut rels).expect("read"); assert!(rels.contains(r#"Target="file:///tmp/linked.png" TargetMode="External""#), "{rels}"); } + +/// #210: `.potx` and `.ppsx` were written with the `.pptx` main content type. +#[test] +fn templates_and_slide_shows_declare_their_own_main_content_type() { + let p = rich_deck(); + for (ext, kind) in [("pptx", "presentation"), ("potx", "template"), ("ppsx", "slideshow")] { + let bytes = deckcraft_pptx::export_as(&p, ext).expect("export"); + let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).expect("zip"); + let ct = std::io::read_to_string(z.by_name("[Content_Types].xml").expect("content types")).expect("read"); + assert!(ct.contains(&format!("presentationml.{kind}.main+xml")), "{ext}: {ct}"); + assert_eq!(deckcraft_pptx::import(&bytes).expect("import").slides.len(), p.slides.len(), "{ext} reads back"); + } +}