use std::path::Path;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock};
use crate::cfb::{self, Compound};
use crate::chart::{parse_chart, ChartData};
use crate::comments::{parse_authors, parse_comments, Comment, CommentAuthor};
use crate::diagram::{parse_diagram, DiagramData};
use crate::error::{Error, Result};
use crate::model::{Content, PlaceholderKind, SlideContent, TextBody};
use crate::opc::{Relationships, TargetMode};
use crate::package::{Package, PackageSeed};
use crate::pml::{content_type, RelKind};
use crate::ppt::{self, LegacyDeck};
use crate::presentation::Presentation;
use crate::properties::{AppProperties, CoreProperties};
use crate::slide::{parse_slide, SlideReport};
use crate::text::{write_content_text, ExtractReport, TextOptions};
use crate::zip::{FileSource, Source};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Located {
LegacyStream,
Relationship,
ContentType,
ConventionalPath,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DocumentDefects {
pub located: Located,
pub unresolved_slides: Vec<(usize, String)>,
pub slides_recovered_from_rels: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SlideRef {
pub part: String,
pub id: Option<u32>,
pub rel_id: String,
}
struct Shared {
presentation_part: String,
presentation: Presentation,
slides: Vec<SlideRef>,
defects: DocumentDefects,
package: Option<PackageSeed>,
legacy: Option<Arc<LegacyDeck>>,
authors: OnceLock<Vec<CommentAuthor>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SlideSection {
pub name: String,
pub slides: Vec<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObjectRef {
pub shape_id: u32,
pub prog_id: Option<String>,
pub rel_id: Option<String>,
pub part: Option<String>,
pub content_type: Option<String>,
pub external: Option<String>,
}
#[derive(Clone)]
pub struct DocumentSeed {
shared: Arc<Shared>,
threads: usize,
}
impl DocumentSeed {
pub fn threads(&self) -> usize {
self.threads
}
}
pub struct Document {
package: Option<Package>,
shared: Arc<Shared>,
threads: usize,
}
fn threads_from_env() -> usize {
std::env::var("PPTXBOSS_THREADS")
.ok()
.and_then(|value| value.trim().parse().ok())
.unwrap_or(0)
}
impl Document {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let source = FileSource::open(path)?;
Self::from_source(Arc::new(source))
}
pub fn from_source(source: Arc<dyn Source>) -> Result<Self> {
let mut head = [0u8; 8];
if source.len() >= 8 {
source.read_at(0, &mut head)?;
}
if cfb::is_compound(&head) || head.starts_with(&[0xd0, 0xcf, 0x11, 0xe0]) {
let compound = Compound::open_source(source)?;
if compound.has_stream("EncryptionInfo") || compound.has_stream("EncryptedPackage") {
return Err(Error::Encrypted("package encrypted with a password".into()));
}
if ppt::is_presentation(&compound) {
return Ok(Self::from_legacy(ppt::open_compound(&compound)?));
}
return Err(Error::NoPresentationStream);
}
Self::from_package(Package::from_source(source)?)
}
pub fn from_legacy(deck: Arc<LegacyDeck>) -> Self {
let slide_ids = deck.slide_ids();
let presentation = Presentation {
slides: slide_ids
.iter()
.map(|id| crate::presentation::SlideId {
id: Some(*id),
rel_id: String::new(),
offset: 0,
})
.collect(),
slide_size: deck
.slide_size
.map(|(cx, cy)| crate::presentation::SlideSize {
cx,
cy,
kind: deck.slide_size_kind.map(str::to_string),
}),
notes_size: deck.notes_size,
first_slide_num: deck.first_slide_number,
root_ok: true,
..Presentation::default()
};
let slides = slide_ids
.iter()
.enumerate()
.map(|(index, id)| SlideRef {
part: format!("PowerPoint Document/slide{}", index + 1),
id: Some(*id),
rel_id: String::new(),
})
.collect();
let shared = Arc::new(Shared {
presentation_part: "PowerPoint Document".to_string(),
presentation,
slides,
defects: DocumentDefects {
located: Located::LegacyStream,
unresolved_slides: Vec::new(),
slides_recovered_from_rels: deck.slides_recovered_by_scan,
},
package: None,
legacy: Some(deck),
authors: OnceLock::new(),
});
Self {
package: None,
shared,
threads: threads_from_env(),
}
}
pub fn load(bytes: Vec<u8>) -> Result<Self> {
Self::from_source(Arc::new(bytes))
}
pub fn from_package(package: Package) -> Result<Self> {
let (presentation_part, located) = locate_presentation(&package)?;
let xml = package.read_part(&presentation_part)?;
let presentation = Presentation::parse(&xml).map_err(|err| Error::Xml {
part: presentation_part.clone(),
offset: err.offset,
msg: err.msg.to_string(),
})?;
let rels = package.rels(&presentation_part)?;
let mut defects = DocumentDefects {
located,
unresolved_slides: Vec::new(),
slides_recovered_from_rels: false,
};
let mut slides = Vec::with_capacity(presentation.slides.len());
for (index, slide_id) in presentation.slides.iter().enumerate() {
match rels
.target_of(&slide_id.rel_id)
.filter(|part| package.has_part(part))
{
Some(part) => slides.push(SlideRef {
part,
id: slide_id.id,
rel_id: slide_id.rel_id.clone(),
}),
None => defects
.unresolved_slides
.push((index, slide_id.rel_id.clone())),
}
}
if slides.is_empty() && presentation.slides.is_empty() {
for rel in rels.iter().filter(|rel| {
rel.mode == TargetMode::Internal && RelKind::of(&rel.rel_type) == RelKind::Slide
}) {
if let Some(part) = rels.resolve(rel).filter(|part| package.has_part(part)) {
slides.push(SlideRef {
part,
id: None,
rel_id: rel.id.clone(),
});
}
}
defects.slides_recovered_from_rels = !slides.is_empty();
}
let shared = Arc::new(Shared {
presentation_part,
presentation,
slides,
defects,
package: Some(package.seed()),
legacy: None,
authors: OnceLock::new(),
});
Ok(Self {
package: Some(package),
shared,
threads: threads_from_env(),
})
}
pub fn core_properties(&self) -> Result<Option<CoreProperties>> {
let Some(part) = self.metadata_part(RelKind::CoreProperties, "/docProps/core.xml")? else {
return Ok(None);
};
let xml = self.pkg()?.read_part(&part)?;
CoreProperties::parse(&xml)
.map(Some)
.map_err(|err| xml_error(&part, err))
}
pub fn app_properties(&self) -> Result<Option<AppProperties>> {
let Some(part) = self.metadata_part(RelKind::ExtendedProperties, "/docProps/app.xml")?
else {
return Ok(None);
};
let xml = self.pkg()?.read_part(&part)?;
AppProperties::parse(&xml)
.map(Some)
.map_err(|err| xml_error(&part, err))
}
fn metadata_part(&self, kind: RelKind, conventional: &str) -> Result<Option<String>> {
let Some(package) = self.package.as_ref() else {
return Ok(None);
};
let rels = package.package_rels()?;
let by_rel = rels
.iter()
.filter(|rel| RelKind::of(&rel.rel_type) == kind)
.find_map(|rel| rels.resolve(rel))
.filter(|part| package.has_part(part));
if by_rel.is_some() {
return Ok(by_rel);
}
Ok(package
.has_part(conventional)
.then(|| conventional.to_string()))
}
pub fn sections(&self) -> Vec<SlideSection> {
self.shared
.presentation
.sections
.iter()
.map(|section| SlideSection {
name: section.name.clone(),
slides: section
.slide_ids
.iter()
.filter_map(|id| {
self.shared
.slides
.iter()
.position(|slide| slide.id == Some(*id))
})
.collect(),
})
.collect()
}
pub fn comment_authors(&self) -> &[CommentAuthor] {
self.shared.authors.get_or_init(|| {
let Some(package) = self.package.as_ref() else {
return Vec::new();
};
let Ok(rels) = package.rels(&self.shared.presentation_part) else {
return Vec::new();
};
let mut authors = Vec::new();
for rel in rels.iter() {
let kind = RelKind::of(&rel.rel_type);
if kind != RelKind::CommentAuthors && kind != RelKind::Authors {
continue;
}
let Some(part) = rels.resolve(rel) else {
continue;
};
let Ok(xml) = package.read_part(&part) else {
continue;
};
if let Ok(mut parsed) = parse_authors(&xml) {
authors.append(&mut parsed);
}
}
authors
})
}
pub fn threads(&self) -> usize {
self.threads
}
pub fn set_threads(&mut self, threads: usize) {
self.threads = threads;
}
pub fn with_threads(mut self, threads: usize) -> Self {
self.threads = threads;
self
}
pub fn seed(&self) -> DocumentSeed {
DocumentSeed {
shared: Arc::clone(&self.shared),
threads: self.threads,
}
}
pub fn from_seed(seed: DocumentSeed) -> Self {
Self {
package: seed.shared.package.clone().map(Package::from_seed),
shared: seed.shared,
threads: seed.threads,
}
}
pub fn package(&self) -> Option<&Package> {
self.package.as_ref()
}
pub fn legacy(&self) -> Option<&Arc<LegacyDeck>> {
self.shared.legacy.as_ref()
}
pub fn is_legacy(&self) -> bool {
self.shared.legacy.is_some()
}
fn pkg(&self) -> Result<&Package> {
self.package.as_ref().ok_or_else(|| {
Error::Unsupported("a legacy binary presentation has no package parts".into())
})
}
pub fn presentation(&self) -> &Presentation {
&self.shared.presentation
}
pub fn presentation_part(&self) -> &str {
&self.shared.presentation_part
}
pub fn defects(&self) -> &DocumentDefects {
&self.shared.defects
}
pub fn slide_refs(&self) -> &[SlideRef] {
&self.shared.slides
}
pub fn slide_count(&self) -> usize {
self.shared.slides.len()
}
pub fn slide(&self, index: usize) -> Result<Slide<'_>> {
let slide_ref = self
.shared
.slides
.get(index)
.ok_or(Error::SlideNotFound(index))?;
if let Some(deck) = &self.shared.legacy {
let slide = deck.slide(index)?;
return Ok(Slide {
doc: self,
index,
part: slide_ref.part.clone(),
content: slide.content,
report: SlideReport::default(),
});
}
let xml = self.pkg()?.read_part(&slide_ref.part)?;
let (content, report) = parse_slide(&xml).map_err(|err| Error::Xml {
part: slide_ref.part.clone(),
offset: err.offset,
msg: err.msg.to_string(),
})?;
Ok(Slide {
doc: self,
index,
part: slide_ref.part.clone(),
content,
report,
})
}
pub fn slides(&self) -> impl Iterator<Item = Result<Slide<'_>>> + '_ {
(0..self.slide_count()).map(move |index| self.slide(index))
}
pub fn slide_part(&self, part: &str) -> Result<(SlideContent, SlideReport)> {
let xml = self.pkg()?.read_part(part)?;
parse_slide(&xml).map_err(|err| Error::Xml {
part: part.to_string(),
offset: err.offset,
msg: err.msg.to_string(),
})
}
fn worker_count(&self, count: usize) -> usize {
if self.threads == 0 && self.is_legacy() {
return 1;
}
let limit = match self.threads {
0 => std::thread::available_parallelism().map_or(1, |n| n.get()),
limit => limit,
};
limit.min(count)
}
pub fn map_slides<T, F>(&self, f: F) -> Vec<T>
where
T: Send,
F: Fn(Result<Slide<'_>>) -> T + Sync,
{
self.map_slides_at(&self.all_slides(), f)
}
pub fn all_slides(&self) -> Vec<usize> {
(0..self.slide_count()).collect()
}
pub fn map_slides_at<T, F>(&self, indices: &[usize], f: F) -> Vec<T>
where
T: Send,
F: Fn(Result<Slide<'_>>) -> T + Sync,
{
let count = indices.len();
let workers = self.worker_count(count);
if workers <= 1 {
return indices.iter().map(|&index| f(self.slide(index))).collect();
}
let seed = self.seed();
let next = AtomicUsize::new(0);
let mut results: Vec<(usize, T)> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..workers)
.map(|_| {
scope.spawn(|| {
let doc = Document::from_seed(seed.clone());
let mut mine = Vec::new();
loop {
let position = next.fetch_add(1, Ordering::Relaxed);
if position >= count {
return mine;
}
mine.push((position, f(doc.slide(indices[position]))));
}
})
})
.collect();
handles
.into_iter()
.flat_map(|handle| match handle.join() {
Ok(mine) => mine,
Err(payload) => std::panic::resume_unwind(payload),
})
.collect()
});
results.sort_by_key(|(position, _)| *position);
results.into_iter().map(|(_, value)| value).collect()
}
pub fn slide_texts(&self, options: &TextOptions) -> (Vec<String>, ExtractReport) {
self.slide_texts_at(&self.all_slides(), options)
}
pub fn slide_texts_at(
&self,
indices: &[usize],
options: &TextOptions,
) -> (Vec<String>, ExtractReport) {
let results = self.map_slides_at(indices, |slide| match slide {
Ok(slide) => {
let mut report = ExtractReport::default();
let text = slide.text_reporting(options, &mut report);
(text, report)
}
Err(err) => {
let mut report = ExtractReport::default();
report.failed_slides.push((0, err.to_string()));
(String::new(), report)
}
});
let mut report = ExtractReport::default();
let mut texts = Vec::with_capacity(results.len());
for (&index, (text, slide_report)) in indices.iter().zip(results) {
report.merge(index, slide_report);
texts.push(text);
}
(texts, report)
}
pub fn text(&self) -> String {
self.text_reporting(&TextOptions::default()).0
}
pub fn text_reporting(&self, options: &TextOptions) -> (String, ExtractReport) {
let (texts, report) = self.slide_texts(options);
let mut out = String::with_capacity(texts.iter().map(|text| text.len() + 2).sum());
let mut first = true;
for text in texts.iter().filter(|text| !text.is_empty()) {
if !first {
out.push_str("\n\n");
}
first = false;
out.push_str(text);
}
(out, report)
}
}
fn write_comment_line(comment: &Comment, out: &mut String) {
if comment.reply {
out.push_str(" ");
}
out.push_str(match comment.reply {
true => "[reply] ",
false => "[comment] ",
});
if let Some(author) = &comment.author {
out.push_str(author);
out.push_str(": ");
}
out.push_str(comment.text.trim());
}
fn xml_error(part: &str, err: crate::xml::XmlError) -> Error {
Error::Xml {
part: part.to_string(),
offset: err.offset,
msg: err.msg.to_string(),
}
}
fn locate_presentation(package: &Package) -> Result<(String, Located)> {
let rels = package.package_rels()?;
let by_rel = rels
.iter()
.filter(|rel| RelKind::of(&rel.rel_type) == RelKind::OfficeDocument)
.filter_map(|rel| rels.resolve(rel))
.find(|part| package.has_part(part));
if let Some(part) = by_rel {
return Ok((part, Located::Relationship));
}
let by_type = package
.parts()
.iter()
.find(|part| {
package
.content_type_of(&part.name)
.is_some_and(content_type::is_presentation_main)
})
.map(|part| part.name.clone());
if let Some(part) = by_type {
return Ok((part, Located::ContentType));
}
if package.has_part("/ppt/presentation.xml") {
return Ok((
"/ppt/presentation.xml".to_string(),
Located::ConventionalPath,
));
}
Err(Error::NotAPresentation)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImageRef {
pub shape_id: u32,
pub rel_id: String,
pub part: Option<String>,
pub content_type: Option<String>,
pub external: Option<String>,
}
pub struct Slide<'d> {
doc: &'d Document,
pub index: usize,
pub part: String,
pub content: SlideContent,
pub report: SlideReport,
}
impl<'d> Slide<'d> {
pub fn number(&self) -> usize {
self.index + 1
}
pub fn document(&self) -> &'d Document {
self.doc
}
pub fn is_hidden(&self) -> bool {
!self.content.show
}
pub fn rels(&self) -> Result<Rc<Relationships>> {
match self.doc.package.as_ref() {
Some(package) => package.rels(&self.part),
None => Ok(Rc::new(Relationships::empty(&self.part))),
}
}
pub fn title(&self) -> Option<String> {
self.content.title()
}
pub fn text(&self) -> String {
let mut report = ExtractReport::default();
self.text_reporting(&TextOptions::default(), &mut report)
}
pub(crate) fn merge_parse_report(&self, report: &mut ExtractReport) {
report.unknown_graphics += self.report.unknown_graphics.len() as u32;
for uri in &self.report.unknown_graphics {
if !report.unknown_graphic_uris.contains(uri) {
report.unknown_graphic_uris.push(uri.clone());
}
}
report.unknown_elements += self.report.unknown_elements;
}
fn frame_text(
&self,
shape: &crate::model::Shape,
options: &TextOptions,
report: &mut ExtractReport,
) -> Option<String> {
let result = match &shape.content {
Content::Chart(Some(rel_id)) if options.charts => self.chart(rel_id).map(|chart| {
let mut out = String::new();
chart.write_text(&options.cell_separator, &mut out);
out
}),
Content::Diagram(Some(rel_id)) if options.diagrams => {
self.diagram(rel_id).map(|diagram| {
let mut out = String::new();
diagram.write_text(&mut out);
out
})
}
_ => return None,
};
match result {
Ok(text) => Some(text).filter(|text| !text.is_empty()),
Err(err) => {
report.failed_frames.push((self.index, err.to_string()));
None
}
}
}
pub fn chart(&self, rel_id: &str) -> Result<ChartData> {
let part = self.frame_part(rel_id)?;
let xml = self.doc.pkg()?.read_part(&part)?;
parse_chart(&xml).map_err(|err| xml_error(&part, err))
}
pub fn diagram(&self, rel_id: &str) -> Result<DiagramData> {
let part = self.frame_part(rel_id)?;
let xml = self.doc.pkg()?.read_part(&part)?;
parse_diagram(&xml).map_err(|err| xml_error(&part, err))
}
fn frame_part(&self, rel_id: &str) -> Result<String> {
let rels = self.rels()?;
let package = self.doc.pkg()?;
rels.target_of(rel_id)
.filter(|part| package.has_part(part))
.ok_or_else(|| Error::MissingRelationship {
part: self.part.clone(),
id: rel_id.to_string(),
})
}
pub fn charts(&self) -> Result<Vec<(u32, ChartData)>> {
let mut charts = Vec::new();
for shape in self.content.walk() {
if let Content::Chart(Some(rel_id)) = &shape.content {
charts.push((shape.id, self.chart(rel_id)?));
}
}
Ok(charts)
}
pub fn diagrams(&self) -> Result<Vec<(u32, DiagramData)>> {
let mut diagrams = Vec::new();
for shape in self.content.walk() {
if let Content::Diagram(Some(rel_id)) = &shape.content {
diagrams.push((shape.id, self.diagram(rel_id)?));
}
}
Ok(diagrams)
}
pub fn text_reporting(&self, options: &TextOptions, report: &mut ExtractReport) -> String {
self.merge_parse_report(report);
if self.is_hidden() && !options.hidden_slides {
report.hidden_slides_skipped += 1;
return String::new();
}
let mut out = String::new();
write_content_text(
&self.content,
options,
&mut |shape| self.frame_text(shape, options, report),
&mut out,
);
if options.notes {
match self.notes() {
Ok(Some(notes)) if !notes.is_empty() => {
if !out.is_empty() {
out.push('\n');
}
notes.write_text(&mut out);
}
Ok(_) => {}
Err(err) => report.failed_notes.push((self.index, err.to_string())),
}
}
if options.comments {
match self.comments() {
Ok(comments) => {
for comment in comments {
if !out.is_empty() {
out.push('\n');
}
write_comment_line(&comment, &mut out);
}
}
Err(err) => report.failed_comments.push((self.index, err.to_string())),
}
}
out
}
fn related_part(&self, kind: RelKind) -> Result<Option<String>> {
let rels = self.rels()?;
let found = rels
.iter()
.filter(|rel| RelKind::of(&rel.rel_type) == kind)
.find_map(|rel| rels.resolve(rel));
Ok(found)
}
pub fn notes_part(&self) -> Result<Option<String>> {
if let Some(deck) = self.doc.legacy() {
return Ok(deck
.has_notes(self.index)
.then(|| format!("PowerPoint Document/notes{}", self.index + 1)));
}
self.related_part(RelKind::NotesSlide)
}
pub fn comments_part(&self) -> Result<Option<String>> {
match self.related_part(RelKind::ModernComments)? {
Some(part) => Ok(Some(part)),
None => self.related_part(RelKind::Comments),
}
}
pub fn comments(&self) -> Result<Vec<Comment>> {
let Some(part) = self.comments_part()? else {
return Ok(Vec::new());
};
let xml = self.doc.pkg()?.read_part(&part)?;
parse_comments(&xml, self.doc.comment_authors()).map_err(|err| xml_error(&part, err))
}
pub fn objects(&self) -> Result<Vec<ObjectRef>> {
let rels = self.rels()?;
let package = self.doc.package.as_ref();
let mut objects = Vec::new();
for shape in self.content.walk() {
let Content::Ole(ole) = &shape.content else {
continue;
};
let rel = ole.rel_id.as_deref().and_then(|id| rels.get(id));
let part = rel
.and_then(|rel| rels.resolve(rel))
.filter(|part| package.is_some_and(|package| package.has_part(part)));
let content_type = part
.as_deref()
.and_then(|part| package.and_then(|package| package.content_type_of(part)))
.map(str::to_string);
objects.push(ObjectRef {
shape_id: shape.id,
prog_id: ole.prog_id.clone(),
rel_id: ole.rel_id.clone(),
part,
content_type,
external: rel
.filter(|rel| rel.mode == TargetMode::External)
.map(|rel| rel.target.clone()),
});
}
Ok(objects)
}
pub fn object_bytes(&self, object: &ObjectRef) -> Result<Vec<u8>> {
let part = object
.part
.as_deref()
.ok_or_else(|| Error::MissingRelationship {
part: self.part.clone(),
id: object.rel_id.clone().unwrap_or_default(),
})?;
let mut out = Vec::new();
self.doc.pkg()?.read_part_into(part, &mut out)?;
Ok(out)
}
pub fn layout_part(&self) -> Result<Option<String>> {
self.related_part(RelKind::SlideLayout)
}
pub fn notes(&self) -> Result<Option<TextBody>> {
if let Some(deck) = self.doc.legacy() {
return deck.notes(self.index);
}
let Some(part) = self.notes_part()? else {
return Ok(None);
};
let (content, _) = self.doc.slide_part(&part)?;
let body_placeholder = content
.walk()
.find(|shape| {
shape
.placeholder
.as_ref()
.is_some_and(|ph| ph.kind == PlaceholderKind::Body)
})
.and_then(|shape| shape.text_body().cloned());
if let Some(body) = body_placeholder {
return Ok(Some(body));
}
let mut merged = TextBody::default();
for shape in content.walk() {
let skip = shape
.placeholder
.as_ref()
.is_some_and(|ph| ph.kind == PlaceholderKind::SlideImage || ph.kind.is_furniture());
if skip {
continue;
}
if let Some(body) = shape.text_body() {
merged.paragraphs.extend(body.paragraphs.iter().cloned());
}
}
Ok(Some(merged))
}
pub fn notes_text(&self) -> Result<Option<String>> {
Ok(self
.notes()?
.filter(|body| !body.is_empty())
.map(|body| body.text()))
}
pub fn images(&self) -> Result<Vec<ImageRef>> {
if let Some(deck) = self.doc.legacy() {
return Ok(self.legacy_images(deck));
}
let package = self.doc.pkg()?;
let rels = self.rels()?;
let mut images = Vec::new();
for shape in self.content.walk() {
let picture = match &shape.content {
Content::Picture(picture) => picture,
Content::Ole(ole) => match &ole.preview {
Some(picture) => picture,
None => continue,
},
_ => continue,
};
let Some(rel_id) = picture.embed.as_ref().or(picture.link.as_ref()) else {
continue;
};
let rel = rels.get(rel_id);
let part = rel
.and_then(|rel| rels.resolve(rel))
.filter(|part| package.has_part(part));
let content_type = part
.as_deref()
.and_then(|part| package.content_type_of(part))
.map(str::to_string);
let external = rel
.filter(|rel| rel.mode == TargetMode::External)
.map(|rel| rel.target.clone());
images.push(ImageRef {
shape_id: shape.id,
rel_id: rel_id.clone(),
part,
content_type,
external,
});
}
Ok(images)
}
fn legacy_images(&self, deck: &LegacyDeck) -> Vec<ImageRef> {
let mut images = Vec::new();
for shape in self.content.walk() {
let picture = match &shape.content {
Content::Picture(picture) => picture,
Content::Ole(ole) => match &ole.preview {
Some(picture) => picture,
None => continue,
},
_ => continue,
};
let Some(index) = picture.embed.as_deref() else {
continue;
};
let content_type = index
.parse::<usize>()
.ok()
.and_then(|index| deck.picture_content_type(index))
.map(str::to_string);
images.push(ImageRef {
shape_id: shape.id,
rel_id: index.to_string(),
part: content_type.is_some().then(|| format!("Pictures/{index}")),
content_type,
external: None,
});
}
images
}
pub fn image_bytes(&self, image: &ImageRef) -> Result<Vec<u8>> {
if let Some(deck) = self.doc.legacy() {
let picture = image
.rel_id
.parse::<usize>()
.ok()
.and_then(|index| deck.picture(index).ok().flatten());
return picture.map(|picture| picture.bytes).ok_or_else(|| {
Error::MissingRelationship {
part: self.part.clone(),
id: image.rel_id.clone(),
}
});
}
let part = image
.part
.as_deref()
.ok_or_else(|| Error::MissingRelationship {
part: self.part.clone(),
id: image.rel_id.clone(),
})?;
let mut out = Vec::new();
self.doc.pkg()?.read_part_into(part, &mut out)?;
Ok(out)
}
pub fn hyperlink_target(&self, rel_id: &str) -> Result<Option<String>> {
let rels = self.rels()?;
Ok(rels.get(rel_id).map(|rel| match rel.mode {
TargetMode::External => rel.target.clone(),
TargetMode::Internal => rels.resolve(rel).unwrap_or_default(),
}))
}
}