Skip to main content

ppt_rs/generator/
builder.rs

1//! PPTX builder - orchestrates ZIP creation and file writing
2
3use std::io::{Write, Seek, Cursor};
4use zip::write::FileOptions;
5use zip::ZipWriter;
6use crate::exc::Result;
7use crate::core::append_usize;
8use super::slide_content::SlideContent;
9use super::memory_profile::estimate_output_capacity;
10use super::package_cache::{self, print_affects_theme_parts};
11use super::package_xml::{
12    create_rels_xml_with_signature,
13    create_presentation_rels_xml_full, create_presentation_rels_xml_full_with_fonts,
14    create_presentation_xml, create_presentation_xml_with_fonts,
15    create_content_types_xml_with_notes_and_charts,
16    content_types_opening, append_digital_signature_content_type,
17    append_embedded_font_content_type, table_styles_rel_id,
18    create_pres_props_xml, create_view_props_xml, create_table_styles_xml,
19    create_handout_master_rels_xml,
20};
21use super::slide_xml::{
22    create_slide_xml, create_slide_xml_with_content, create_slide_rels_xml,
23};
24use super::theme_xml::{
25    create_slide_master_xml, create_master_rels_xml, create_theme_xml, create_layout_rels_xml,
26};
27use super::layout_parts::{create_slide_layout_xml, STANDARD_LAYOUT_COUNT};
28use super::template::PptxTemplate;
29use super::props_xml::{create_core_props_xml, create_app_props_xml};
30use super::notes_xml::*;
31use crate::generator::presentation_theme::office_theme_xml;
32use crate::generator::charts::{
33    chart_embedding_filename, create_chart_rels_xml, generate_chart_part_xml,
34    reference_workbook_bytes,
35};
36use crate::generator::slide_content::print_settings::PrintWhat;
37use crate::generator::slide_content::presentation_settings::PresentationSettings;
38use super::media_registry::MediaRegistry;
39
40fn zip_options() -> FileOptions {
41    FileOptions::default()
42}
43
44/// First relationship id after layout (rId1) and optional notes slide.
45fn slide_content_rel_start(has_notes: bool, image_count: usize) -> usize {
46    2 + usize::from(has_notes) + image_count
47}
48
49fn build_media_registry(slides: &[SlideContent]) -> MediaRegistry {
50    let mut registry = MediaRegistry::default();
51    for slide in slides {
52        for image in &slide.images {
53            if let Some(bytes) = image.get_bytes() {
54                registry.image_number(&bytes, &image.extension());
55            }
56        }
57    }
58    registry
59}
60
61fn build_media_registry_lazy(slides: &dyn LazySlideSource) -> MediaRegistry {
62    let mut registry = MediaRegistry::default();
63    for i in 0..slides.slide_count() {
64        if let Some(slide) = slides.generate_slide(i) {
65            for image in &slide.images {
66                if let Some(bytes) = image.get_bytes() {
67                    registry.image_number(&bytes, &image.extension());
68                }
69            }
70        }
71    }
72    registry
73}
74
75fn slide_image_rel_targets(slide: &SlideContent, registry: &MediaRegistry) -> Vec<(usize, String)> {
76    let mut images = Vec::with_capacity(slide.images.len());
77    for image in &slide.images {
78        if let Some(bytes) = image.get_bytes() {
79            if let Some(num) = registry.lookup_number(&bytes, &image.extension()) {
80                images.push((num, image.extension()));
81            }
82        }
83    }
84    images
85}
86
87/// Collect hyperlink relationship XML (`<Relationship .../>`) for every shape on
88/// the slide whose hyperlink has an assigned relationship id. The slide XML
89/// references these ids via `<a:hlinkClick r:id="..."/>`, so the matching
90/// relationship must be present in the slide's `.rels` part.
91fn slide_hyperlink_relationships(slide: &SlideContent) -> Vec<String> {
92    slide
93        .shapes
94        .iter()
95        .filter_map(|s| s.hyperlink.as_ref())
96        .filter_map(|h| {
97            h.r_id
98                .as_ref()
99                .map(|rid| crate::generator::generate_hyperlink_relationship_xml(h, rid))
100        })
101        .collect()
102}
103
104fn set_notes_part_path(path: &mut String, notes_part_num: usize) {
105    path.clear();
106    path.push_str("ppt/notesSlides/notesSlide");
107    append_usize(path, notes_part_num);
108    path.push_str(".xml");
109}
110
111#[cfg(debug_assertions)]
112fn debug_assert_package_valid(bytes: &[u8]) {
113    use crate::core::validate_package_bytes;
114    let report = validate_package_bytes(bytes);
115    debug_assert!(
116        report.is_valid(),
117        "generated PPTX failed package validation: {:?}",
118        report.error_messages()
119    );
120}
121
122#[cfg(not(debug_assertions))]
123fn debug_assert_package_valid(_bytes: &[u8]) {}
124
125/// Create a minimal but valid PPTX file
126pub fn create_pptx(title: &str, slides: usize) -> Result<Vec<u8>> {
127    let buffer = Vec::with_capacity(slides.saturating_mul(6_000).max(8_192));
128    let cursor = Cursor::new(buffer);
129    let mut zip = ZipWriter::new(cursor);
130    let options = zip_options();
131
132    write_package_files(&mut zip, &options, title, slides, None, None)?;
133
134    let cursor = zip.finish()?;
135    let bytes = cursor.into_inner();
136    debug_assert_package_valid(&bytes);
137    Ok(bytes)
138}
139
140/// Create a PPTX file with custom slide content
141pub fn create_pptx_with_content(
142    title: &str,
143    slides: Vec<SlideContent>,
144) -> Result<Vec<u8>> {
145    create_pptx_with_settings(title, &slides, None)
146}
147
148/// Create a PPTX file with custom slide content, settings, and optional template deck.
149pub fn create_pptx_with_template(
150    title: &str,
151    slides: &[SlideContent],
152    template_path: &str,
153    settings: Option<PresentationSettings>,
154) -> Result<Vec<u8>> {
155    let mut merged = settings.unwrap_or_default();
156    merged.template_path = Some(template_path.to_string());
157    create_pptx_with_settings(title, slides, Some(merged))
158}
159
160/// Resolve layout part index for a slide (respects template layout count).
161fn resolve_layout_number(slide: &SlideContent, template: Option<&PptxTemplate>) -> usize {
162    let requested = slide.layout.layout_number();
163    template
164        .map(|t| t.resolve_layout_number(requested))
165        .unwrap_or(requested)
166}
167
168fn load_template(settings: Option<&PresentationSettings>) -> Result<Option<PptxTemplate>> {
169    if let Some(path) = settings.and_then(|s| s.template_path.as_deref()) {
170        Ok(Some(PptxTemplate::load(path)?))
171    } else {
172        Ok(None)
173    }
174}
175
176/// Create a PPTX file with custom slide content and presentation-level settings
177pub fn create_pptx_with_settings(
178    title: &str,
179    slides: &[SlideContent],
180    settings: Option<PresentationSettings>,
181) -> Result<Vec<u8>> {
182    let buffer = Vec::with_capacity(estimate_output_capacity(slides.len(), Some(slides)));
183    let cursor = Cursor::new(buffer);
184    let mut zip = ZipWriter::new(cursor);
185    let options = zip_options();
186
187    write_package_files(&mut zip, &options, title, slides.len(), Some(slides), settings)?;
188
189    let cursor = zip.finish()?;
190    let bytes = cursor.into_inner();
191    debug_assert_package_valid(&bytes);
192    Ok(bytes)
193}
194
195/// Create a PPTX file and write it directly to a writer (streaming API).
196/// This is more memory-efficient for large presentations as it avoids
197/// buffering the entire ZIP file in memory.
198///
199/// # Example
200/// ```rust,no_run
201/// use std::fs::File;
202/// use ppt_rs::create_pptx_to_writer;
203///
204/// let file = File::create("output.pptx")?;
205/// create_pptx_to_writer(file, "My Presentation", 10)?;
206/// # Ok::<(), ppt_rs::PptxError>(())
207/// ```
208pub fn create_pptx_to_writer<W: Write + Seek>(
209    writer: W,
210    title: &str,
211    slides: usize,
212) -> Result<W> {
213    let mut zip = ZipWriter::new(writer);
214    let options = FileOptions::default();
215
216    write_package_files(&mut zip, &options, title, slides, None, None)?;
217
218    Ok(zip.finish()?)
219}
220
221/// Create a PPTX file with custom content and write it directly to a writer (streaming API).
222/// This is more memory-efficient for large presentations.
223///
224/// # Example
225/// ```rust,no_run
226/// use std::fs::File;
227/// use ppt_rs::{create_pptx_with_content_to_writer, SlideContent};
228///
229/// let file = File::create("output.pptx")?;
230/// let slides = vec![
231///     SlideContent::new("Title").add_bullet("Point 1"),
232///     SlideContent::new("Slide 2").add_bullet("Point 2"),
233/// ];
234/// create_pptx_with_content_to_writer(file, "My Presentation", &slides, None)?;
235/// # Ok::<(), ppt_rs::PptxError>(())
236/// ```
237pub fn create_pptx_with_content_to_writer<W: Write + Seek>(
238    writer: W,
239    title: &str,
240    slides: &[SlideContent],
241    settings: Option<PresentationSettings>,
242) -> Result<W> {
243    let mut zip = ZipWriter::new(writer);
244    let options = FileOptions::default();
245
246    write_package_files(&mut zip, &options, title, slides.len(), Some(slides), settings)?;
247
248    Ok(zip.finish()?)
249}
250
251/// Lazy slide source - allows generating slides on-demand instead of all at once.
252/// This is useful for:
253/// - Very large presentations that don't fit in memory
254/// - Dynamically generated slide content
255/// - Streaming data sources
256///
257/// # Example
258/// ```rust,no_run
259/// use std::fs::File;
260/// use ppt_rs::{create_pptx_lazy_to_writer, LazySlideSource, SlideContent};
261///
262/// struct MySlideGenerator {
263///     count: usize,
264/// }
265///
266/// impl LazySlideSource for MySlideGenerator {
267///     fn slide_count(&self) -> usize {
268///         self.count
269///     }
270///
271///     fn generate_slide(&self, index: usize) -> Option<SlideContent> {
272///         if index < self.count {
273///             Some(SlideContent::new(&format!("Slide {}", index + 1))
274///                 .add_bullet(&format!("Content {}", index + 1)))
275///         } else {
276///             None
277///         }
278///     }
279/// }
280///
281/// let file = File::create("output.pptx")?;
282/// create_pptx_lazy_to_writer(file, "My Presentation", Box::new(MySlideGenerator { count: 100 }), None)?;
283/// # Ok::<(), ppt_rs::PptxError>(())
284/// ```
285pub trait LazySlideSource {
286    /// Return the total number of slides
287    fn slide_count(&self) -> usize;
288
289    /// Generate a slide by index (0-based). Return None if index is out of bounds.
290    fn generate_slide(&self, index: usize) -> Option<SlideContent>;
291
292    /// Notes and chart count from a single slide generation when both are needed.
293    fn slide_features(&self, index: usize) -> Option<(bool, usize)> {
294        self.generate_slide(index)
295            .map(|s| (s.notes.is_some(), s.charts.len()))
296    }
297
298    /// Check if a slide has notes (default implementation checks the generated slide)
299    fn slide_has_notes(&self, index: usize) -> bool {
300        self.slide_features(index)
301            .map(|(notes, _)| notes)
302            .unwrap_or(false)
303    }
304
305    /// Get the number of charts in a slide (default implementation checks the generated slide)
306    fn slide_chart_count(&self, index: usize) -> usize {
307        self.slide_features(index)
308            .map(|(_, charts)| charts)
309            .unwrap_or(0)
310    }
311}
312
313/// Create a PPTX file using lazy slide generation and write it directly to a writer.
314/// This is the most memory-efficient API for large presentations.
315///
316/// # Example
317/// ```rust,no_run
318/// use std::fs::File;
319/// use ppt_rs::{create_pptx_lazy_to_writer, LazySlideSource, SlideContent};
320///
321/// struct MySlideGenerator {
322///     count: usize,
323/// }
324///
325/// impl LazySlideSource for MySlideGenerator {
326///     fn slide_count(&self) -> usize {
327///         self.count
328///     }
329///
330///     fn generate_slide(&self, index: usize) -> Option<SlideContent> {
331///         if index < self.count {
332///             Some(SlideContent::new(&format!("Slide {}", index + 1))
333///                 .add_bullet(&format!("Content {}", index + 1)))
334///         } else {
335///             None
336///         }
337///     }
338/// }
339///
340/// let file = File::create("output.pptx")?;
341/// let generator = Box::new(MySlideGenerator { count: 1000 });
342/// create_pptx_lazy_to_writer(file, "Large Presentation", generator, None)?;
343/// # Ok::<(), ppt_rs::PptxError>(())
344/// ```
345pub fn create_pptx_lazy_to_writer<W: Write + Seek>(
346    writer: W,
347    title: &str,
348    slides: Box<dyn LazySlideSource>,
349    settings: Option<PresentationSettings>,
350) -> Result<W> {
351    let mut zip = ZipWriter::new(writer);
352    let options = FileOptions::default();
353
354    write_package_files_lazy(&mut zip, &options, title, slides.as_ref(), settings)?;
355
356    Ok(zip.finish()?)
357}
358
359/// Chart metadata for slides
360struct ChartInfo {
361    total_charts: usize,
362    slide_start_indices: Vec<usize>,
363}
364
365fn set_slide_xml_path(path: &mut String, slide_num: usize) {
366    path.clear();
367    path.push_str("ppt/slides/slide");
368    append_usize(path, slide_num);
369    path.push_str(".xml");
370}
371
372fn set_slide_rels_path(path: &mut String, slide_num: usize) {
373    path.clear();
374    path.push_str("ppt/slides/_rels/slide");
375    append_usize(path, slide_num);
376    path.push_str(".xml.rels");
377}
378
379fn push_chart_rid(rids: &mut Vec<String>, rel_num: usize) {
380    let mut rid = String::with_capacity(8);
381    rid.push_str("rId");
382    append_usize(&mut rid, rel_num);
383    rids.push(rid);
384}
385
386/// Collect chart metadata from slides (eager version)
387fn collect_chart_info(slides: Option<&[SlideContent]>) -> ChartInfo {
388    let mut total_charts = 0;
389    let mut slide_start_indices = Vec::new();
390
391    if let Some(slides) = slides {
392        slide_start_indices.reserve(slides.len());
393        for slide in slides {
394            slide_start_indices.push(total_charts + 1);
395            total_charts += slide.charts.len();
396        }
397    }
398
399    ChartInfo {
400        total_charts,
401        slide_start_indices,
402    }
403}
404
405/// Collect chart metadata from lazy slide source
406fn collect_chart_info_lazy(slides: &dyn LazySlideSource) -> ChartInfo {
407    let mut total_charts = 0;
408    let mut slide_start_indices = Vec::with_capacity(slides.slide_count());
409
410    for i in 0..slides.slide_count() {
411        slide_start_indices.push(total_charts + 1);
412        total_charts += slides
413            .slide_features(i)
414            .map(|(_, chart_count)| chart_count)
415            .unwrap_or(0);
416    }
417
418    ChartInfo {
419        total_charts,
420        slide_start_indices,
421    }
422}
423
424/// Whether print settings request handout master packaging.
425fn uses_handouts(settings: Option<&PresentationSettings>) -> bool {
426    settings
427        .and_then(|s| s.print.as_ref())
428        .map(|p| p.print_what == PrintWhat::Handouts)
429        .unwrap_or(false)
430}
431
432/// Whether settings configure a digital signature package.
433fn has_digital_signature(settings: Option<&PresentationSettings>) -> bool {
434    settings
435        .and_then(|s| s.digital_signature.as_ref())
436        .is_some()
437}
438
439/// Whether settings configure embedded fonts.
440fn has_embedded_fonts(settings: Option<&PresentationSettings>) -> bool {
441    settings
442        .and_then(|s| s.embedded_fonts.as_ref())
443        .map(|f| !f.is_empty())
444        .unwrap_or(false)
445}
446
447/// Borrow embedded fonts from settings, if any.
448fn embedded_fonts(settings: Option<&PresentationSettings>) -> Option<&super::slide_content::embedded_fonts::EmbeddedFontList> {
449    settings.and_then(|s| s.embedded_fonts.as_ref())
450}
451
452/// Prepare settings by assigning relationship IDs to embedded fonts.
453/// Must be called after `has_notes` and `has_handout` are known.
454fn prepare_settings(settings: &mut Option<PresentationSettings>, slide_count: usize, has_notes: bool, has_handout: bool) {
455    if let Some(s) = settings {
456        if let Some(fonts) = s.embedded_fonts.as_mut() {
457            let first_rid = table_styles_rel_id(slide_count, has_notes, has_handout) + 1;
458            fonts.assign_relationship_ids(first_rid);
459        }
460    }
461}
462
463/// Collect slide titles for `docProps/app.xml` (eager version).
464///
465/// Returns one title per slide, falling back to "Slide N" placeholders when no
466/// custom slide content is available or a slide has an empty title.
467fn collect_slide_titles(custom_slides: Option<&[SlideContent]>, slide_count: usize) -> Vec<String> {
468    match custom_slides {
469        Some(slides) => slides
470            .iter()
471            .enumerate()
472            .map(|(i, s)| {
473                if s.title.trim().is_empty() {
474                    format!("Slide {}", i + 1)
475                } else {
476                    s.title.clone()
477                }
478            })
479            .collect(),
480        None => (0..slide_count)
481            .map(|i| format!("Slide {}", i + 1))
482            .collect(),
483    }
484}
485
486/// Collect slide titles for `docProps/app.xml` (lazy version).
487fn collect_slide_titles_lazy(slides: &dyn LazySlideSource, slide_count: usize) -> Vec<String> {
488    let mut titles = Vec::with_capacity(slide_count);
489    for i in 0..slide_count {
490        let title = slides
491            .generate_slide(i)
492            .map(|s| {
493                if s.title.trim().is_empty() {
494                    format!("Slide {}", i + 1)
495                } else {
496                    s.title
497                }
498            })
499            .unwrap_or_else(|| format!("Slide {}", i + 1));
500        titles.push(title);
501    }
502    titles
503}
504
505/// Write content types XML
506fn write_content_types<W: Write + Seek>(
507    zip: &mut ZipWriter<W>,
508    options: &FileOptions,
509    slide_count: usize,
510    custom_slides: Option<&[SlideContent]>,
511    chart_info: &ChartInfo,
512    has_handout: bool,
513    settings: Option<&PresentationSettings>,
514) -> Result<()> {
515    let media_exts = custom_slides
516        .map(build_media_registry)
517        .map(|registry| registry.extensions())
518        .unwrap_or_default();
519    let mut content_types = create_content_types_xml_with_notes_and_charts(
520        slide_count,
521        custom_slides,
522        chart_info.total_charts,
523        has_handout,
524        &media_exts,
525    );
526
527    if has_digital_signature(settings) {
528        append_digital_signature_content_type(&mut content_types);
529    }
530    if has_embedded_fonts(settings) {
531        append_embedded_font_content_type(&mut content_types);
532    }
533
534    let ink_count = custom_slides
535        .map(|slides| slides.iter().filter(|s| s.ink_annotations.is_some()).count())
536        .unwrap_or(0);
537    super::package_xml::append_ink_content_types(&mut content_types, ink_count);
538
539    zip.start_file("[Content_Types].xml", *options)?;
540    zip.write_all(content_types.as_bytes())?;
541    Ok(())
542}
543
544/// Write presentation relationships in PowerPoint order.
545fn write_presentation_relationships<W: Write + Seek>(
546    zip: &mut ZipWriter<W>,
547    options: &FileOptions,
548    slide_count: usize,
549    has_notes: bool,
550    has_handout: bool,
551    settings: Option<&PresentationSettings>,
552) -> Result<()> {
553    let pres_rels = if let Some(fonts) = embedded_fonts(settings) {
554        create_presentation_rels_xml_full_with_fonts(slide_count, has_notes, has_handout, fonts)
555    } else {
556        create_presentation_rels_xml_full(slide_count, has_notes, has_handout)
557    };
558
559    zip.start_file("ppt/_rels/presentation.xml.rels", *options)?;
560    zip.write_all(pres_rels.as_bytes())?;
561    Ok(())
562}
563
564/// Write presProps, viewProps, and tableStyles (always emitted).
565fn write_standard_package_parts<W: Write + Seek>(
566    zip: &mut ZipWriter<W>,
567    options: &FileOptions,
568    settings: Option<&PresentationSettings>,
569) -> Result<()> {
570    let pres_props = create_pres_props_xml(settings);
571    zip.start_file("ppt/presProps.xml", *options)?;
572    zip.write_all(pres_props.as_bytes())?;
573
574    let view_props = create_view_props_xml();
575    zip.start_file("ppt/viewProps.xml", *options)?;
576    zip.write_all(view_props.as_bytes())?;
577
578    let table_styles = create_table_styles_xml();
579    zip.start_file("ppt/tableStyles.xml", *options)?;
580    zip.write_all(table_styles.as_bytes())?;
581    Ok(())
582}
583
584/// Write handout master when print settings use handouts.
585fn write_handout_master<W: Write + Seek>(
586    zip: &mut ZipWriter<W>,
587    options: &FileOptions,
588    settings: Option<&PresentationSettings>,
589) -> Result<()> {
590    use crate::generator::slide_content::print_settings::PrintSettings;
591
592    let handout_xml = settings
593        .and_then(|s| s.print.as_ref())
594        .map(|p| p.to_handout_master_xml())
595        .unwrap_or_else(|| PrintSettings::default().to_handout_master_xml());
596
597    zip.start_file("ppt/handoutMasters/handoutMaster1.xml", *options)?;
598    zip.write_all(handout_xml.as_bytes())?;
599
600    zip.start_file("ppt/theme/theme3.xml", *options)?;
601    zip.write_all(office_theme_xml().as_bytes())?;
602
603    let rels = create_handout_master_rels_xml();
604    zip.start_file("ppt/handoutMasters/_rels/handoutMaster1.xml.rels", *options)?;
605    zip.write_all(rels.as_bytes())?;
606    Ok(())
607}
608
609/// Write notes master files if needed
610fn write_notes_master<W: Write + Seek>(
611    zip: &mut ZipWriter<W>,
612    options: &FileOptions,
613) -> Result<()> {
614    let notes_master = create_notes_master_xml();
615    zip.start_file("ppt/notesMasters/notesMaster1.xml", *options)?;
616    zip.write_all(notes_master.as_bytes())?;
617
618    zip.start_file("ppt/theme/theme2.xml", *options)?;
619    zip.write_all(office_theme_xml().as_bytes())?;
620
621    let notes_master_rels = create_notes_master_rels_xml();
622    zip.start_file("ppt/notesMasters/_rels/notesMaster1.xml.rels", *options)?;
623    zip.write_all(notes_master_rels.as_bytes())?;
624    Ok(())
625}
626
627/// Write theme and layout files
628fn write_theme_and_layouts<W: Write + Seek>(
629    zip: &mut ZipWriter<W>,
630    options: &FileOptions,
631    settings: Option<&PresentationSettings>,
632    template: Option<&PptxTemplate>,
633) -> Result<()> {
634    let print = settings.and_then(|s| s.print.as_ref());
635
636    if let Some(tmpl) = template {
637        for (path, data) in tmpl.parts() {
638            zip.start_file(path, *options)?;
639            zip.write_all(data)?;
640        }
641        if let Some(theme) = settings.and_then(|s| s.theme.as_ref()) {
642            let theme_xml = create_theme_xml(Some(theme));
643            zip.start_file("ppt/theme/theme1.xml", *options)?;
644            zip.write_all(theme_xml.as_bytes())?;
645        }
646        return Ok(());
647    }
648
649    let use_cached_layouts = !print_affects_theme_parts(print);
650
651    for n in 1..=STANDARD_LAYOUT_COUNT {
652        zip.start_file(format!("ppt/slideLayouts/slideLayout{n}.xml"), *options)?;
653        if use_cached_layouts {
654            zip.write_all(package_cache::default_layout_xml(n).as_bytes())?;
655        } else {
656            let layout_xml = create_slide_layout_xml(n, print);
657            zip.write_all(layout_xml.as_bytes())?;
658        }
659
660        zip.start_file(format!("ppt/slideLayouts/_rels/slideLayout{n}.xml.rels"), *options)?;
661        zip.write_all(create_layout_rels_xml().as_bytes())?;
662    }
663
664    zip.start_file("ppt/slideMasters/slideMaster1.xml", *options)?;
665    if use_cached_layouts {
666        zip.write_all(package_cache::default_slide_master_xml().as_bytes())?;
667    } else {
668        let slide_master = create_slide_master_xml(print);
669        zip.write_all(slide_master.as_bytes())?;
670    }
671
672    zip.start_file("ppt/slideMasters/_rels/slideMaster1.xml.rels", *options)?;
673    if use_cached_layouts {
674        zip.write_all(package_cache::master_rels_xml().as_bytes())?;
675    } else {
676        let master_rels = create_master_rels_xml();
677        zip.write_all(master_rels.as_bytes())?;
678    }
679
680    if let Some(theme) = settings.and_then(|s| s.theme.as_ref()) {
681        let theme_xml = create_theme_xml(Some(theme));
682        zip.start_file("ppt/theme/theme1.xml", *options)?;
683        zip.write_all(theme_xml.as_bytes())?;
684    } else {
685        zip.start_file("ppt/theme/theme1.xml", *options)?;
686        zip.write_all(office_theme_xml().as_bytes())?;
687    }
688
689    Ok(())
690}
691
692/// Write document properties
693fn write_document_properties<W: Write + Seek>(
694    zip: &mut ZipWriter<W>,
695    options: &FileOptions,
696    title: &str,
697    slide_count: usize,
698    notes_count: usize,
699    slide_titles: &[String],
700) -> Result<()> {
701    // Core properties
702    let core_props = create_core_props_xml(title);
703    zip.start_file("docProps/core.xml", *options)?;
704    zip.write_all(core_props.as_bytes())?;
705
706    // App properties
707    let app_props = create_app_props_xml(slide_count, notes_count, slide_titles);
708    zip.start_file("docProps/app.xml", *options)?;
709    zip.write_all(app_props.as_bytes())?;
710
711    Ok(())
712}
713
714/// Write all package files to the ZIP archive (eager version with Vec<SlideContent>)
715fn write_package_files<W: Write + Seek>(
716    zip: &mut ZipWriter<W>,
717    options: &FileOptions,
718    title: &str,
719    slide_count: usize,
720    custom_slides: Option<&[SlideContent]>,
721    mut settings: Option<PresentationSettings>,
722) -> Result<()> {
723    let has_notes = custom_slides
724        .map(|slides| slides.iter().any(|s| s.notes.is_some()))
725        .unwrap_or(false);
726    let has_handout = uses_handouts(settings.as_ref());
727    prepare_settings(&mut settings, slide_count, has_notes, has_handout);
728    let has_signature = has_digital_signature(settings.as_ref());
729    let template = load_template(settings.as_ref())?;
730
731    let chart_info = collect_chart_info(custom_slides);
732
733    // 1. Content types
734    write_content_types(zip, options, slide_count, custom_slides, &chart_info, has_handout, settings.as_ref())?;
735
736    // 2. Package relationships
737    let rels = create_rels_xml_with_signature(has_signature);
738    zip.start_file("_rels/.rels", *options)?;
739    zip.write_all(rels.as_bytes())?;
740
741    // 3. Presentation relationships
742    write_presentation_relationships(zip, options, slide_count, has_notes, has_handout, settings.as_ref())?;
743
744    // 4. Presentation document
745    let presentation = if let Some(fonts) = embedded_fonts(settings.as_ref()) {
746        create_presentation_xml_with_fonts(title, slide_count, has_notes, has_handout, fonts)
747    } else {
748        create_presentation_xml(title, slide_count, has_notes, has_handout)
749    };
750    zip.start_file("ppt/presentation.xml", *options)?;
751    zip.write_all(presentation.as_bytes())?;
752
753    // 5. Standard package parts (presProps, viewProps, tableStyles)
754    write_standard_package_parts(zip, options, settings.as_ref())?;
755
756    // 6. Handout master (when printing handouts)
757    if has_handout {
758        write_handout_master(zip, options, settings.as_ref())?;
759    }
760
761    // 7. Slides
762    write_slides(zip, options, slide_count, custom_slides)?;
763
764    // 8. Slide relationships
765    write_slide_relationships_extended(
766        zip,
767        options,
768        custom_slides,
769        &chart_info.slide_start_indices,
770        slide_count,
771        template.as_ref(),
772    )?;
773
774    // 9. Notes relationships and master
775    if has_notes {
776        write_notes_relationships(zip, options, custom_slides)?;
777        write_notes_master(zip, options)?;
778    }
779
780    // 10. Theme and layouts
781    write_theme_and_layouts(zip, options, settings.as_ref(), template.as_ref())?;
782
783    // 11. Document properties
784    let notes_count = custom_slides
785        .map(|slides| slides.iter().filter(|s| s.notes.is_some()).count())
786        .unwrap_or(0);
787    let slide_titles = collect_slide_titles(custom_slides, slide_count);
788    write_document_properties(zip, options, title, slide_count, notes_count, &slide_titles)?;
789
790    // 12. Charts (with embedded workbooks)
791    if chart_info.total_charts > 0 {
792        write_charts(zip, options, custom_slides, &chart_info.slide_start_indices)?;
793    }
794
795    // 13. Images
796    write_images(zip, options, custom_slides)?;
797
798    // 14. Embedded font data parts
799    if let Some(fonts) = embedded_fonts(settings.as_ref()) {
800        write_embedded_font_parts(zip, options, fonts)?;
801    }
802
803    // 15. Digital signature package parts
804    if has_signature {
805        write_digital_signature_parts(zip, options, settings.as_ref())?;
806    }
807
808    Ok(())
809}
810
811/// Write all package files to the ZIP archive (lazy version with LazySlideSource)
812fn write_package_files_lazy<W: Write + Seek>(
813    zip: &mut ZipWriter<W>,
814    options: &FileOptions,
815    title: &str,
816    slides: &dyn LazySlideSource,
817    mut settings: Option<PresentationSettings>,
818) -> Result<()> {
819    let slide_count = slides.slide_count();
820    let has_notes = (0..slide_count).any(|i| {
821        slides
822            .slide_features(i)
823            .map(|(notes, _)| notes)
824            .unwrap_or(false)
825    });
826    let has_handout = uses_handouts(settings.as_ref());
827    prepare_settings(&mut settings, slide_count, has_notes, has_handout);
828    let has_signature = has_digital_signature(settings.as_ref());
829    let template = load_template(settings.as_ref())?;
830
831    let chart_info = collect_chart_info_lazy(slides);
832
833    // 1. Content types (lazy version)
834    write_content_types_lazy(zip, options, slide_count, slides, &chart_info, has_handout, settings.as_ref())?;
835
836    // 2. Package relationships
837    let rels = create_rels_xml_with_signature(has_signature);
838    zip.start_file("_rels/.rels", *options)?;
839    zip.write_all(rels.as_bytes())?;
840
841    // 3. Presentation relationships
842    write_presentation_relationships(zip, options, slide_count, has_notes, has_handout, settings.as_ref())?;
843
844    // 4. Presentation document
845    let presentation = if let Some(fonts) = embedded_fonts(settings.as_ref()) {
846        create_presentation_xml_with_fonts(title, slide_count, has_notes, has_handout, fonts)
847    } else {
848        create_presentation_xml(title, slide_count, has_notes, has_handout)
849    };
850    zip.start_file("ppt/presentation.xml", *options)?;
851    zip.write_all(presentation.as_bytes())?;
852
853    // 5. Standard package parts
854    write_standard_package_parts(zip, options, settings.as_ref())?;
855
856    // 6. Handout master
857    if has_handout {
858        write_handout_master(zip, options, settings.as_ref())?;
859    }
860
861    // 7–8–12. Slides, relationships, and charts (single pass per slide)
862    write_slide_packages_lazy(
863        zip,
864        options,
865        slides,
866        &chart_info.slide_start_indices,
867        template.as_ref(),
868    )?;
869
870    // 9. Notes relationships and master (lazy version)
871    if has_notes {
872        write_notes_relationships_lazy(zip, options, slides)?;
873        write_notes_master(zip, options)?;
874    }
875
876    // 10. Theme and layouts
877    write_theme_and_layouts(zip, options, settings.as_ref(), template.as_ref())?;
878
879    // 11. Document properties
880    let notes_count = (0..slide_count)
881        .filter(|i| {
882            slides
883                .slide_features(*i)
884                .map(|(notes, _)| notes)
885                .unwrap_or(false)
886        })
887        .count();
888    let slide_titles = collect_slide_titles_lazy(slides, slide_count);
889    write_document_properties(zip, options, title, slide_count, notes_count, &slide_titles)?;
890
891    // 12. Images
892    write_images_lazy(zip, options, slides)?;
893
894    // 13. Embedded font data parts
895    if let Some(fonts) = embedded_fonts(settings.as_ref()) {
896        write_embedded_font_parts(zip, options, fonts)?;
897    }
898
899    // 14. Digital signature package parts
900    if has_signature {
901        write_digital_signature_parts(zip, options, settings.as_ref())?;
902    }
903
904    Ok(())
905}
906
907/// Write content types for lazy slides
908fn write_content_types_lazy<W: Write + Seek>(
909    zip: &mut ZipWriter<W>,
910    options: &FileOptions,
911    slide_count: usize,
912    slides: &dyn LazySlideSource,
913    chart_info: &ChartInfo,
914    has_handout: bool,
915    settings: Option<&PresentationSettings>,
916) -> Result<()> {
917    let notes_count = (0..slide_count)
918        .filter(|i| {
919            slides
920                .slide_features(*i)
921                .map(|(notes, _)| notes)
922                .unwrap_or(false)
923        })
924        .count();
925
926    let ink_count = (0..slide_count)
927        .filter(|i| {
928            slides
929                .generate_slide(*i)
930                .map(|s| s.ink_annotations.is_some())
931                .unwrap_or(false)
932        })
933        .count();
934
935    let media_registry = build_media_registry_lazy(slides);
936    let media_exts = media_registry.extensions();
937
938    let mut content_types = content_types_opening(&media_exts, chart_info.total_charts);
939
940    for i in 1..=slide_count {
941        content_types.push_str(&format!(
942            "\n<Override PartName=\"/ppt/slides/slide{i}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.slide+xml\"/>"
943        ));
944    }
945
946    if notes_count > 0 {
947        let mut notes_index = 0usize;
948        for i in 0..slide_count {
949            if slides
950                .slide_features(i)
951                .map(|(notes, _)| notes)
952                .unwrap_or(false)
953            {
954                notes_index += 1;
955                content_types.push_str(&format!(
956                    "\n<Override PartName=\"/ppt/notesSlides/notesSlide{notes_index}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml\"/>"
957                ));
958            }
959        }
960        content_types.push_str("\n<Override PartName=\"/ppt/notesMasters/notesMaster1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml\"/>");
961        content_types.push_str("\n<Override PartName=\"/ppt/theme/theme2.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.theme+xml\"/>");
962    }
963
964    if has_handout {
965        content_types.push_str("\n<Override PartName=\"/ppt/handoutMasters/handoutMaster1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml\"/>");
966        content_types.push_str("\n<Override PartName=\"/ppt/theme/theme3.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.theme+xml\"/>");
967    }
968
969    for i in 1..=chart_info.total_charts {
970        content_types.push_str(&format!(
971            "\n<Override PartName=\"/ppt/charts/chart{i}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.drawingml.chart+xml\"/>"
972        ));
973        content_types.push_str(&format!(
974            "\n<Override PartName=\"/ppt/embeddings/{}\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"/>",
975            chart_embedding_filename(i)
976        ));
977    }
978
979    content_types.push_str(
980        r#"
981<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
982<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
983<Override PartName="/ppt/tableStyles.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml"/>
984<Override PartName="/ppt/viewProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml"/>
985<Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/>
986<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
987<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>"#,
988    );
989    super::layout_parts::append_layout_content_type_overrides(&mut content_types, STANDARD_LAYOUT_COUNT);
990
991    if has_digital_signature(settings) {
992        append_digital_signature_content_type(&mut content_types);
993    }
994    if has_embedded_fonts(settings) {
995        append_embedded_font_content_type(&mut content_types);
996    }
997
998    super::package_xml::append_ink_content_types(&mut content_types, ink_count);
999
1000    content_types.push_str("\n</Types>");
1001
1002    zip.start_file("[Content_Types].xml", *options)?;
1003    zip.write_all(content_types.as_bytes())?;
1004    Ok(())
1005}
1006
1007/// Write a chart part with rels and embedded Excel workbook.
1008fn write_chart_package<W: Write + Seek>(
1009    zip: &mut ZipWriter<W>,
1010    options: &FileOptions,
1011    chart_idx: usize,
1012    chart: &crate::generator::charts::Chart,
1013) -> Result<()> {
1014    let chart_xml = generate_chart_part_xml(chart);
1015    zip.start_file(format!("ppt/charts/chart{chart_idx}.xml"), *options)?;
1016    zip.write_all(chart_xml.as_bytes())?;
1017
1018    let embedding_name = chart_embedding_filename(chart_idx);
1019    let rels_xml = create_chart_rels_xml(&embedding_name);
1020    zip.start_file(format!("ppt/charts/_rels/chart{chart_idx}.xml.rels"), *options)?;
1021    zip.write_all(rels_xml.as_bytes())?;
1022
1023    zip.start_file(format!("ppt/embeddings/{embedding_name}"), *options)?;
1024    zip.write_all(reference_workbook_bytes())?;
1025    Ok(())
1026}
1027
1028/// Write slide XML, relationships, and chart parts in one pass (lazy version).
1029fn write_slide_packages_lazy<W: Write + Seek>(
1030    zip: &mut ZipWriter<W>,
1031    options: &FileOptions,
1032    slides: &dyn LazySlideSource,
1033    slide_chart_start_indices: &[usize],
1034    template: Option<&PptxTemplate>,
1035) -> Result<()> {
1036    let media_registry = build_media_registry_lazy(slides);
1037    let mut slide_path = String::with_capacity(48);
1038    let mut rels_path = String::with_capacity(56);
1039    let mut notes_part_num = 0usize;
1040    let mut ink_part_num = 0usize;
1041
1042    for i in 0..slides.slide_count() {
1043        let Some(slide) = slides.generate_slide(i) else {
1044            continue;
1045        };
1046        let slide_num = i + 1;
1047        let layout_number = resolve_layout_number(&slide, template);
1048        let images = slide_image_rel_targets(&slide, &media_registry);
1049        let image_count = images.len();
1050
1051        let start_rid = slide_content_rel_start(slide.notes.is_some(), image_count);
1052        let mut chart_rids = Vec::with_capacity(slide.charts.len());
1053        for j in 0..slide.charts.len() {
1054            push_chart_rid(&mut chart_rids, start_rid + j);
1055        }
1056
1057        let ink_rel_id = if slide.ink_annotations.is_some() {
1058            ink_part_num += 1;
1059            let ink_xml = slide
1060                .ink_annotations
1061                .as_ref()
1062                .expect("ink checked above")
1063                .part_xml();
1064            zip.start_file(format!("ppt/ink/ink{ink_part_num}.xml"), *options)?;
1065            zip.write_all(ink_xml.as_bytes())?;
1066            Some(format!("rId{}", start_rid + slide.charts.len()))
1067        } else {
1068            None
1069        };
1070
1071        let slide_xml = create_slide_xml_with_content(
1072            slide_num,
1073            &slide,
1074            &chart_rids,
1075            ink_rel_id.as_deref(),
1076        );
1077        set_slide_xml_path(&mut slide_path, slide_num);
1078        zip.start_file(&slide_path, *options)?;
1079        zip.write_all(slide_xml.as_bytes())?;
1080
1081        let notes_part = if slide.notes.is_some() {
1082            notes_part_num += 1;
1083            Some(notes_part_num)
1084        } else {
1085            None
1086        };
1087
1088        if let Some(ref notes) = slide.notes {
1089            let notes_xml = create_notes_xml(slide_num, notes);
1090            set_notes_part_path(&mut slide_path, notes_part_num);
1091            zip.start_file(&slide_path, *options)?;
1092            zip.write_all(notes_xml.as_bytes())?;
1093        }
1094
1095        let mut chart_rels = Vec::with_capacity(slide.charts.len());
1096        let start_chart_idx = slide_chart_start_indices[i];
1097        for j in 0..slide.charts.len() {
1098            let mut rid = String::with_capacity(8);
1099            rid.push_str("rId");
1100            append_usize(&mut rid, start_rid + j);
1101            let mut target = String::with_capacity(24);
1102            target.push_str("../charts/chart");
1103            append_usize(&mut target, start_chart_idx + j);
1104            target.push_str(".xml");
1105            chart_rels.push((rid, target));
1106        }
1107
1108        let ink_rel_tuple = ink_rel_id.map(|_| (start_rid + slide.charts.len(), ink_part_num));
1109        let slide_rels = super::package_xml::create_slide_rels_xml_with_images(
1110            layout_number,
1111            slide.notes.is_some(),
1112            notes_part.unwrap_or(1),
1113            &chart_rels,
1114            &images,
1115            &slide_hyperlink_relationships(&slide),
1116            ink_rel_tuple,
1117        );
1118        set_slide_rels_path(&mut rels_path, slide_num);
1119        zip.start_file(&rels_path, *options)?;
1120        zip.write_all(slide_rels.as_bytes())?;
1121
1122        for (j, chart) in slide.charts.iter().enumerate() {
1123            let chart_idx = start_chart_idx + j;
1124            write_chart_package(zip, options, chart_idx, chart)?;
1125        }
1126    }
1127
1128    Ok(())
1129}
1130
1131/// Write notes relationship files (lazy version)
1132fn write_notes_relationships_lazy<W: Write + Seek>(
1133    zip: &mut ZipWriter<W>,
1134    options: &FileOptions,
1135    slides: &dyn LazySlideSource,
1136) -> Result<()> {
1137    let mut notes_part_num = 0usize;
1138    for i in 0..slides.slide_count() {
1139        if slides
1140            .slide_features(i)
1141            .map(|(notes, _)| notes)
1142            .unwrap_or(false)
1143        {
1144            notes_part_num += 1;
1145            let slide_num = i + 1;
1146            let notes_rels = create_notes_rels_xml(slide_num);
1147            zip.start_file(format!("ppt/notesSlides/_rels/notesSlide{notes_part_num}.xml.rels"), *options)?;
1148            zip.write_all(notes_rels.as_bytes())?;
1149        }
1150    }
1151    Ok(())
1152}
1153
1154/// Write slide XML files (eager version)
1155fn write_slides<W: Write + Seek>(
1156    zip: &mut ZipWriter<W>,
1157    options: &FileOptions,
1158    slide_count: usize,
1159    custom_slides: Option<&[SlideContent]>,
1160) -> Result<()> {
1161    let mut zip_path = String::with_capacity(48);
1162
1163    match custom_slides {
1164        Some(slides) => {
1165            let mut notes_part_num = 0usize;
1166            let mut ink_part_num = 0usize;
1167            for (i, slide) in slides.iter().enumerate() {
1168                let slide_num = i + 1;
1169
1170                let mut chart_rids = Vec::with_capacity(slide.charts.len());
1171                let start_rid = slide_content_rel_start(slide.notes.is_some(), slide.images.len());
1172                for j in 0..slide.charts.len() {
1173                    push_chart_rid(&mut chart_rids, start_rid + j);
1174                }
1175
1176                let ink_rel_id = if slide.ink_annotations.is_some() {
1177                    ink_part_num += 1;
1178                    let ink_xml = slide
1179                        .ink_annotations
1180                        .as_ref()
1181                        .expect("ink checked above")
1182                        .part_xml();
1183                    zip.start_file(format!("ppt/ink/ink{ink_part_num}.xml"), *options)?;
1184                    zip.write_all(ink_xml.as_bytes())?;
1185                    Some(format!("rId{}", start_rid + slide.charts.len()))
1186                } else {
1187                    None
1188                };
1189
1190                let slide_xml = create_slide_xml_with_content(
1191                    slide_num,
1192                    slide,
1193                    &chart_rids,
1194                    ink_rel_id.as_deref(),
1195                );
1196                set_slide_xml_path(&mut zip_path, slide_num);
1197                zip.start_file(&zip_path, *options)?;
1198                zip.write_all(slide_xml.as_bytes())?;
1199
1200                if let Some(notes) = &slide.notes {
1201                    notes_part_num += 1;
1202                    let notes_xml = create_notes_xml(slide_num, notes);
1203                    set_notes_part_path(&mut zip_path, notes_part_num);
1204                    zip.start_file(&zip_path, *options)?;
1205                    zip.write_all(notes_xml.as_bytes())?;
1206                }
1207            }
1208        }
1209        None => {
1210            for i in 1..=slide_count {
1211                let slide_xml = create_slide_xml(i, "Presentation");
1212                set_slide_xml_path(&mut zip_path, i);
1213                zip.start_file(&zip_path, *options)?;
1214                zip.write_all(slide_xml.as_bytes())?;
1215            }
1216        }
1217    }
1218    Ok(())
1219}
1220
1221/// Write slide relationship files with notes and charts (eager version)
1222fn write_slide_relationships_extended<W: Write + Seek>(
1223    zip: &mut ZipWriter<W>,
1224    options: &FileOptions,
1225    custom_slides: Option<&[SlideContent]>,
1226    slide_chart_start_indices: &[usize],
1227    slide_count: usize,
1228    template: Option<&PptxTemplate>,
1229) -> Result<()> {
1230    let media_registry = custom_slides
1231        .map(build_media_registry)
1232        .unwrap_or_default();
1233    let mut notes_part_num = 0usize;
1234
1235    match custom_slides {
1236        Some(slides) => {
1237            let mut zip_path = String::with_capacity(56);
1238            let mut ink_part_num = 0usize;
1239            for (i, slide) in slides.iter().enumerate() {
1240                let slide_num = i + 1;
1241                let layout_number = resolve_layout_number(slide, template);
1242                let images = slide_image_rel_targets(slide, &media_registry);
1243                let image_count = images.len();
1244                let notes_part = if slide.notes.is_some() {
1245                    notes_part_num += 1;
1246                    Some(notes_part_num)
1247                } else {
1248                    None
1249                };
1250
1251                let mut chart_rels = Vec::with_capacity(slide.charts.len());
1252                let start_chart_idx = slide_chart_start_indices[i];
1253                let start_rid = slide_content_rel_start(slide.notes.is_some(), image_count);
1254
1255                for j in 0..slide.charts.len() {
1256                    let mut rid = String::with_capacity(8);
1257                    rid.push_str("rId");
1258                    append_usize(&mut rid, start_rid + j);
1259                    let mut target = String::with_capacity(24);
1260                    target.push_str("../charts/chart");
1261                    append_usize(&mut target, start_chart_idx + j);
1262                    target.push_str(".xml");
1263                    chart_rels.push((rid, target));
1264                }
1265
1266                let ink_rel_tuple = if slide.ink_annotations.is_some() {
1267                    ink_part_num += 1;
1268                    Some((start_rid + slide.charts.len(), ink_part_num))
1269                } else {
1270                    None
1271                };
1272
1273                let slide_rels = super::package_xml::create_slide_rels_xml_with_images(
1274                    layout_number,
1275                    slide.notes.is_some(),
1276                    notes_part.unwrap_or(1),
1277                    &chart_rels,
1278                    &images,
1279                    &slide_hyperlink_relationships(slide),
1280                    ink_rel_tuple,
1281                );
1282                set_slide_rels_path(&mut zip_path, slide_num);
1283                zip.start_file(&zip_path, *options)?;
1284                zip.write_all(slide_rels.as_bytes())?;
1285            }
1286        }
1287        None => {
1288            let mut zip_path = String::with_capacity(56);
1289            for i in 1..=slide_count {
1290                let slide_rels = create_slide_rels_xml();
1291                set_slide_rels_path(&mut zip_path, i);
1292                zip.start_file(&zip_path, *options)?;
1293                zip.write_all(slide_rels.as_bytes())?;
1294            }
1295        }
1296    }
1297    Ok(())
1298}
1299
1300/// Write chart files (eager version)
1301fn write_charts<W: Write + Seek>(
1302    zip: &mut ZipWriter<W>,
1303    options: &FileOptions,
1304    custom_slides: Option<&[SlideContent]>,
1305    slide_chart_start_indices: &[usize],
1306) -> Result<()> {
1307    if let Some(slides) = custom_slides {
1308        for (i, slide) in slides.iter().enumerate() {
1309            let start_chart_idx = slide_chart_start_indices[i];
1310            for (j, chart) in slide.charts.iter().enumerate() {
1311                let chart_idx = start_chart_idx + j;
1312                write_chart_package(zip, options, chart_idx, chart)?;
1313            }
1314        }
1315    }
1316    Ok(())
1317}
1318
1319/// Write notes relationship files (eager version)
1320fn write_notes_relationships<W: Write + Seek>(
1321    zip: &mut ZipWriter<W>,
1322    options: &FileOptions,
1323    custom_slides: Option<&[SlideContent]>,
1324) -> Result<()> {
1325    if let Some(slides) = custom_slides {
1326        let mut notes_part_num = 0usize;
1327        for (i, slide) in slides.iter().enumerate() {
1328            if slide.notes.is_some() {
1329                notes_part_num += 1;
1330                let slide_num = i + 1;
1331                let notes_rels = create_notes_rels_xml(slide_num);
1332                zip.start_file(format!("ppt/notesSlides/_rels/notesSlide{notes_part_num}.xml.rels"), *options)?;
1333                zip.write_all(notes_rels.as_bytes())?;
1334            }
1335        }
1336    }
1337    Ok(())
1338}
1339
1340/// Write embedded font data parts to `ppt/fonts/`.
1341fn write_embedded_font_parts<W: Write + Seek>(
1342    zip: &mut ZipWriter<W>,
1343    options: &FileOptions,
1344    fonts: &super::slide_content::embedded_fonts::EmbeddedFontList,
1345) -> Result<()> {
1346    for font in fonts.fonts() {
1347        zip.start_file(font.part_name(), *options)?;
1348        zip.write_all(&font.data)?;
1349    }
1350    Ok(())
1351}
1352
1353/// Write image files to ppt/media/
1354fn write_images<W: Write + Seek>(
1355    zip: &mut ZipWriter<W>,
1356    options: &FileOptions,
1357    custom_slides: Option<&[SlideContent]>,
1358) -> Result<()> {
1359    if let Some(slides) = custom_slides {
1360        let registry = build_media_registry(slides);
1361        for (i, (bytes, ext)) in registry.files().iter().enumerate() {
1362            let filename = format!("ppt/media/image{}.{}", i + 1, ext);
1363            zip.start_file(filename, *options)?;
1364            zip.write_all(bytes)?;
1365        }
1366    }
1367    Ok(())
1368}
1369
1370/// Write digital signature parts (`_xmlsignatures/`).
1371fn write_digital_signature_parts<W: Write + Seek>(
1372    zip: &mut ZipWriter<W>,
1373    options: &FileOptions,
1374    settings: Option<&PresentationSettings>,
1375) -> Result<()> {
1376    let signature = settings
1377        .and_then(|s| s.digital_signature.as_ref())
1378        .expect("digital signature parts requested but none configured");
1379
1380    // `origin.sigs` is the signature origin relationships part.
1381    zip.start_file("_xmlsignatures/origin.sigs", *options)?;
1382    zip.write_all(signature.to_origin_xml().as_bytes())?;
1383
1384    zip.start_file("_xmlsignatures/sig1.xml", *options)?;
1385    zip.write_all(signature.to_signature_xml().as_bytes())?;
1386
1387    Ok(())
1388}
1389
1390/// Write image files from a lazy slide source.
1391fn write_images_lazy<W: Write + Seek>(
1392    zip: &mut ZipWriter<W>,
1393    options: &FileOptions,
1394    slides: &dyn LazySlideSource,
1395) -> Result<()> {
1396    let registry = build_media_registry_lazy(slides);
1397    for (i, (bytes, ext)) in registry.files().iter().enumerate() {
1398        let filename = format!("ppt/media/image{}.{}", i + 1, ext);
1399        zip.start_file(filename, *options)?;
1400        zip.write_all(bytes)?;
1401    }
1402    Ok(())
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::*;
1408    use std::io::{Cursor, Read};
1409
1410    /// A simple test slide source that generates numbered slides
1411    struct TestSlideSource {
1412        count: usize,
1413        with_notes: bool,
1414    }
1415
1416    impl LazySlideSource for TestSlideSource {
1417        fn slide_count(&self) -> usize {
1418            self.count
1419        }
1420
1421        fn generate_slide(&self, index: usize) -> Option<SlideContent> {
1422            if index < self.count {
1423                let mut slide = SlideContent::new(&format!("Slide {}", index + 1))
1424                    .add_bullet(&format!("Point {}", index + 1));
1425
1426                if self.with_notes {
1427                    slide.notes = Some("Speaker notes here".to_string());
1428                }
1429
1430                Some(slide)
1431            } else {
1432                None
1433            }
1434        }
1435
1436        fn slide_has_notes(&self, index: usize) -> bool {
1437            self.with_notes && index < self.count
1438        }
1439
1440        fn slide_chart_count(&self, _index: usize) -> usize {
1441            0
1442        }
1443    }
1444
1445    #[test]
1446    fn test_create_pptx_to_writer() {
1447        let buffer = Vec::new();
1448        let cursor = Cursor::new(buffer);
1449        let result = create_pptx_to_writer(cursor, "Test Presentation", 3);
1450        assert!(result.is_ok());
1451    }
1452
1453    #[test]
1454    fn test_create_pptx_with_digital_signature() {
1455        use crate::generator::slide_content::{DigitalSignature, SignerInfo};
1456
1457        let signature = DigitalSignature::new(SignerInfo::new("Alice"));
1458        let settings = PresentationSettings::new().digital_signature(signature);
1459        let bytes = create_pptx_with_settings("Signed", &[
1460            SlideContent::new("Slide 1").add_bullet("Point 1"),
1461        ], Some(settings)).unwrap();
1462
1463        let report = crate::core::validate_package_bytes(&bytes);
1464        assert!(report.is_valid(), "signature package invalid: {:?}", report.issues);
1465
1466        let cursor = Cursor::new(bytes);
1467        let mut archive = zip::ZipArchive::new(cursor).unwrap();
1468        let names: std::collections::HashSet<String> = (0..archive.len())
1469            .map(|i| archive.by_index(i).unwrap().name().to_string())
1470            .collect();
1471        assert!(names.contains("_xmlsignatures/origin.sigs"));
1472        assert!(names.contains("_xmlsignatures/sig1.xml"));
1473        assert!(names.contains("_rels/.rels"));
1474    }
1475
1476    #[test]
1477    fn test_create_pptx_with_embedded_fonts() {
1478        use crate::generator::slide_content::{EmbeddedFont, EmbeddedFontList, FontStyle};
1479
1480        let mut fonts = EmbeddedFontList::new();
1481        fonts.add(EmbeddedFont::new("Arial", FontStyle::Regular, vec![0u8; 20], ""));
1482        let settings = PresentationSettings::new().embedded_fonts(fonts);
1483
1484        let bytes = create_pptx_with_settings("Font Demo", &[
1485            SlideContent::new("Slide 1").add_bullet("Point 1"),
1486        ], Some(settings)).unwrap();
1487
1488        let report = crate::core::validate_package_bytes(&bytes);
1489        assert!(report.is_valid(), "embedded font package invalid: {:?}", report.issues);
1490
1491        let cursor = Cursor::new(bytes);
1492        let mut archive = zip::ZipArchive::new(cursor).unwrap();
1493        let names: std::collections::HashSet<String> = (0..archive.len())
1494            .map(|i| archive.by_index(i).unwrap().name().to_string())
1495            .collect();
1496        assert!(names.contains("ppt/fonts/Arial-regular.fntdata"));
1497
1498        let mut rels = String::new();
1499        archive.by_name("ppt/_rels/presentation.xml.rels").unwrap()
1500            .read_to_string(&mut rels).unwrap();
1501        assert!(rels.contains("relationships/font"));
1502        assert!(rels.contains("fonts/Arial-regular.fntdata"));
1503
1504        let mut presentation = String::new();
1505        archive.by_name("ppt/presentation.xml").unwrap()
1506            .read_to_string(&mut presentation).unwrap();
1507        assert!(presentation.contains("<p:embeddedFontLst>"));
1508        assert!(presentation.contains("Arial"));
1509    }
1510
1511    #[test]
1512    fn test_create_pptx_with_ink_annotations() {
1513        use crate::generator::slide_content::{InkAnnotations, InkPen, InkStroke};
1514
1515        let mut ink = InkAnnotations::new();
1516        ink.add_stroke(
1517            InkStroke::new(InkPen::red())
1518                .add_point(100.0, 100.0)
1519                .add_point(200.0, 200.0),
1520        );
1521
1522        let bytes = create_pptx_with_content("Ink Demo", vec![
1523            SlideContent::new("Slide 1").add_bullet("Point 1").with_ink(ink),
1524        ])
1525        .unwrap();
1526
1527        let report = crate::core::validate_package_bytes(&bytes);
1528        assert!(report.is_valid(), "ink package invalid: {:?}", report.issues);
1529
1530        let cursor = Cursor::new(bytes);
1531        let mut archive = zip::ZipArchive::new(cursor).unwrap();
1532        let names: std::collections::HashSet<String> = (0..archive.len())
1533            .map(|i| archive.by_index(i).unwrap().name().to_string())
1534            .collect();
1535        assert!(names.contains("ppt/ink/ink1.xml"));
1536
1537        let mut rels = String::new();
1538        archive
1539            .by_name("ppt/slides/_rels/slide1.xml.rels")
1540            .unwrap()
1541            .read_to_string(&mut rels)
1542            .unwrap();
1543        assert!(rels.contains("relationships/ink"));
1544        assert!(rels.contains("../ink/ink1.xml"));
1545
1546        let mut slide = String::new();
1547        archive
1548            .by_name("ppt/slides/slide1.xml")
1549            .unwrap()
1550            .read_to_string(&mut slide)
1551            .unwrap();
1552        assert!(slide.contains("mc:AlternateContent"));
1553        assert!(slide.contains("p:contentPart"));
1554    }
1555
1556    #[test]
1557    fn test_create_pptx_with_content_to_writer() {
1558        let slides = vec![
1559            SlideContent::new("Title").add_bullet("Point 1"),
1560            SlideContent::new("Slide 2").add_bullet("Point 2"),
1561        ];
1562
1563        let buffer = Vec::new();
1564        let cursor = Cursor::new(buffer);
1565        let result = create_pptx_with_content_to_writer(cursor, "Test", &slides, None);
1566        assert!(result.is_ok());
1567    }
1568
1569    #[test]
1570    fn test_create_pptx_lazy_to_writer() {
1571        let source = TestSlideSource { count: 10, with_notes: false };
1572
1573        let buffer = Vec::new();
1574        let cursor = Cursor::new(buffer);
1575        let result = create_pptx_lazy_to_writer(cursor, "Lazy Test", Box::new(source), None);
1576        assert!(result.is_ok());
1577    }
1578
1579    #[test]
1580    fn test_create_pptx_lazy_with_notes() {
1581        let source = TestSlideSource { count: 5, with_notes: true };
1582
1583        let buffer = Vec::new();
1584        let cursor = Cursor::new(buffer);
1585        let result = create_pptx_lazy_to_writer(cursor, "Lazy Test with Notes", Box::new(source), None);
1586        assert!(result.is_ok());
1587    }
1588
1589    #[test]
1590    fn test_lazy_slide_source() {
1591        let source = TestSlideSource { count: 3, with_notes: false };
1592
1593        assert_eq!(source.slide_count(), 3);
1594        assert!(!source.slide_has_notes(0));
1595        assert_eq!(source.slide_chart_count(0), 0);
1596
1597        let slide = source.generate_slide(0);
1598        assert!(slide.is_some());
1599        assert_eq!(slide.unwrap().title, "Slide 1");
1600
1601        let out_of_bounds = source.generate_slide(10);
1602        assert!(out_of_bounds.is_none());
1603    }
1604
1605    #[test]
1606    fn test_streaming_api_compatibility() {
1607        // Test that the streaming API produces the same output as the in-memory API
1608        let slides = vec![
1609            SlideContent::new("Test").add_bullet("Item 1"),
1610        ];
1611
1612        // In-memory version
1613        let in_memory = create_pptx_with_content("Test", slides.clone()).unwrap();
1614
1615        // Streaming version
1616        let buffer = Vec::new();
1617        let cursor = Cursor::new(buffer);
1618        let streaming = create_pptx_with_content_to_writer(cursor, "Test", &slides, None).unwrap().into_inner();
1619
1620        // Both should produce valid ZIP files (non-empty)
1621        assert!(!in_memory.is_empty());
1622        assert!(!streaming.is_empty());
1623    }
1624
1625    #[test]
1626    fn test_lazy_vs_eager_compatibility() {
1627        // Test that lazy and eager APIs produce compatible output
1628        let eager_slides = vec![
1629            SlideContent::new("Slide 1").add_bullet("Point 1"),
1630            SlideContent::new("Slide 2").add_bullet("Point 2"),
1631        ];
1632
1633        // Eager version
1634        let eager = create_pptx_with_content("Test", eager_slides).unwrap();
1635
1636        // Lazy version (equivalent content)
1637        let source = TestSlideSource { count: 2, with_notes: false };
1638        let buffer = Vec::new();
1639        let cursor = Cursor::new(buffer);
1640        let lazy = create_pptx_lazy_to_writer(cursor, "Test", Box::new(source), None).unwrap().into_inner();
1641
1642        // Both should produce valid ZIP files
1643        assert!(!eager.is_empty());
1644        assert!(!lazy.is_empty());
1645    }
1646}