Skip to main content

ppt_rs/generator/
package_xml.rs

1//! Package-level XML generation (content types, relationships, presentation)
2
3use crate::core::{append_usize, escape_xml};
4use crate::generator::slide_content::presentation_settings::PresentationSettings;
5use crate::generator::slide_content::embedded_fonts::EmbeddedFontList;
6use crate::generator::charts::chart_embedding_filename;
7use crate::generator::layout_parts::append_layout_content_type_overrides;
8use crate::generator::layout_parts::STANDARD_LAYOUT_COUNT;
9use crate::generator::theme_xml::layout_rel_target;
10
11/// First slide master id (`0x80000000`). Layout and notes master ids follow in the same id space.
12pub const SLIDE_MASTER_ID: u32 = 2_147_483_648;
13/// Id for the first (blank) slide layout on slideMaster1.
14pub const SLIDE_LAYOUT_ID: u32 = SLIDE_MASTER_ID + 1;
15/// Id for notesMaster1 — after all slide layout ids on slideMaster1.
16pub const NOTES_MASTER_ID: u32 = SLIDE_LAYOUT_ID + STANDARD_LAYOUT_COUNT as u32;
17/// Id for handoutMaster1.
18pub const HANDOUT_MASTER_ID: u32 = NOTES_MASTER_ID + 1;
19
20const DEFAULT_TEXT_STYLE: &str = r#"<p:defaultTextStyle><a:defPPr><a:defRPr lang="en-US"/></a:defPPr><a:lvl1pPr marL="0" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl1pPr><a:lvl2pPr marL="457200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl2pPr><a:lvl3pPr marL="914400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl3pPr><a:lvl4pPr marL="1371600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl4pPr><a:lvl5pPr marL="1828800" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl5pPr><a:lvl6pPr marL="2286000" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl6pPr><a:lvl7pPr marL="2743200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl7pPr><a:lvl8pPr marL="3200400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl8pPr><a:lvl9pPr marL="3657600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl9pPr></p:defaultTextStyle>"#;
21
22const CONTENT_TYPES_HEADER: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
23<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
24<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
25<Default Extension="xml" ContentType="application/xml"/>
26<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>"#;
27
28fn append_image_content_type_defaults(xml: &mut String, media_exts: &[String]) {
29    let mut has_jpg = false;
30    let mut has_png = false;
31    let mut has_gif = false;
32    for ext in media_exts {
33        match ext.as_str() {
34            "jpg" | "jpeg" => has_jpg = true,
35            "png" => has_png = true,
36            "gif" => has_gif = true,
37            _ => {}
38        }
39    }
40    if has_jpg {
41        xml.push_str(r#"<Default Extension="jpg" ContentType="image/jpeg"/>"#);
42    }
43    if has_png {
44        xml.push_str(r#"<Default Extension="png" ContentType="image/png"/>"#);
45    }
46    if has_gif {
47        xml.push_str(r#"<Default Extension="gif" ContentType="image/gif"/>"#);
48    }
49}
50
51pub fn content_types_opening(media_exts: &[String], chart_count: usize) -> String {
52    let mut xml = String::from(
53        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
54<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">"#,
55    );
56    append_image_content_type_defaults(&mut xml, media_exts);
57    xml.push_str(
58        r#"<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
59<Default Extension="xml" ContentType="application/xml"/>"#,
60    );
61    if chart_count > 0 {
62        xml.push_str(
63            r#"<Default Extension="xlsx" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"/>"#,
64        );
65    }
66    xml.push_str(
67        r#"<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>"#,
68    );
69    xml
70}
71
72const CONTENT_TYPES_FOOTER: &str = r#"
73<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
74<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
75<Override PartName="/ppt/tableStyles.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml"/>
76<Override PartName="/ppt/viewProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml"/>
77<Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/>
78<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
79<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
80</Types>"#;
81
82const SLIDE_OVERRIDE_PREFIX: &str =
83    "\n<Override PartName=\"/ppt/slides/slide";
84const SLIDE_OVERRIDE_SUFFIX: &str =
85    ".xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.slide+xml\"/>";
86
87fn append_slide_overrides(xml: &mut String, slides: usize) {
88    for i in 1..=slides {
89        xml.push_str(SLIDE_OVERRIDE_PREFIX);
90        append_usize(xml, i);
91        xml.push_str(SLIDE_OVERRIDE_SUFFIX);
92    }
93}
94
95/// Relationship id for slide master (always rId1).
96pub const SLIDE_MASTER_REL_ID: usize = 1;
97
98/// First slide relationship id in `presentation.xml.rels` (immediately after slide master).
99pub fn first_slide_rel_id(_has_notes: bool, _has_handout: bool) -> usize {
100    2
101}
102
103/// Relationship id for slide `slide_num` (1-based).
104pub fn slide_rel_id(slide_num: usize, _has_notes: bool, _has_handout: bool) -> usize {
105    slide_num + 1
106}
107
108/// Notes master relationship id (after all slides).
109pub fn notes_master_rel_id(slide_count: usize) -> usize {
110    slide_count + 2
111}
112
113/// Handout master relationship id (after slides and optional notes master).
114pub fn handout_master_rel_id(slide_count: usize, has_notes: bool) -> usize {
115    slide_count + 2 + usize::from(has_notes)
116}
117
118/// presProps relationship id (after optional masters).
119pub fn pres_props_rel_id(slide_count: usize, has_notes: bool, has_handout: bool) -> usize {
120    slide_count + 2 + usize::from(has_notes) + usize::from(has_handout)
121}
122
123/// viewProps relationship id.
124pub fn view_props_rel_id(slide_count: usize, has_notes: bool, has_handout: bool) -> usize {
125    pres_props_rel_id(slide_count, has_notes, has_handout) + 1
126}
127
128/// theme1 relationship id.
129pub fn theme_rel_id(slide_count: usize, has_notes: bool, has_handout: bool) -> usize {
130    pres_props_rel_id(slide_count, has_notes, has_handout) + 2
131}
132
133/// tableStyles relationship id.
134pub fn table_styles_rel_id(slide_count: usize, has_notes: bool, has_handout: bool) -> usize {
135    pres_props_rel_id(slide_count, has_notes, has_handout) + 3
136}
137
138/// Slide id value for slide `slide_num` (1-based). PowerPoint starts at 256.
139pub fn slide_id_value(slide_num: usize) -> usize {
140    255 + slide_num
141}
142
143/// Create `[Content_Types].xml`
144pub fn create_content_types_xml(slides: usize) -> String {
145    let mut xml = String::with_capacity(1024 + slides * 140);
146    xml.push_str(CONTENT_TYPES_HEADER);
147    append_slide_overrides(&mut xml, slides);
148    append_layout_content_type_overrides(&mut xml, STANDARD_LAYOUT_COUNT);
149    xml.push_str(CONTENT_TYPES_FOOTER);
150    xml
151}
152
153const PACKAGE_RELS_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
154<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
155<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
156<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
157<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
158</Relationships>"#;
159
160/// Create _rels/.rels
161pub fn create_rels_xml() -> &'static str {
162    PACKAGE_RELS_XML
163}
164
165/// Create _rels/.rels with an optional digital signature origin relationship.
166pub fn create_rels_xml_with_signature(has_signature: bool) -> String {
167    let mut xml = String::from(PACKAGE_RELS_XML);
168    if has_signature {
169        // Insert signature origin relationship before </Relationships>.
170        // The static XML contains rId1-rId3, so rId4 is the next free id.
171        if let Some(pos) = xml.rfind("</Relationships>") {
172            xml.insert_str(
173                pos,
174                r#"<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin" Target="_xmlsignatures/origin.sigs"/>"#,
175            );
176        }
177    }
178    xml
179}
180
181/// Create ppt/_rels/presentation.xml.rels
182pub fn create_presentation_rels_xml(slides: usize) -> String {
183    create_presentation_rels_xml_full(slides, false, false)
184}
185
186/// Create ppt/_rels/presentation.xml.rels with notes master
187pub fn create_presentation_rels_xml_with_notes(slides: usize) -> String {
188    create_presentation_rels_xml_full(slides, true, false)
189}
190
191/// Create ppt/_rels/presentation.xml.rels with optional notes/handout masters.
192pub fn create_presentation_rels_xml_full(
193    slides: usize,
194    has_notes: bool,
195    has_handout: bool,
196) -> String {
197    let mut xml = String::with_capacity(768 + slides * 120);
198    xml.push_str(
199        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
200<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
201<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>"#,
202    );
203
204    for i in 1..=slides {
205        let rid = slide_rel_id(i, has_notes, has_handout);
206        xml.push_str("\n<Relationship Id=\"rId");
207        append_usize(&mut xml, rid);
208        xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide\" Target=\"slides/slide");
209        append_usize(&mut xml, i);
210        xml.push_str(".xml\"/>");
211    }
212
213    if has_notes {
214        xml.push_str("\n<Relationship Id=\"rId");
215        append_usize(&mut xml, notes_master_rel_id(slides));
216        xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster\" Target=\"notesMasters/notesMaster1.xml\"/>");
217    }
218
219    if has_handout {
220        xml.push_str("\n<Relationship Id=\"rId");
221        append_usize(&mut xml, handout_master_rel_id(slides, has_notes));
222        xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster\" Target=\"handoutMasters/handoutMaster1.xml\"/>");
223    }
224
225    xml.push_str("\n<Relationship Id=\"rId");
226    append_usize(&mut xml, pres_props_rel_id(slides, has_notes, has_handout));
227    xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps\" Target=\"presProps.xml\"/>");
228
229    xml.push_str("\n<Relationship Id=\"rId");
230    append_usize(&mut xml, view_props_rel_id(slides, has_notes, has_handout));
231    xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps\" Target=\"viewProps.xml\"/>");
232
233    xml.push_str("\n<Relationship Id=\"rId");
234    append_usize(&mut xml, theme_rel_id(slides, has_notes, has_handout));
235    xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme\" Target=\"theme/theme1.xml\"/>");
236
237    xml.push_str("\n<Relationship Id=\"rId");
238    append_usize(&mut xml, table_styles_rel_id(slides, has_notes, has_handout));
239    xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles\" Target=\"tableStyles.xml\"/>");
240
241    xml.push_str("\n</Relationships>");
242    xml
243}
244
245/// Create ppt/_rels/presentation.xml.rels including embedded font relationships.
246pub fn create_presentation_rels_xml_full_with_fonts(
247    slides: usize,
248    has_notes: bool,
249    has_handout: bool,
250    fonts: &EmbeddedFontList,
251) -> String {
252    let mut xml = create_presentation_rels_xml_full(slides, has_notes, has_handout);
253
254    for font in fonts.fonts() {
255        let target = font.rel_target();
256        xml.insert_str(
257            xml.rfind("</Relationships>").unwrap_or(xml.len()),
258            &format!(
259                r#"<Relationship Id="{}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/font" Target="{}"/>"#,
260                font.relationship_id,
261                escape_xml(&target),
262            ),
263        );
264    }
265
266    xml
267}
268
269/// Default table style list required by PowerPoint for presentations with tables.
270pub fn create_table_styles_xml() -> &'static str {
271    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
272<a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" def="{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"/>"#
273}
274
275/// Create ppt/presProps.xml with optional slide-show and handout print settings.
276pub fn create_pres_props_xml(settings: Option<&PresentationSettings>) -> String {
277    let mut xml = String::from(
278        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:presentationPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">"#,
279    );
280
281    if let Some(s) = settings {
282        if let Some(ref show) = s.slide_show {
283            xml.push_str(&show.to_xml());
284        }
285        // Handout print preferences are expressed via the handout master part.
286        // PowerPoint strips `<p:prnPr>` from presProps on repair when it is paired
287        // with a packaged handout master, so we do not emit it during generation.
288    }
289
290    xml.push_str(
291        r#"<p:extLst><p:ext uri="{E76CE94A-603C-4142-B9EB-6D1370010A27}"><p14:discardImageEditData xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="0"/></p:ext><p:ext uri="{D31A062A-798A-4329-ABDD-BBA856620510}"><p14:defaultImageDpi xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="0"/></p:ext><p:ext uri="{FD5EFAAD-0ECE-453E-9831-46B23BE46B34}"><p15:chartTrackingRefBased xmlns:p15="http://schemas.microsoft.com/office/powerpoint/2012/main" val="0"/></p:ext></p:extLst></p:presentationPr>"#,
292    );
293    xml
294}
295
296/// Default view properties part required by PowerPoint.
297pub fn create_view_props_xml() -> &'static str {
298    // All children of CT_ViewProperties are optional, but when present they
299    // must carry their own required descendants (e.g. normalViewPr needs
300    // restoredLeft + restoredTop; slideViewPr needs cSldViewPr). Emitting
301    // empty `<p:normalViewPr/>` / `<p:slideViewPr/>` violates the schema and
302    // triggers a PowerPoint repair prompt. `lastView` is omitted because
303    // `sldThumbnailView` is a non-standard enum extension with the same effect.
304    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
305<p:viewPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
306<p:normalViewPr><p:restoredLeft sz="15620"/><p:restoredTop sz="94660"/></p:normalViewPr>
307<p:slideViewPr><p:cSldViewPr><p:cViewPr varScale="1"><p:scale><a:sx n="64" d="100"/><a:sy n="64" d="100"/></p:scale><p:origin x="-1392" y="-96"/></p:cViewPr><p:guideLst/></p:cSldViewPr></p:slideViewPr>
308<p:notesTextViewPr><p:cViewPr><p:scale><a:sx n="1" d="1"/><a:sy n="1" d="1"/></p:scale><p:origin x="0" y="0"/></p:cViewPr></p:notesTextViewPr>
309<p:gridSpacing cx="72008" cy="72008"/>
310</p:viewPr>"#
311}
312
313/// Create ppt/presentation.xml
314pub fn create_presentation_xml(
315    _title: &str,
316    slides: usize,
317    has_notes: bool,
318    has_handout: bool,
319) -> String {
320    let mut xml = String::with_capacity(896 + slides * 48);
321    xml.push_str(
322        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
323<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" saveSubsetFonts="1" autoCompressPictures="0">
324<p:sldMasterIdLst>
325<p:sldMasterId id=""#,
326    );
327    append_usize(&mut xml, SLIDE_MASTER_ID as usize);
328    xml.push_str(r#"" r:id="rId1"/>
329</p:sldMasterIdLst>"#);
330
331    if has_notes {
332        xml.push_str("\n<p:notesMasterIdLst>\n<p:notesMasterId r:id=\"rId");
333        append_usize(&mut xml, notes_master_rel_id(slides));
334        xml.push_str("\"/>\n</p:notesMasterIdLst>");
335    }
336
337    if has_handout {
338        xml.push_str("\n<p:handoutMasterIdLst>\n<p:handoutMasterId r:id=\"rId");
339        append_usize(&mut xml, handout_master_rel_id(slides, has_notes));
340        xml.push_str("\"/>\n</p:handoutMasterIdLst>");
341    }
342
343    xml.push_str("\n<p:sldIdLst>");
344
345    for i in 1..=slides {
346        let id = slide_id_value(i);
347        let rid = slide_rel_id(i, has_notes, has_handout);
348        xml.push_str("\n<p:sldId id=\"");
349        append_usize(&mut xml, id);
350        xml.push_str("\" r:id=\"rId");
351        append_usize(&mut xml, rid);
352        xml.push_str("\"/>");
353    }
354
355    xml.push_str(
356        r#"
357</p:sldIdLst>
358<p:sldSz cx="9144000" cy="6858000" type="screen4x3"/>
359<p:notesSz cx="6858000" cy="9144000"/>
360"#,
361    );
362    xml.push_str(DEFAULT_TEXT_STYLE);
363    xml.push_str(
364        r#"<p:extLst><p:ext uri="{EFAFB233-063F-42B5-8137-9DF3F51BA10A}"><p15:sldGuideLst xmlns:p15="http://schemas.microsoft.com/office/powerpoint/2012/main"/></p:ext></p:extLst></p:presentation>"#,
365    );
366    xml
367}
368
369/// Create ppt/presentation.xml including an embedded font list.
370pub fn create_presentation_xml_with_fonts(
371    title: &str,
372    slides: usize,
373    has_notes: bool,
374    has_handout: bool,
375    fonts: &EmbeddedFontList,
376) -> String {
377    let mut xml = create_presentation_xml(title, slides, has_notes, has_handout);
378    let font_xml = fonts.to_xml();
379    if !font_xml.is_empty() {
380        // Insert <p:embeddedFontLst> before the closing </p:presentation>.
381        if let Some(pos) = xml.rfind("</p:presentation>") {
382            xml.insert_str(pos, &font_xml);
383        }
384    }
385    xml
386}
387
388/// Create `[Content_Types].xml` with notes, charts, and optional handout master.
389pub fn create_content_types_xml_with_notes_and_charts(
390    slides: usize,
391    custom_slides: Option<&[super::slide_content::SlideContent]>,
392    chart_count: usize,
393    has_handout: bool,
394    media_exts: &[String],
395) -> String {
396    let notes_count = custom_slides
397        .map(|slides_slice| slides_slice.iter().filter(|slide| slide.notes.is_some()).count())
398        .unwrap_or(0);
399    let mut xml = String::with_capacity(2048 + slides * 140 + notes_count * 140 + chart_count * 120);
400    xml.push_str(&content_types_opening(media_exts, chart_count));
401    append_slide_overrides(&mut xml, slides);
402
403    if let Some(slides_slice) = custom_slides {
404        let mut notes_index = 0usize;
405        for slide in slides_slice {
406            if slide.notes.is_some() {
407                notes_index += 1;
408                xml.push_str("\n<Override PartName=\"/ppt/notesSlides/notesSlide");
409                append_usize(&mut xml, notes_index);
410                xml.push_str(".xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml\"/>");
411            }
412        }
413        if notes_count > 0 {
414            xml.push_str("\n<Override PartName=\"/ppt/notesMasters/notesMaster1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml\"/>");
415            xml.push_str("\n<Override PartName=\"/ppt/theme/theme2.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.theme+xml\"/>");
416        }
417    }
418
419    if has_handout {
420        xml.push_str("\n<Override PartName=\"/ppt/handoutMasters/handoutMaster1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml\"/>");
421        xml.push_str("\n<Override PartName=\"/ppt/theme/theme3.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.theme+xml\"/>");
422    }
423
424    for i in 1..=chart_count {
425        xml.push_str("\n<Override PartName=\"/ppt/charts/chart");
426        append_usize(&mut xml, i);
427        xml.push_str(".xml\" ContentType=\"application/vnd.openxmlformats-officedocument.drawingml.chart+xml\"/>");
428        xml.push_str("\n<Override PartName=\"/ppt/embeddings/");
429        xml.push_str(&chart_embedding_filename(i));
430        xml.push_str("\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"/>");
431    }
432
433    append_layout_content_type_overrides(&mut xml, STANDARD_LAYOUT_COUNT);
434    xml.push_str(CONTENT_TYPES_FOOTER);
435    xml
436}
437
438/// Append the digital signature content type entries to an existing
439/// `[Content_Types].xml` string (inserted before `</Types>`).
440pub fn append_digital_signature_content_type(xml: &mut String) {
441    if let Some(pos) = xml.rfind("</Types>") {
442        xml.insert_str(
443            pos,
444            r#"<Default Extension="sigs" ContentType="application/vnd.openxmlformats-package.digital-signature-origin"/><Override PartName="/_xmlsignatures/sig1.xml" ContentType="application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"/>"#,
445        );
446    }
447}
448
449/// Append the embedded font content type default (`.fntdata`) to an existing
450/// `[Content_Types].xml` string (inserted before `</Types>`).
451pub fn append_embedded_font_content_type(xml: &mut String) {
452    if let Some(pos) = xml.rfind("</Types>") {
453        xml.insert_str(
454            pos,
455            r#"<Default Extension="fntdata" ContentType="application/x-fontdata"/>"#,
456        );
457    }
458}
459
460/// Append ink annotation content type overrides to an existing
461/// `[Content_Types].xml` string (inserted before `</Types>`).
462pub fn append_ink_content_types(xml: &mut String, ink_count: usize) {
463    if ink_count == 0 {
464        return;
465    }
466    if let Some(pos) = xml.rfind("</Types>") {
467        let mut overrides = String::with_capacity(ink_count * 140);
468        for i in 1..=ink_count {
469            overrides.push_str(&format!(
470                "\n<Override PartName=\"/ppt/ink/ink{i}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.ink+xml\"/>"
471            ));
472        }
473        xml.insert_str(pos, &overrides);
474    }
475}
476
477/// Handout master relationship XML (theme link).
478pub fn create_handout_master_rels_xml() -> String {
479    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
480<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
481<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme3.xml"/>
482</Relationships>"#.to_string()
483}
484
485/// Create slide relationship XML with notes and charts
486pub fn create_slide_rels_xml_extended(
487    layout_number: usize,
488    has_notes: bool,
489    notes_part_num: usize,
490    chart_rels: &[(String, String)],
491) -> String {
492    let layout_target = layout_rel_target(layout_number);
493    let mut xml = String::with_capacity(512 + chart_rels.len() * 96);
494    xml.push_str(
495        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
496<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
497<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target=""#,
498    );
499    xml.push_str(&layout_target);
500    xml.push_str("\"/>");
501
502    if has_notes {
503        xml.push_str("\n<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide\" Target=\"../notesSlides/notesSlide");
504        append_usize(&mut xml, notes_part_num);
505        xml.push_str(".xml\"/>");
506    }
507
508    for (rid, target) in chart_rels {
509        xml.push_str("\n<Relationship Id=\"");
510        xml.push_str(rid);
511        xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart\" Target=\"");
512        xml.push_str(target);
513        xml.push_str("\"/>");
514    }
515
516    xml.push_str("\n</Relationships>");
517    xml
518}
519
520/// Create slide relationship XML with notes, charts, images, and optional ink.
521pub fn create_slide_rels_xml_with_images(
522    layout_number: usize,
523    has_notes: bool,
524    notes_part_num: usize,
525    chart_rels: &[(String, String)],
526    images: &[(usize, String)],
527    hyperlink_rels: &[String],
528    ink_rel: Option<(usize, usize)>,
529) -> String {
530    let layout_target = layout_rel_target(layout_number);
531    let mut xml = String::with_capacity(
532        512 + images.len() * 96 + chart_rels.len() * 96 + hyperlink_rels.len() * 160 + 128,
533    );
534    xml.push_str(
535        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
536<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
537<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target=""#,
538    );
539    xml.push_str(&layout_target);
540    xml.push_str("\"/>");
541
542    let mut next_rid = 2usize;
543    if has_notes {
544        xml.push_str("\n<Relationship Id=\"rId");
545        append_usize(&mut xml, next_rid);
546        xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide\" Target=\"../notesSlides/notesSlide");
547        append_usize(&mut xml, notes_part_num);
548        xml.push_str(".xml\"/>");
549        next_rid += 1;
550    }
551
552    for (i, (image_num, ext)) in images.iter().enumerate() {
553        xml.push_str("\n<Relationship Id=\"rId");
554        append_usize(&mut xml, next_rid + i);
555        xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"../media/image");
556        append_usize(&mut xml, *image_num);
557        xml.push_str(".");
558        xml.push_str(ext);
559        xml.push_str("\"/>");
560    }
561
562    for (rid, target) in chart_rels {
563        xml.push_str("\n<Relationship Id=\"");
564        xml.push_str(rid);
565        xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart\" Target=\"");
566        xml.push_str(target);
567        xml.push_str("\"/>");
568    }
569
570    if let Some((rid, ink_num)) = ink_rel {
571        xml.push_str("\n<Relationship Id=\"rId");
572        append_usize(&mut xml, rid);
573        xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/ink\" Target=\"../ink/ink");
574        append_usize(&mut xml, ink_num);
575        xml.push_str(".xml\"/>");
576    }
577
578    for rel in hyperlink_rels {
579        xml.push('\n');
580        xml.push_str(rel);
581    }
582
583    xml.push_str("\n</Relationships>");
584    xml
585}