1use 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
44fn 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
87fn 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
125pub 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
140pub 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
148pub 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
160fn 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
176pub 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
195pub 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
221pub 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
251pub trait LazySlideSource {
286 fn slide_count(&self) -> usize;
288
289 fn generate_slide(&self, index: usize) -> Option<SlideContent>;
291
292 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 fn slide_has_notes(&self, index: usize) -> bool {
300 self.slide_features(index)
301 .map(|(notes, _)| notes)
302 .unwrap_or(false)
303 }
304
305 fn slide_chart_count(&self, index: usize) -> usize {
307 self.slide_features(index)
308 .map(|(_, charts)| charts)
309 .unwrap_or(0)
310 }
311}
312
313pub 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
359struct 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
386fn 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
405fn 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
424fn 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
432fn has_digital_signature(settings: Option<&PresentationSettings>) -> bool {
434 settings
435 .and_then(|s| s.digital_signature.as_ref())
436 .is_some()
437}
438
439fn 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
447fn embedded_fonts(settings: Option<&PresentationSettings>) -> Option<&super::slide_content::embedded_fonts::EmbeddedFontList> {
449 settings.and_then(|s| s.embedded_fonts.as_ref())
450}
451
452fn 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
463fn 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
486fn 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
505fn 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
544fn 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
564fn 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
584fn 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
609fn 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
627fn 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
692fn 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 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 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
714fn 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 write_content_types(zip, options, slide_count, custom_slides, &chart_info, has_handout, settings.as_ref())?;
735
736 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 write_presentation_relationships(zip, options, slide_count, has_notes, has_handout, settings.as_ref())?;
743
744 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 write_standard_package_parts(zip, options, settings.as_ref())?;
755
756 if has_handout {
758 write_handout_master(zip, options, settings.as_ref())?;
759 }
760
761 write_slides(zip, options, slide_count, custom_slides)?;
763
764 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 if has_notes {
776 write_notes_relationships(zip, options, custom_slides)?;
777 write_notes_master(zip, options)?;
778 }
779
780 write_theme_and_layouts(zip, options, settings.as_ref(), template.as_ref())?;
782
783 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 if chart_info.total_charts > 0 {
792 write_charts(zip, options, custom_slides, &chart_info.slide_start_indices)?;
793 }
794
795 write_images(zip, options, custom_slides)?;
797
798 if let Some(fonts) = embedded_fonts(settings.as_ref()) {
800 write_embedded_font_parts(zip, options, fonts)?;
801 }
802
803 if has_signature {
805 write_digital_signature_parts(zip, options, settings.as_ref())?;
806 }
807
808 Ok(())
809}
810
811fn 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 write_content_types_lazy(zip, options, slide_count, slides, &chart_info, has_handout, settings.as_ref())?;
835
836 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 write_presentation_relationships(zip, options, slide_count, has_notes, has_handout, settings.as_ref())?;
843
844 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 write_standard_package_parts(zip, options, settings.as_ref())?;
855
856 if has_handout {
858 write_handout_master(zip, options, settings.as_ref())?;
859 }
860
861 write_slide_packages_lazy(
863 zip,
864 options,
865 slides,
866 &chart_info.slide_start_indices,
867 template.as_ref(),
868 )?;
869
870 if has_notes {
872 write_notes_relationships_lazy(zip, options, slides)?;
873 write_notes_master(zip, options)?;
874 }
875
876 write_theme_and_layouts(zip, options, settings.as_ref(), template.as_ref())?;
878
879 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 write_images_lazy(zip, options, slides)?;
893
894 if let Some(fonts) = embedded_fonts(settings.as_ref()) {
896 write_embedded_font_parts(zip, options, fonts)?;
897 }
898
899 if has_signature {
901 write_digital_signature_parts(zip, options, settings.as_ref())?;
902 }
903
904 Ok(())
905}
906
907fn 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
1007fn 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
1028fn 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
1131fn 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
1154fn 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
1221fn 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
1300fn 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
1319fn 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
1340fn 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
1353fn 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
1370fn 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 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
1390fn 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 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 let slides = vec![
1609 SlideContent::new("Test").add_bullet("Item 1"),
1610 ];
1611
1612 let in_memory = create_pptx_with_content("Test", slides.clone()).unwrap();
1614
1615 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 assert!(!in_memory.is_empty());
1622 assert!(!streaming.is_empty());
1623 }
1624
1625 #[test]
1626 fn test_lazy_vs_eager_compatibility() {
1627 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 let eager = create_pptx_with_content("Test", eager_slides).unwrap();
1635
1636 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 assert!(!eager.is_empty());
1644 assert!(!lazy.is_empty());
1645 }
1646}