From 21249df5da70920bd9d55e1ec9e91354d27b5251 Mon Sep 17 00:00:00 2001 From: justinjohn0306 Date: Mon, 12 Oct 2026 17:21:58 +0530 Subject: [PATCH] PPTX: .potx and .ppsx declare a template and slide show main part (#210) Saving with a .potx or .ppsx name wrote a byte-identical .pptx: the main part was always presentationml.presentation.main+xml, because format_for_path folded all three extensions into "pptx". It now keeps potx/ppsx, and deckcraft_pptx::export_as picks the template or slideshow content type. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/file.rs | 10 +++++++--- crates/pptx/src/lib.rs | 13 ++++++++++++- crates/pptx/src/write/mod.rs | 8 +++++--- crates/pptx/tests/roundtrip.rs | 13 +++++++++++++ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 55aac8bd..96218056 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -104,10 +104,10 @@ fn open_bytes(s: &mut Session, p: &Value) -> Result { Ok(json!({"document": i})) } -/// 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())), @@ -138,8 +138,12 @@ pub fn pdf_bytes(doc: &Presentation, p: &Value) -> Result> { 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 92274b62..2ef0e33d 100644 --- a/crates/pptx/src/lib.rs +++ b/crates/pptx/src/lib.rs @@ -51,7 +51,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 5d15246d..6b053d2d 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_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"; @@ -360,7 +362,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 { @@ -500,7 +502,7 @@ pub fn export(p: &Presentation) -> Result, PptxError> { let ts = crate::tables::table_styles_xml(&x.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 420a3921..58e3f52c 100644 --- a/crates/pptx/tests/roundtrip.rs +++ b/crates/pptx/tests/roundtrip.rs @@ -730,3 +730,16 @@ fn date_fields_keep_their_type_and_saved_text() { assert_eq!(by_name(&q.slides[0], name).text, by_name(&p.slides[0], name).text, "{name}"); } } + +/// #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"); + } +}