1use std::path::Path;
10use std::rc::Rc;
11use std::sync::atomic::{AtomicUsize, Ordering};
12use std::sync::{Arc, OnceLock};
13
14use crate::cfb::{self, Compound};
15use crate::chart::{parse_chart, ChartData};
16use crate::comments::{parse_authors, parse_comments, Comment, CommentAuthor};
17use crate::diagram::{parse_diagram, DiagramData};
18use crate::error::{Error, Result};
19use crate::model::{Content, PlaceholderKind, SlideContent, TextBody};
20use crate::opc::{Relationships, TargetMode};
21use crate::package::{Package, PackageSeed};
22use crate::pml::{content_type, RelKind};
23use crate::ppt::{self, LegacyDeck};
24use crate::presentation::Presentation;
25use crate::properties::{AppProperties, CoreProperties};
26use crate::slide::{parse_slide, SlideReport};
27use crate::text::{write_content_text, ExtractReport, TextOptions};
28use crate::zip::{FileSource, Source};
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum Located {
33 LegacyStream,
35 Relationship,
37 ContentType,
39 ConventionalPath,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct DocumentDefects {
46 pub located: Located,
47 pub unresolved_slides: Vec<(usize, String)>,
49 pub slides_recovered_from_rels: bool,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct SlideRef {
56 pub part: String,
58 pub id: Option<u32>,
60 pub rel_id: String,
62}
63
64struct Shared {
65 presentation_part: String,
66 presentation: Presentation,
67 slides: Vec<SlideRef>,
68 defects: DocumentDefects,
69 package: Option<PackageSeed>,
71 legacy: Option<Arc<LegacyDeck>>,
73 authors: OnceLock<Vec<CommentAuthor>>,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct SlideSection {
80 pub name: String,
81 pub slides: Vec<usize>,
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct ObjectRef {
88 pub shape_id: u32,
90 pub prog_id: Option<String>,
92 pub rel_id: Option<String>,
93 pub part: Option<String>,
95 pub content_type: Option<String>,
96 pub external: Option<String>,
98}
99
100#[derive(Clone)]
102pub struct DocumentSeed {
103 shared: Arc<Shared>,
104 threads: usize,
105}
106
107impl DocumentSeed {
108 pub fn threads(&self) -> usize {
110 self.threads
111 }
112}
113
114pub struct Document {
116 package: Option<Package>,
117 shared: Arc<Shared>,
118 threads: usize,
119}
120
121fn threads_from_env() -> usize {
123 std::env::var("PPTXBOSS_THREADS")
124 .ok()
125 .and_then(|value| value.trim().parse().ok())
126 .unwrap_or(0)
127}
128
129impl Document {
130 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
132 let source = FileSource::open(path)?;
133 Self::from_source(Arc::new(source))
134 }
135
136 pub fn from_source(source: Arc<dyn Source>) -> Result<Self> {
139 let mut head = [0u8; 8];
140 if source.len() >= 8 {
141 source.read_at(0, &mut head)?;
142 }
143 if cfb::is_compound(&head) || head.starts_with(&[0xd0, 0xcf, 0x11, 0xe0]) {
144 let compound = Compound::open_source(source)?;
145 if compound.has_stream("EncryptionInfo") || compound.has_stream("EncryptedPackage") {
146 return Err(Error::Encrypted("package encrypted with a password".into()));
147 }
148 if ppt::is_presentation(&compound) {
149 return Ok(Self::from_legacy(ppt::open_compound(&compound)?));
150 }
151 return Err(Error::NoPresentationStream);
152 }
153 Self::from_package(Package::from_source(source)?)
154 }
155
156 pub fn from_legacy(deck: Arc<LegacyDeck>) -> Self {
158 let slide_ids = deck.slide_ids();
159 let presentation = Presentation {
160 slides: slide_ids
161 .iter()
162 .map(|id| crate::presentation::SlideId {
163 id: Some(*id),
164 rel_id: String::new(),
165 offset: 0,
166 })
167 .collect(),
168 slide_size: deck
169 .slide_size
170 .map(|(cx, cy)| crate::presentation::SlideSize {
171 cx,
172 cy,
173 kind: deck.slide_size_kind.map(str::to_string),
174 }),
175 notes_size: deck.notes_size,
176 first_slide_num: deck.first_slide_number,
177 root_ok: true,
178 ..Presentation::default()
179 };
180 let slides = slide_ids
181 .iter()
182 .enumerate()
183 .map(|(index, id)| SlideRef {
184 part: format!("PowerPoint Document/slide{}", index + 1),
185 id: Some(*id),
186 rel_id: String::new(),
187 })
188 .collect();
189 let shared = Arc::new(Shared {
190 presentation_part: "PowerPoint Document".to_string(),
191 presentation,
192 slides,
193 defects: DocumentDefects {
194 located: Located::LegacyStream,
195 unresolved_slides: Vec::new(),
196 slides_recovered_from_rels: deck.slides_recovered_by_scan,
197 },
198 package: None,
199 legacy: Some(deck),
200 authors: OnceLock::new(),
201 });
202 Self {
203 package: None,
204 shared,
205 threads: threads_from_env(),
206 }
207 }
208
209 pub fn load(bytes: Vec<u8>) -> Result<Self> {
211 Self::from_source(Arc::new(bytes))
212 }
213
214 pub fn from_package(package: Package) -> Result<Self> {
215 let (presentation_part, located) = locate_presentation(&package)?;
216 let xml = package.read_part(&presentation_part)?;
217 let presentation = Presentation::parse(&xml).map_err(|err| Error::Xml {
218 part: presentation_part.clone(),
219 offset: err.offset,
220 msg: err.msg.to_string(),
221 })?;
222 let rels = package.rels(&presentation_part)?;
223 let mut defects = DocumentDefects {
224 located,
225 unresolved_slides: Vec::new(),
226 slides_recovered_from_rels: false,
227 };
228 let mut slides = Vec::with_capacity(presentation.slides.len());
229 for (index, slide_id) in presentation.slides.iter().enumerate() {
230 match rels
231 .target_of(&slide_id.rel_id)
232 .filter(|part| package.has_part(part))
233 {
234 Some(part) => slides.push(SlideRef {
235 part,
236 id: slide_id.id,
237 rel_id: slide_id.rel_id.clone(),
238 }),
239 None => defects
240 .unresolved_slides
241 .push((index, slide_id.rel_id.clone())),
242 }
243 }
244 if slides.is_empty() && presentation.slides.is_empty() {
245 for rel in rels.iter().filter(|rel| {
246 rel.mode == TargetMode::Internal && RelKind::of(&rel.rel_type) == RelKind::Slide
247 }) {
248 if let Some(part) = rels.resolve(rel).filter(|part| package.has_part(part)) {
249 slides.push(SlideRef {
250 part,
251 id: None,
252 rel_id: rel.id.clone(),
253 });
254 }
255 }
256 defects.slides_recovered_from_rels = !slides.is_empty();
257 }
258 let shared = Arc::new(Shared {
259 presentation_part,
260 presentation,
261 slides,
262 defects,
263 package: Some(package.seed()),
264 legacy: None,
265 authors: OnceLock::new(),
266 });
267 Ok(Self {
268 package: Some(package),
269 shared,
270 threads: threads_from_env(),
271 })
272 }
273
274 pub fn core_properties(&self) -> Result<Option<CoreProperties>> {
276 let Some(part) = self.metadata_part(RelKind::CoreProperties, "/docProps/core.xml")? else {
277 return Ok(None);
278 };
279 let xml = self.pkg()?.read_part(&part)?;
280 CoreProperties::parse(&xml)
281 .map(Some)
282 .map_err(|err| xml_error(&part, err))
283 }
284
285 pub fn app_properties(&self) -> Result<Option<AppProperties>> {
287 let Some(part) = self.metadata_part(RelKind::ExtendedProperties, "/docProps/app.xml")?
288 else {
289 return Ok(None);
290 };
291 let xml = self.pkg()?.read_part(&part)?;
292 AppProperties::parse(&xml)
293 .map(Some)
294 .map_err(|err| xml_error(&part, err))
295 }
296
297 fn metadata_part(&self, kind: RelKind, conventional: &str) -> Result<Option<String>> {
299 let Some(package) = self.package.as_ref() else {
300 return Ok(None);
301 };
302 let rels = package.package_rels()?;
303 let by_rel = rels
304 .iter()
305 .filter(|rel| RelKind::of(&rel.rel_type) == kind)
306 .find_map(|rel| rels.resolve(rel))
307 .filter(|part| package.has_part(part));
308 if by_rel.is_some() {
309 return Ok(by_rel);
310 }
311 Ok(package
312 .has_part(conventional)
313 .then(|| conventional.to_string()))
314 }
315
316 pub fn sections(&self) -> Vec<SlideSection> {
319 self.shared
320 .presentation
321 .sections
322 .iter()
323 .map(|section| SlideSection {
324 name: section.name.clone(),
325 slides: section
326 .slide_ids
327 .iter()
328 .filter_map(|id| {
329 self.shared
330 .slides
331 .iter()
332 .position(|slide| slide.id == Some(*id))
333 })
334 .collect(),
335 })
336 .collect()
337 }
338
339 pub fn comment_authors(&self) -> &[CommentAuthor] {
342 self.shared.authors.get_or_init(|| {
343 let Some(package) = self.package.as_ref() else {
344 return Vec::new();
345 };
346 let Ok(rels) = package.rels(&self.shared.presentation_part) else {
347 return Vec::new();
348 };
349 let mut authors = Vec::new();
350 for rel in rels.iter() {
351 let kind = RelKind::of(&rel.rel_type);
352 if kind != RelKind::CommentAuthors && kind != RelKind::Authors {
353 continue;
354 }
355 let Some(part) = rels.resolve(rel) else {
356 continue;
357 };
358 let Ok(xml) = package.read_part(&part) else {
359 continue;
360 };
361 if let Ok(mut parsed) = parse_authors(&xml) {
362 authors.append(&mut parsed);
363 }
364 }
365 authors
366 })
367 }
368
369 pub fn threads(&self) -> usize {
371 self.threads
372 }
373
374 pub fn set_threads(&mut self, threads: usize) {
378 self.threads = threads;
379 }
380
381 pub fn with_threads(mut self, threads: usize) -> Self {
383 self.threads = threads;
384 self
385 }
386
387 pub fn seed(&self) -> DocumentSeed {
389 DocumentSeed {
390 shared: Arc::clone(&self.shared),
391 threads: self.threads,
392 }
393 }
394
395 pub fn from_seed(seed: DocumentSeed) -> Self {
397 Self {
398 package: seed.shared.package.clone().map(Package::from_seed),
399 shared: seed.shared,
400 threads: seed.threads,
401 }
402 }
403
404 pub fn package(&self) -> Option<&Package> {
406 self.package.as_ref()
407 }
408
409 pub fn legacy(&self) -> Option<&Arc<LegacyDeck>> {
411 self.shared.legacy.as_ref()
412 }
413
414 pub fn is_legacy(&self) -> bool {
416 self.shared.legacy.is_some()
417 }
418
419 fn pkg(&self) -> Result<&Package> {
420 self.package.as_ref().ok_or_else(|| {
421 Error::Unsupported("a legacy binary presentation has no package parts".into())
422 })
423 }
424
425 pub fn presentation(&self) -> &Presentation {
426 &self.shared.presentation
427 }
428
429 pub fn presentation_part(&self) -> &str {
431 &self.shared.presentation_part
432 }
433
434 pub fn defects(&self) -> &DocumentDefects {
435 &self.shared.defects
436 }
437
438 pub fn slide_refs(&self) -> &[SlideRef] {
440 &self.shared.slides
441 }
442
443 pub fn slide_count(&self) -> usize {
444 self.shared.slides.len()
445 }
446
447 pub fn slide(&self, index: usize) -> Result<Slide<'_>> {
449 let slide_ref = self
450 .shared
451 .slides
452 .get(index)
453 .ok_or(Error::SlideNotFound(index))?;
454 if let Some(deck) = &self.shared.legacy {
455 let slide = deck.slide(index)?;
456 return Ok(Slide {
457 doc: self,
458 index,
459 part: slide_ref.part.clone(),
460 content: slide.content,
461 report: SlideReport::default(),
462 });
463 }
464 let xml = self.pkg()?.read_part(&slide_ref.part)?;
465 let (content, report) = parse_slide(&xml).map_err(|err| Error::Xml {
466 part: slide_ref.part.clone(),
467 offset: err.offset,
468 msg: err.msg.to_string(),
469 })?;
470 Ok(Slide {
471 doc: self,
472 index,
473 part: slide_ref.part.clone(),
474 content,
475 report,
476 })
477 }
478
479 pub fn slides(&self) -> impl Iterator<Item = Result<Slide<'_>>> + '_ {
481 (0..self.slide_count()).map(move |index| self.slide(index))
482 }
483
484 pub fn slide_part(&self, part: &str) -> Result<(SlideContent, SlideReport)> {
486 let xml = self.pkg()?.read_part(part)?;
487 parse_slide(&xml).map_err(|err| Error::Xml {
488 part: part.to_string(),
489 offset: err.offset,
490 msg: err.msg.to_string(),
491 })
492 }
493
494 fn worker_count(&self, count: usize) -> usize {
496 if self.threads == 0 && self.is_legacy() {
497 return 1;
498 }
499 let limit = match self.threads {
500 0 => std::thread::available_parallelism().map_or(1, |n| n.get()),
501 limit => limit,
502 };
503 limit.min(count)
504 }
505
506 pub fn map_slides<T, F>(&self, f: F) -> Vec<T>
511 where
512 T: Send,
513 F: Fn(Result<Slide<'_>>) -> T + Sync,
514 {
515 self.map_slides_at(&self.all_slides(), f)
516 }
517
518 pub fn all_slides(&self) -> Vec<usize> {
520 (0..self.slide_count()).collect()
521 }
522
523 pub fn map_slides_at<T, F>(&self, indices: &[usize], f: F) -> Vec<T>
527 where
528 T: Send,
529 F: Fn(Result<Slide<'_>>) -> T + Sync,
530 {
531 let count = indices.len();
532 let workers = self.worker_count(count);
533 if workers <= 1 {
534 return indices.iter().map(|&index| f(self.slide(index))).collect();
535 }
536 let seed = self.seed();
537 let next = AtomicUsize::new(0);
538 let mut results: Vec<(usize, T)> = std::thread::scope(|scope| {
539 let handles: Vec<_> = (0..workers)
540 .map(|_| {
541 scope.spawn(|| {
542 let doc = Document::from_seed(seed.clone());
543 let mut mine = Vec::new();
544 loop {
545 let position = next.fetch_add(1, Ordering::Relaxed);
546 if position >= count {
547 return mine;
548 }
549 mine.push((position, f(doc.slide(indices[position]))));
550 }
551 })
552 })
553 .collect();
554 handles
555 .into_iter()
556 .flat_map(|handle| match handle.join() {
557 Ok(mine) => mine,
558 Err(payload) => std::panic::resume_unwind(payload),
559 })
560 .collect()
561 });
562 results.sort_by_key(|(position, _)| *position);
563 results.into_iter().map(|(_, value)| value).collect()
564 }
565
566 pub fn slide_texts(&self, options: &TextOptions) -> (Vec<String>, ExtractReport) {
568 self.slide_texts_at(&self.all_slides(), options)
569 }
570
571 pub fn slide_texts_at(
575 &self,
576 indices: &[usize],
577 options: &TextOptions,
578 ) -> (Vec<String>, ExtractReport) {
579 let results = self.map_slides_at(indices, |slide| match slide {
580 Ok(slide) => {
581 let mut report = ExtractReport::default();
582 let text = slide.text_reporting(options, &mut report);
583 (text, report)
584 }
585 Err(err) => {
586 let mut report = ExtractReport::default();
587 report.failed_slides.push((0, err.to_string()));
588 (String::new(), report)
589 }
590 });
591 let mut report = ExtractReport::default();
592 let mut texts = Vec::with_capacity(results.len());
593 for (&index, (text, slide_report)) in indices.iter().zip(results) {
594 report.merge(index, slide_report);
595 texts.push(text);
596 }
597 (texts, report)
598 }
599
600 pub fn text(&self) -> String {
602 self.text_reporting(&TextOptions::default()).0
603 }
604
605 pub fn text_reporting(&self, options: &TextOptions) -> (String, ExtractReport) {
606 let (texts, report) = self.slide_texts(options);
607 let mut out = String::with_capacity(texts.iter().map(|text| text.len() + 2).sum());
608 let mut first = true;
609 for text in texts.iter().filter(|text| !text.is_empty()) {
610 if !first {
611 out.push_str("\n\n");
612 }
613 first = false;
614 out.push_str(text);
615 }
616 (out, report)
617 }
618}
619
620fn write_comment_line(comment: &Comment, out: &mut String) {
622 if comment.reply {
623 out.push_str(" ");
624 }
625 out.push_str(match comment.reply {
626 true => "[reply] ",
627 false => "[comment] ",
628 });
629 if let Some(author) = &comment.author {
630 out.push_str(author);
631 out.push_str(": ");
632 }
633 out.push_str(comment.text.trim());
634}
635
636fn xml_error(part: &str, err: crate::xml::XmlError) -> Error {
637 Error::Xml {
638 part: part.to_string(),
639 offset: err.offset,
640 msg: err.msg.to_string(),
641 }
642}
643
644fn locate_presentation(package: &Package) -> Result<(String, Located)> {
645 let rels = package.package_rels()?;
646 let by_rel = rels
647 .iter()
648 .filter(|rel| RelKind::of(&rel.rel_type) == RelKind::OfficeDocument)
649 .filter_map(|rel| rels.resolve(rel))
650 .find(|part| package.has_part(part));
651 if let Some(part) = by_rel {
652 return Ok((part, Located::Relationship));
653 }
654 let by_type = package
655 .parts()
656 .iter()
657 .find(|part| {
658 package
659 .content_type_of(&part.name)
660 .is_some_and(content_type::is_presentation_main)
661 })
662 .map(|part| part.name.clone());
663 if let Some(part) = by_type {
664 return Ok((part, Located::ContentType));
665 }
666 if package.has_part("/ppt/presentation.xml") {
667 return Ok((
668 "/ppt/presentation.xml".to_string(),
669 Located::ConventionalPath,
670 ));
671 }
672 Err(Error::NotAPresentation)
673}
674
675#[derive(Clone, Debug, PartialEq, Eq)]
677pub struct ImageRef {
678 pub shape_id: u32,
680 pub rel_id: String,
681 pub part: Option<String>,
683 pub content_type: Option<String>,
685 pub external: Option<String>,
687}
688
689pub struct Slide<'d> {
691 doc: &'d Document,
692 pub index: usize,
693 pub part: String,
695 pub content: SlideContent,
696 pub report: SlideReport,
697}
698
699impl<'d> Slide<'d> {
700 pub fn number(&self) -> usize {
702 self.index + 1
703 }
704
705 pub fn document(&self) -> &'d Document {
706 self.doc
707 }
708
709 pub fn is_hidden(&self) -> bool {
711 !self.content.show
712 }
713
714 pub fn rels(&self) -> Result<Rc<Relationships>> {
716 match self.doc.package.as_ref() {
717 Some(package) => package.rels(&self.part),
718 None => Ok(Rc::new(Relationships::empty(&self.part))),
719 }
720 }
721
722 pub fn title(&self) -> Option<String> {
724 self.content.title()
725 }
726
727 pub fn text(&self) -> String {
729 let mut report = ExtractReport::default();
730 self.text_reporting(&TextOptions::default(), &mut report)
731 }
732
733 pub(crate) fn merge_parse_report(&self, report: &mut ExtractReport) {
735 report.unknown_graphics += self.report.unknown_graphics.len() as u32;
736 for uri in &self.report.unknown_graphics {
737 if !report.unknown_graphic_uris.contains(uri) {
738 report.unknown_graphic_uris.push(uri.clone());
739 }
740 }
741 report.unknown_elements += self.report.unknown_elements;
742 }
743
744 fn frame_text(
747 &self,
748 shape: &crate::model::Shape,
749 options: &TextOptions,
750 report: &mut ExtractReport,
751 ) -> Option<String> {
752 let result = match &shape.content {
753 Content::Chart(Some(rel_id)) if options.charts => self.chart(rel_id).map(|chart| {
754 let mut out = String::new();
755 chart.write_text(&options.cell_separator, &mut out);
756 out
757 }),
758 Content::Diagram(Some(rel_id)) if options.diagrams => {
759 self.diagram(rel_id).map(|diagram| {
760 let mut out = String::new();
761 diagram.write_text(&mut out);
762 out
763 })
764 }
765 _ => return None,
766 };
767 match result {
768 Ok(text) => Some(text).filter(|text| !text.is_empty()),
769 Err(err) => {
770 report.failed_frames.push((self.index, err.to_string()));
771 None
772 }
773 }
774 }
775
776 pub fn chart(&self, rel_id: &str) -> Result<ChartData> {
778 let part = self.frame_part(rel_id)?;
779 let xml = self.doc.pkg()?.read_part(&part)?;
780 parse_chart(&xml).map_err(|err| xml_error(&part, err))
781 }
782
783 pub fn diagram(&self, rel_id: &str) -> Result<DiagramData> {
785 let part = self.frame_part(rel_id)?;
786 let xml = self.doc.pkg()?.read_part(&part)?;
787 parse_diagram(&xml).map_err(|err| xml_error(&part, err))
788 }
789
790 fn frame_part(&self, rel_id: &str) -> Result<String> {
791 let rels = self.rels()?;
792 let package = self.doc.pkg()?;
793 rels.target_of(rel_id)
794 .filter(|part| package.has_part(part))
795 .ok_or_else(|| Error::MissingRelationship {
796 part: self.part.clone(),
797 id: rel_id.to_string(),
798 })
799 }
800
801 pub fn charts(&self) -> Result<Vec<(u32, ChartData)>> {
803 let mut charts = Vec::new();
804 for shape in self.content.walk() {
805 if let Content::Chart(Some(rel_id)) = &shape.content {
806 charts.push((shape.id, self.chart(rel_id)?));
807 }
808 }
809 Ok(charts)
810 }
811
812 pub fn diagrams(&self) -> Result<Vec<(u32, DiagramData)>> {
814 let mut diagrams = Vec::new();
815 for shape in self.content.walk() {
816 if let Content::Diagram(Some(rel_id)) = &shape.content {
817 diagrams.push((shape.id, self.diagram(rel_id)?));
818 }
819 }
820 Ok(diagrams)
821 }
822
823 pub fn text_reporting(&self, options: &TextOptions, report: &mut ExtractReport) -> String {
825 self.merge_parse_report(report);
826 if self.is_hidden() && !options.hidden_slides {
827 report.hidden_slides_skipped += 1;
828 return String::new();
829 }
830 let mut out = String::new();
831 write_content_text(
832 &self.content,
833 options,
834 &mut |shape| self.frame_text(shape, options, report),
835 &mut out,
836 );
837 if options.notes {
838 match self.notes() {
839 Ok(Some(notes)) if !notes.is_empty() => {
840 if !out.is_empty() {
841 out.push('\n');
842 }
843 notes.write_text(&mut out);
844 }
845 Ok(_) => {}
846 Err(err) => report.failed_notes.push((self.index, err.to_string())),
847 }
848 }
849 if options.comments {
850 match self.comments() {
851 Ok(comments) => {
852 for comment in comments {
853 if !out.is_empty() {
854 out.push('\n');
855 }
856 write_comment_line(&comment, &mut out);
857 }
858 }
859 Err(err) => report.failed_comments.push((self.index, err.to_string())),
860 }
861 }
862 out
863 }
864
865 fn related_part(&self, kind: RelKind) -> Result<Option<String>> {
867 let rels = self.rels()?;
868 let found = rels
869 .iter()
870 .filter(|rel| RelKind::of(&rel.rel_type) == kind)
871 .find_map(|rel| rels.resolve(rel));
872 Ok(found)
873 }
874
875 pub fn notes_part(&self) -> Result<Option<String>> {
877 if let Some(deck) = self.doc.legacy() {
878 return Ok(deck
879 .has_notes(self.index)
880 .then(|| format!("PowerPoint Document/notes{}", self.index + 1)));
881 }
882 self.related_part(RelKind::NotesSlide)
883 }
884
885 pub fn comments_part(&self) -> Result<Option<String>> {
887 match self.related_part(RelKind::ModernComments)? {
888 Some(part) => Ok(Some(part)),
889 None => self.related_part(RelKind::Comments),
890 }
891 }
892
893 pub fn comments(&self) -> Result<Vec<Comment>> {
895 let Some(part) = self.comments_part()? else {
896 return Ok(Vec::new());
897 };
898 let xml = self.doc.pkg()?.read_part(&part)?;
899 parse_comments(&xml, self.doc.comment_authors()).map_err(|err| xml_error(&part, err))
900 }
901
902 pub fn objects(&self) -> Result<Vec<ObjectRef>> {
904 let rels = self.rels()?;
905 let package = self.doc.package.as_ref();
906 let mut objects = Vec::new();
907 for shape in self.content.walk() {
908 let Content::Ole(ole) = &shape.content else {
909 continue;
910 };
911 let rel = ole.rel_id.as_deref().and_then(|id| rels.get(id));
912 let part = rel
913 .and_then(|rel| rels.resolve(rel))
914 .filter(|part| package.is_some_and(|package| package.has_part(part)));
915 let content_type = part
916 .as_deref()
917 .and_then(|part| package.and_then(|package| package.content_type_of(part)))
918 .map(str::to_string);
919 objects.push(ObjectRef {
920 shape_id: shape.id,
921 prog_id: ole.prog_id.clone(),
922 rel_id: ole.rel_id.clone(),
923 part,
924 content_type,
925 external: rel
926 .filter(|rel| rel.mode == TargetMode::External)
927 .map(|rel| rel.target.clone()),
928 });
929 }
930 Ok(objects)
931 }
932
933 pub fn object_bytes(&self, object: &ObjectRef) -> Result<Vec<u8>> {
935 let part = object
936 .part
937 .as_deref()
938 .ok_or_else(|| Error::MissingRelationship {
939 part: self.part.clone(),
940 id: object.rel_id.clone().unwrap_or_default(),
941 })?;
942 let mut out = Vec::new();
943 self.doc.pkg()?.read_part_into(part, &mut out)?;
944 Ok(out)
945 }
946
947 pub fn layout_part(&self) -> Result<Option<String>> {
949 self.related_part(RelKind::SlideLayout)
950 }
951
952 pub fn notes(&self) -> Result<Option<TextBody>> {
956 if let Some(deck) = self.doc.legacy() {
957 return deck.notes(self.index);
958 }
959 let Some(part) = self.notes_part()? else {
960 return Ok(None);
961 };
962 let (content, _) = self.doc.slide_part(&part)?;
963 let body_placeholder = content
964 .walk()
965 .find(|shape| {
966 shape
967 .placeholder
968 .as_ref()
969 .is_some_and(|ph| ph.kind == PlaceholderKind::Body)
970 })
971 .and_then(|shape| shape.text_body().cloned());
972 if let Some(body) = body_placeholder {
973 return Ok(Some(body));
974 }
975 let mut merged = TextBody::default();
976 for shape in content.walk() {
977 let skip = shape
978 .placeholder
979 .as_ref()
980 .is_some_and(|ph| ph.kind == PlaceholderKind::SlideImage || ph.kind.is_furniture());
981 if skip {
982 continue;
983 }
984 if let Some(body) = shape.text_body() {
985 merged.paragraphs.extend(body.paragraphs.iter().cloned());
986 }
987 }
988 Ok(Some(merged))
989 }
990
991 pub fn notes_text(&self) -> Result<Option<String>> {
993 Ok(self
994 .notes()?
995 .filter(|body| !body.is_empty())
996 .map(|body| body.text()))
997 }
998
999 pub fn images(&self) -> Result<Vec<ImageRef>> {
1001 if let Some(deck) = self.doc.legacy() {
1002 return Ok(self.legacy_images(deck));
1003 }
1004 let package = self.doc.pkg()?;
1005 let rels = self.rels()?;
1006 let mut images = Vec::new();
1007 for shape in self.content.walk() {
1008 let picture = match &shape.content {
1009 Content::Picture(picture) => picture,
1010 Content::Ole(ole) => match &ole.preview {
1011 Some(picture) => picture,
1012 None => continue,
1013 },
1014 _ => continue,
1015 };
1016 let Some(rel_id) = picture.embed.as_ref().or(picture.link.as_ref()) else {
1017 continue;
1018 };
1019 let rel = rels.get(rel_id);
1020 let part = rel
1021 .and_then(|rel| rels.resolve(rel))
1022 .filter(|part| package.has_part(part));
1023 let content_type = part
1024 .as_deref()
1025 .and_then(|part| package.content_type_of(part))
1026 .map(str::to_string);
1027 let external = rel
1028 .filter(|rel| rel.mode == TargetMode::External)
1029 .map(|rel| rel.target.clone());
1030 images.push(ImageRef {
1031 shape_id: shape.id,
1032 rel_id: rel_id.clone(),
1033 part,
1034 content_type,
1035 external,
1036 });
1037 }
1038 Ok(images)
1039 }
1040
1041 fn legacy_images(&self, deck: &LegacyDeck) -> Vec<ImageRef> {
1043 let mut images = Vec::new();
1044 for shape in self.content.walk() {
1045 let picture = match &shape.content {
1046 Content::Picture(picture) => picture,
1047 Content::Ole(ole) => match &ole.preview {
1048 Some(picture) => picture,
1049 None => continue,
1050 },
1051 _ => continue,
1052 };
1053 let Some(index) = picture.embed.as_deref() else {
1054 continue;
1055 };
1056 let content_type = index
1057 .parse::<usize>()
1058 .ok()
1059 .and_then(|index| deck.picture_content_type(index))
1060 .map(str::to_string);
1061 images.push(ImageRef {
1062 shape_id: shape.id,
1063 rel_id: index.to_string(),
1064 part: content_type.is_some().then(|| format!("Pictures/{index}")),
1065 content_type,
1066 external: None,
1067 });
1068 }
1069 images
1070 }
1071
1072 pub fn image_bytes(&self, image: &ImageRef) -> Result<Vec<u8>> {
1074 if let Some(deck) = self.doc.legacy() {
1075 let picture = image
1076 .rel_id
1077 .parse::<usize>()
1078 .ok()
1079 .and_then(|index| deck.picture(index).ok().flatten());
1080 return picture.map(|picture| picture.bytes).ok_or_else(|| {
1081 Error::MissingRelationship {
1082 part: self.part.clone(),
1083 id: image.rel_id.clone(),
1084 }
1085 });
1086 }
1087 let part = image
1088 .part
1089 .as_deref()
1090 .ok_or_else(|| Error::MissingRelationship {
1091 part: self.part.clone(),
1092 id: image.rel_id.clone(),
1093 })?;
1094 let mut out = Vec::new();
1095 self.doc.pkg()?.read_part_into(part, &mut out)?;
1096 Ok(out)
1097 }
1098
1099 pub fn hyperlink_target(&self, rel_id: &str) -> Result<Option<String>> {
1101 let rels = self.rels()?;
1102 Ok(rels.get(rel_id).map(|rel| match rel.mode {
1103 TargetMode::External => rel.target.clone(),
1104 TargetMode::Internal => rels.resolve(rel).unwrap_or_default(),
1105 }))
1106 }
1107}