use std::collections::HashMap;
use std::io::{Read, Seek};
use drawing::{Fill, Geometry, PresetShape, ShapeProperties, TextAnchor, TextBody};
use opc::{Package, Relationships, TargetMode};
use xml_core::{BytesStart, Event, Reader};
use crate::error::{Error, Result};
use crate::model::{
AutoShape, ColorScheme, Connector, CustomPropertyValue, DEFAULT_SLIDE_HEIGHT_EMU,
DEFAULT_SLIDE_WIDTH_EMU, DocumentProperties, EmbeddedFont, FontScheme, MediaFormat, Picture,
PictureFormat, Placeholder, PlaceholderKind, Presentation, Shape, ShapeConnection, ShapeGroup,
Slide, SlideChart, SlideComment, SlideHyperlinkTarget, SlideMedia, SlideTable, SlideTableStyle,
TableCell, TableCellProperties, TableRow, TableStylePart, Theme,
};
const OFFICE_DOCUMENT_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
const NOTES_SLIDE_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
const SLIDE_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
const SLIDE_MASTER_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
const THEME_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
const COMMENT_AUTHORS_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors";
const COMMENT_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
const TABLE_NAMESPACE: &str = "http://schemas.openxmlformats.org/drawingml/2006/table";
const CORE_PROPERTIES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
const EXTENDED_PROPERTIES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
const CUSTOM_PROPERTIES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
const TABLE_STYLES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles";
const TABLE_STYLE_GUID: &str = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
impl Presentation {
pub fn read_from<R: Read + Seek>(reader: R) -> Result<Self> {
let package = Package::read_from(reader)?;
let main_relationship = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == OFFICE_DOCUMENT_RELATIONSHIP_TYPE)
.ok_or(Error::MissingMainPresentation)?;
let presentation_part_name = match main_relationship.target_mode {
TargetMode::Internal => format!("/{}", main_relationship.target),
TargetMode::External => return Err(Error::MissingMainPresentation),
};
let presentation_part = package
.part(&presentation_part_name)
.ok_or(Error::MissingMainPresentation)?;
let xml = std::str::from_utf8(&presentation_part.data)
.map_err(|error| Error::InvalidUtf8(presentation_part_name.clone(), error))?;
let parsed = parse_presentation_xml(xml)?;
let mut presentation = Presentation::new();
presentation.slide_width_emu = parsed.slide_width_emu;
presentation.slide_height_emu = parsed.slide_height_emu;
presentation.theme = read_presentation_theme(&package, &presentation_part.relationships)?;
if let Some(relationship) = presentation_part
.relationships
.iter()
.find(|relationship| relationship.rel_type == TABLE_STYLES_RELATIONSHIP_TYPE)
{
let part_name = format!("/ppt/{}", relationship.target);
if let Some(part) = package.part(&part_name) {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
presentation.table_styles = parse_table_styles_xml(xml)?;
}
}
for parsed_font in &parsed.embedded_fonts {
presentation.embedded_fonts.push(EmbeddedFont {
typeface: parsed_font.typeface.clone(),
regular: resolve_embedded_font_variant(
&package,
&presentation_part.relationships,
&parsed_font.regular,
),
bold: resolve_embedded_font_variant(
&package,
&presentation_part.relationships,
&parsed_font.bold,
),
italic: resolve_embedded_font_variant(
&package,
&presentation_part.relationships,
&parsed_font.italic,
),
bold_italic: resolve_embedded_font_variant(
&package,
&presentation_part.relationships,
&parsed_font.bold_italic,
),
});
}
presentation.properties = read_document_properties(&package)?;
let comment_authors = match presentation_part
.relationships
.iter()
.find(|relationship| relationship.rel_type == COMMENT_AUTHORS_RELATIONSHIP_TYPE)
{
Some(relationship) => {
let part_name = format!("/ppt/{}", relationship.target);
match package.part(&part_name) {
Some(part) => {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
parse_comment_authors_xml(xml)?
}
None => HashMap::new(),
}
}
None => HashMap::new(),
};
for relationship_id in &parsed.slide_relationship_ids {
let relationship = presentation_part
.relationships
.by_id(relationship_id)
.ok_or_else(|| Error::MissingSlideRelationship(relationship_id.clone()))?;
let slide_part_name = format!("/ppt/{}", relationship.target);
let slide_part = package
.part(&slide_part_name)
.ok_or_else(|| Error::MissingSlidePart(slide_part_name.clone()))?;
let slide_xml = std::str::from_utf8(&slide_part.data)
.map_err(|error| Error::InvalidUtf8(slide_part_name.clone(), error))?;
let context = SlideContext {
slide_part_name: &slide_part_name,
relationships: &slide_part.relationships,
package: &package,
};
let mut slide = parse_slide_xml(slide_xml, &context)?;
if let Some(notes_relationship) = slide_part
.relationships
.iter()
.find(|relationship| relationship.rel_type == NOTES_SLIDE_RELATIONSHIP_TYPE)
{
let notes_part_name =
resolve_relative_target(&slide_part_name, ¬es_relationship.target);
let notes_part = package
.part(¬es_part_name)
.ok_or_else(|| Error::MissingNotesPart(notes_part_name.clone()))?;
let notes_xml = std::str::from_utf8(¬es_part.data)
.map_err(|error| Error::InvalidUtf8(notes_part_name.clone(), error))?;
slide.notes = Some(parse_notes_slide_xml(notes_xml)?);
}
if let Some(comments_relationship) = slide_part
.relationships
.iter()
.find(|relationship| relationship.rel_type == COMMENT_RELATIONSHIP_TYPE)
{
let comments_part_name =
resolve_relative_target(&slide_part_name, &comments_relationship.target);
if let Some(comments_part) = package.part(&comments_part_name) {
let comments_xml = std::str::from_utf8(&comments_part.data)
.map_err(|error| Error::InvalidUtf8(comments_part_name.clone(), error))?;
slide.comments = parse_comments_xml(comments_xml, &comment_authors)?;
}
}
presentation.slides.push(slide);
}
Ok(presentation)
}
}
struct ParsedPresentation {
slide_width_emu: i64,
slide_height_emu: i64,
slide_relationship_ids: Vec<String>,
embedded_fonts: Vec<ParsedEmbeddedFont>,
}
struct ParsedEmbeddedFont {
typeface: String,
regular: Option<String>,
bold: Option<String>,
italic: Option<String>,
bold_italic: Option<String>,
}
fn parse_presentation_xml(xml: &str) -> Result<ParsedPresentation> {
let mut slide_width_emu = DEFAULT_SLIDE_WIDTH_EMU;
let mut slide_height_emu = DEFAULT_SLIDE_HEIGHT_EMU;
let mut slide_relationship_ids = Vec::new();
let mut embedded_fonts = Vec::new();
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Empty(start) if start.local_name().as_ref() == b"sldId" => {
if let Some(relationship_id) = attribute_by_qname(&start, b"r:id") {
slide_relationship_ids.push(relationship_id);
}
}
Event::Empty(start) if start.local_name().as_ref() == b"sldSz" => {
if let Some(value) =
attribute_by_qname(&start, b"cx").and_then(|value| value.parse().ok())
{
slide_width_emu = value;
}
if let Some(value) =
attribute_by_qname(&start, b"cy").and_then(|value| value.parse().ok())
{
slide_height_emu = value;
}
}
Event::Start(start) if start.local_name().as_ref() == b"embeddedFontLst" => {
embedded_fonts = parse_embedded_font_list(&mut reader)?;
}
_ => {}
}
}
Ok(ParsedPresentation {
slide_width_emu,
slide_height_emu,
slide_relationship_ids,
embedded_fonts,
})
}
fn parse_embedded_font_list(reader: &mut Reader) -> Result<Vec<ParsedEmbeddedFont>> {
let mut fonts = Vec::new();
loop {
match reader.read_event()? {
Event::Start(start) if start.local_name().as_ref() == b"embeddedFont" => {
fonts.push(parse_embedded_font(reader)?);
}
Event::End(end) if end.local_name().as_ref() == b"embeddedFontLst" => break,
Event::Eof => break,
_ => {}
}
}
Ok(fonts)
}
fn parse_embedded_font(reader: &mut Reader) -> Result<ParsedEmbeddedFont> {
let mut typeface = String::new();
let mut regular = None;
let mut bold = None;
let mut italic = None;
let mut bold_italic = None;
loop {
match reader.read_event()? {
Event::Empty(start) if start.local_name().as_ref() == b"font" => {
typeface = attribute_by_qname(&start, b"typeface").unwrap_or_default();
}
Event::Empty(start) if start.local_name().as_ref() == b"regular" => {
regular = attribute_by_qname(&start, b"r:id");
}
Event::Empty(start) if start.local_name().as_ref() == b"bold" => {
bold = attribute_by_qname(&start, b"r:id");
}
Event::Empty(start) if start.local_name().as_ref() == b"italic" => {
italic = attribute_by_qname(&start, b"r:id");
}
Event::Empty(start) if start.local_name().as_ref() == b"boldItalic" => {
bold_italic = attribute_by_qname(&start, b"r:id");
}
Event::Start(_) => skip_subtree(reader)?,
Event::End(end) if end.local_name().as_ref() == b"embeddedFont" => break,
Event::Eof => break,
_ => {}
}
}
Ok(ParsedEmbeddedFont {
typeface,
regular,
bold,
italic,
bold_italic,
})
}
struct SlideContext<'a> {
slide_part_name: &'a str,
relationships: &'a Relationships,
package: &'a Package,
}
fn resolve_relative_target(base_part_name: &str, target: &str) -> String {
let base_directory = match base_part_name.rfind('/') {
Some(index) => &base_part_name[..index],
None => "",
};
let mut segments: Vec<&str> = base_directory
.split('/')
.filter(|segment| !segment.is_empty())
.collect();
for segment in target.split('/') {
match segment {
"" | "." => {}
".." => {
segments.pop();
}
other => segments.push(other),
}
}
format!("/{}", segments.join("/"))
}
fn parse_slide_xml(xml: &str, context: &SlideContext) -> Result<Slide> {
let mut slide = Slide::new();
let mut reader = Reader::from_xml_str(xml);
let content = parse_shapes_until_eof(&mut reader, context)?;
slide.shapes = content.shapes;
slide.background = content.background;
slide.hidden = content.hidden;
slide.name = content.name;
slide.hide_master_graphics = content.hide_master_graphics;
Ok(slide)
}
struct ParsedSlideContent {
shapes: Vec<Shape>,
background: Option<Fill>,
hidden: bool,
name: Option<String>,
hide_master_graphics: bool,
}
fn parse_shapes_until_eof(
reader: &mut Reader,
context: &SlideContext,
) -> Result<ParsedSlideContent> {
let mut shapes = Vec::new();
let mut background = None;
let mut hidden = false;
let mut name = None;
let mut hide_master_graphics = false;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"sld" => {
hidden = attribute_by_qname(&start, b"show").as_deref() == Some("0");
hide_master_graphics =
attribute_by_qname(&start, b"showMasterSp").as_deref() == Some("0");
}
Event::Start(start) if start.local_name().as_ref() == b"cSld" => {
name = attribute_by_qname(&start, b"name").filter(|value| !value.is_empty());
}
Event::Start(start) if start.local_name().as_ref() == b"bg" => {
background = parse_slide_background(reader)?;
}
Event::Start(start) if start.local_name().as_ref() == b"sp" => {
shapes.push(Shape::AutoShape(parse_auto_shape(reader, Some(context))?));
}
Event::Start(start) if start.local_name().as_ref() == b"pic" => {
shapes.push(parse_pic_shape(reader, context)?);
}
Event::Start(start) if start.local_name().as_ref() == b"graphicFrame" => {
shapes.push(parse_graphic_frame_shape(reader, context)?);
}
Event::Start(start) if start.local_name().as_ref() == b"grpSp" => {
shapes.push(Shape::Group(parse_group_shape(reader, context)?));
}
Event::Start(start) if start.local_name().as_ref() == b"cxnSp" => {
shapes.push(Shape::Connector(parse_connector(reader)?));
}
_ => {}
}
}
Ok(ParsedSlideContent {
shapes,
background,
hidden,
name,
hide_master_graphics,
})
}
fn parse_slide_background(reader: &mut Reader) -> Result<Option<Fill>> {
let mut fill = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"bg" => break,
Event::Start(start) if start.local_name().as_ref() == b"bgPr" => {
fill = drawing::read_fill(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(fill)
}
fn parse_group_shape(reader: &mut Reader, context: &SlideContext) -> Result<ShapeGroup> {
let mut id: u32 = 0;
let mut name = String::new();
let mut offset_emu = (0i64, 0i64);
let mut extent_emu = (0i64, 0i64);
let mut child_offset_emu = (0i64, 0i64);
let mut child_extent_emu = (0i64, 0i64);
let mut rotation_60000ths: i32 = 0;
let mut flip_horizontal = false;
let mut flip_vertical = false;
let mut shapes = Vec::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"grpSp" => break,
Event::Start(start) if start.local_name().as_ref() == b"nvGrpSpPr" => {}
Event::End(end) if end.local_name().as_ref() == b"nvGrpSpPr" => {}
Event::Empty(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
}
Event::Start(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
skip_subtree(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"cNvGrpSpPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"cNvGrpSpPr" => {
skip_subtree(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"nvPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"nvPr" => skip_subtree(reader)?,
Event::Start(start) if start.local_name().as_ref() == b"grpSpPr" => {}
Event::End(end) if end.local_name().as_ref() == b"grpSpPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"xfrm" => {
rotation_60000ths = attribute_by_qname(&start, b"rot")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
flip_horizontal = attribute_by_qname(&start, b"flipH").as_deref() == Some("1");
flip_vertical = attribute_by_qname(&start, b"flipV").as_deref() == Some("1");
let transform = parse_group_transform(reader)?;
offset_emu = transform.offset;
extent_emu = transform.extent;
child_offset_emu = transform.child_offset;
child_extent_emu = transform.child_extent;
}
Event::Start(start) if start.local_name().as_ref() == b"sp" => {
shapes.push(Shape::AutoShape(parse_auto_shape(reader, Some(context))?));
}
Event::Start(start) if start.local_name().as_ref() == b"pic" => {
shapes.push(parse_pic_shape(reader, context)?);
}
Event::Start(start) if start.local_name().as_ref() == b"graphicFrame" => {
shapes.push(parse_graphic_frame_shape(reader, context)?);
}
Event::Start(start) if start.local_name().as_ref() == b"grpSp" => {
shapes.push(Shape::Group(parse_group_shape(reader, context)?));
}
Event::Start(start) if start.local_name().as_ref() == b"cxnSp" => {
shapes.push(Shape::Connector(parse_connector(reader)?));
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(ShapeGroup {
id,
name,
offset_emu,
extent_emu,
child_offset_emu,
child_extent_emu,
rotation_60000ths,
flip_horizontal,
flip_vertical,
shapes,
})
}
struct GroupTransform {
offset: (i64, i64),
extent: (i64, i64),
child_offset: (i64, i64),
child_extent: (i64, i64),
}
fn parse_group_transform(reader: &mut Reader) -> Result<GroupTransform> {
let mut offset = (0i64, 0i64);
let mut extent = (0i64, 0i64);
let mut child_offset = (0i64, 0i64);
let mut child_extent = (0i64, 0i64);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"xfrm" => break,
Event::Empty(start) if start.local_name().as_ref() == b"off" => {
offset = read_xy_attributes(&start, b"x", b"y");
}
Event::Empty(start) if start.local_name().as_ref() == b"ext" => {
extent = read_xy_attributes(&start, b"cx", b"cy");
}
Event::Empty(start) if start.local_name().as_ref() == b"chOff" => {
child_offset = read_xy_attributes(&start, b"x", b"y");
}
Event::Empty(start) if start.local_name().as_ref() == b"chExt" => {
child_extent = read_xy_attributes(&start, b"cx", b"cy");
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(GroupTransform {
offset,
extent,
child_offset,
child_extent,
})
}
fn read_xy_attributes(start: &BytesStart, first: &[u8], second: &[u8]) -> (i64, i64) {
let first_value = attribute_by_qname(start, first)
.and_then(|value| value.parse().ok())
.unwrap_or(0);
let second_value = attribute_by_qname(start, second)
.and_then(|value| value.parse().ok())
.unwrap_or(0);
(first_value, second_value)
}
fn parse_connector(reader: &mut Reader) -> Result<Connector> {
let mut id: u32 = 0;
let mut name = String::new();
let mut properties = ShapeProperties::new();
let mut start_connection = None;
let mut end_connection = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"cxnSp" => break,
Event::Start(start) if start.local_name().as_ref() == b"nvCxnSpPr" => {}
Event::End(end) if end.local_name().as_ref() == b"nvCxnSpPr" => {}
Event::Empty(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
}
Event::Start(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
skip_subtree(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"cNvCxnSpPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"cNvCxnSpPr" => {}
Event::End(end) if end.local_name().as_ref() == b"cNvCxnSpPr" => {}
Event::Empty(start) if start.local_name().as_ref() == b"stCxn" => {
start_connection = Some(parse_shape_connection(&start));
}
Event::Empty(start) if start.local_name().as_ref() == b"endCxn" => {
end_connection = Some(parse_shape_connection(&start));
}
Event::Empty(start) if start.local_name().as_ref() == b"nvPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"nvPr" => skip_subtree(reader)?,
Event::Empty(start) if start.local_name().as_ref() == b"spPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"spPr" => {
properties = drawing::read_shape_properties(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(Connector {
id,
name,
properties,
start_connection,
end_connection,
})
}
fn parse_shape_connection(start: &BytesStart) -> ShapeConnection {
ShapeConnection {
shape_id: attribute_by_qname(start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0),
index: attribute_by_qname(start, b"idx")
.and_then(|value| value.parse().ok())
.unwrap_or(0),
}
}
fn parse_auto_shape(reader: &mut Reader, context: Option<&SlideContext>) -> Result<AutoShape> {
let mut id: u32 = 0;
let mut name = String::new();
let mut properties = ShapeProperties::new();
let mut text_body = None;
let mut placeholder = None;
let mut is_text_box = false;
let mut hyperlink = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"sp" => break,
Event::Start(start) if start.local_name().as_ref() == b"nvSpPr" => {}
Event::End(end) if end.local_name().as_ref() == b"nvSpPr" => {}
Event::Empty(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
}
Event::Start(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
hyperlink = parse_cnv_pr_hyperlink(reader, context)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"cNvSpPr" => {
is_text_box = attribute_by_qname(&start, b"txBox").as_deref() == Some("1");
}
Event::Start(start) if start.local_name().as_ref() == b"cNvSpPr" => {
is_text_box = attribute_by_qname(&start, b"txBox").as_deref() == Some("1");
skip_subtree(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"nvPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"nvPr" => {
placeholder = parse_placeholder_wrapper(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"spPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"spPr" => {
properties = drawing::read_shape_properties(reader)?;
}
Event::Start(start) if start.local_name().as_ref() == b"txBody" => {
text_body = Some(drawing::read_text_body(reader)?);
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(AutoShape {
id,
name,
properties,
text_body,
placeholder,
is_text_box,
hyperlink,
})
}
fn parse_cnv_pr_hyperlink(
reader: &mut Reader,
context: Option<&SlideContext>,
) -> Result<Option<SlideHyperlinkTarget>> {
let mut hyperlink = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"cNvPr" => break,
Event::Empty(start) if start.local_name().as_ref() == b"hlinkClick" => {
hyperlink = resolve_hyperlink_target(&start, context);
}
Event::Start(start) if start.local_name().as_ref() == b"hlinkClick" => {
hyperlink = resolve_hyperlink_target(&start, context);
skip_subtree(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(hyperlink)
}
fn resolve_hyperlink_target(
start: &BytesStart,
context: Option<&SlideContext>,
) -> Option<SlideHyperlinkTarget> {
let action = attribute_by_qname(start, b"action");
let relationship_id = attribute_by_qname(start, b"r:id");
if relationship_id.is_none() {
return match action.as_deref() {
Some("ppaction://hlinkshowjump?jump=nextslide") => {
Some(SlideHyperlinkTarget::NextSlide)
}
Some("ppaction://hlinkshowjump?jump=previousslide") => {
Some(SlideHyperlinkTarget::PreviousSlide)
}
Some("ppaction://hlinkshowjump?jump=firstslide") => {
Some(SlideHyperlinkTarget::FirstSlide)
}
Some("ppaction://hlinkshowjump?jump=lastslide") => {
Some(SlideHyperlinkTarget::LastSlide)
}
_ => None,
};
}
let relationship = context?.relationships.by_id(&relationship_id?)?;
if action.as_deref() == Some("ppaction://hlinksldjump")
&& relationship.rel_type == SLIDE_RELATIONSHIP_TYPE
{
if let Some(index) = parse_slide_relationship_target(&relationship.target) {
return Some(SlideHyperlinkTarget::Slide(index));
}
}
Some(SlideHyperlinkTarget::External(relationship.target.clone()))
}
fn parse_slide_relationship_target(target: &str) -> Option<usize> {
let number = target
.strip_prefix("../slides/slide")?
.strip_suffix(".xml")?;
let slide_number: usize = number.parse().ok()?;
slide_number.checked_sub(1)
}
fn parse_placeholder_wrapper(reader: &mut Reader) -> Result<Option<Placeholder>> {
let mut placeholder = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"nvPr" => break,
Event::Empty(start) if start.local_name().as_ref() == b"ph" => {
placeholder = Some(parse_placeholder_attributes(&start));
}
Event::Start(start) if start.local_name().as_ref() == b"ph" => {
placeholder = Some(parse_placeholder_attributes(&start));
skip_subtree(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(placeholder)
}
fn parse_placeholder_attributes(start: &BytesStart) -> Placeholder {
let kind = attribute_by_qname(start, b"type")
.map(|token| PlaceholderKind::from_xml_token(&token))
.unwrap_or(PlaceholderKind::Object);
let mut placeholder = Placeholder::new(kind);
if let Some(index) = attribute_by_qname(start, b"idx").and_then(|value| value.parse().ok()) {
placeholder = placeholder.with_index(index);
}
placeholder
}
fn parse_pic_shape(reader: &mut Reader, context: &SlideContext) -> Result<Shape> {
let mut id: u32 = 0;
let mut name = String::new();
let mut description = String::new();
let mut embed_relationship_id: Option<String> = None;
let mut link_relationship_id: Option<String> = None;
let mut media_relationship_id: Option<String> = None;
let mut properties = ShapeProperties::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"pic" => break,
Event::Start(start) if start.local_name().as_ref() == b"nvPicPr" => {}
Event::End(end) if end.local_name().as_ref() == b"nvPicPr" => {}
Event::Empty(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
description = attribute_by_qname(&start, b"descr").unwrap_or_default();
}
Event::Start(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
description = attribute_by_qname(&start, b"descr").unwrap_or_default();
skip_subtree(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"cNvPicPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"cNvPicPr" => {
skip_subtree(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"nvPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"nvPr" => {
media_relationship_id = parse_media_reference(reader)?;
}
Event::Start(start) if start.local_name().as_ref() == b"blipFill" => {
let (embed, link) = parse_blip_fill_embed(reader)?;
embed_relationship_id = embed;
link_relationship_id = link;
}
Event::Empty(start) if start.local_name().as_ref() == b"spPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"spPr" => {
properties = drawing::read_shape_properties(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
let offset_emu = properties
.transform
.as_ref()
.and_then(|transform| transform.offset)
.unwrap_or((0, 0));
let extent_emu = properties
.transform
.as_ref()
.and_then(|transform| transform.extent)
.unwrap_or((0, 0));
if let Some(media_relationship_id) = media_relationship_id {
let relationship = context
.relationships
.by_id(&media_relationship_id)
.ok_or_else(|| Error::MissingMediaRelationship(media_relationship_id.clone()))?;
let media_part_name =
resolve_relative_target(context.slide_part_name, &relationship.target);
let media_part = context
.package
.part(&media_part_name)
.ok_or_else(|| Error::MissingMediaPart(media_part_name.clone()))?;
let format = MediaFormat::from_extension(&media_part_name)
.ok_or_else(|| Error::UnsupportedMediaFormat(media_part_name.clone()))?;
let poster_image = embed_relationship_id.and_then(|poster_relationship_id| {
let relationship = context.relationships.by_id(&poster_relationship_id)?;
let poster_part_name =
resolve_relative_target(context.slide_part_name, &relationship.target);
let poster_part = context.package.part(&poster_part_name)?;
let poster_format = PictureFormat::from_extension(&poster_part_name)?;
Some((poster_part.data.clone(), poster_format))
});
return Ok(Shape::Media(SlideMedia {
id,
name,
data: media_part.data.clone(),
format,
offset_emu,
extent_emu,
poster_image,
}));
}
properties.transform = None;
let relationship_id =
embed_relationship_id.ok_or_else(|| Error::MissingImageRelationship(String::new()))?;
let relationship = context
.relationships
.by_id(&relationship_id)
.ok_or_else(|| Error::MissingImageRelationship(relationship_id.clone()))?;
let image_part_name = resolve_relative_target(context.slide_part_name, &relationship.target);
let image_part = context
.package
.part(&image_part_name)
.ok_or_else(|| Error::MissingImagePart(image_part_name.clone()))?;
let format = PictureFormat::from_extension(&image_part_name)
.ok_or_else(|| Error::UnsupportedImageFormat(image_part_name.clone()))?;
let is_default_shape = matches!(
properties.geometry,
None | Some(Geometry::Preset(PresetShape::Rectangle))
) && properties.fill.is_none()
&& properties.line.is_none();
let shape_properties = if is_default_shape {
None
} else {
Some(properties)
};
let external_link = link_relationship_id
.and_then(|id| context.relationships.by_id(&id))
.map(|relationship| relationship.target.clone());
Ok(Shape::Picture(Picture {
id,
name,
data: image_part.data.clone(),
format,
offset_emu,
extent_emu,
description,
shape_properties,
external_link,
}))
}
fn parse_media_reference(reader: &mut Reader) -> Result<Option<String>> {
let mut relationship_id = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"nvPr" => break,
Event::Empty(start) if start.local_name().as_ref() == b"videoFile" => {
relationship_id = attribute_by_qname(&start, b"r:link");
}
Event::Empty(start) if start.local_name().as_ref() == b"audioFile" => {
relationship_id = attribute_by_qname(&start, b"r:link");
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(relationship_id)
}
fn parse_blip_fill_embed(reader: &mut Reader) -> Result<(Option<String>, Option<String>)> {
let mut embed_relationship_id = None;
let mut link_relationship_id = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"blipFill" => break,
Event::Empty(start) if start.local_name().as_ref() == b"blip" => {
embed_relationship_id = attribute_by_qname(&start, b"r:embed");
link_relationship_id = attribute_by_qname(&start, b"r:link");
}
Event::Start(start) if start.local_name().as_ref() == b"blip" => {
embed_relationship_id = attribute_by_qname(&start, b"r:embed");
link_relationship_id = attribute_by_qname(&start, b"r:link");
skip_subtree(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok((embed_relationship_id, link_relationship_id))
}
fn parse_graphic_frame_shape(reader: &mut Reader, context: &SlideContext) -> Result<Shape> {
let mut id: u32 = 0;
let mut name = String::new();
let mut offset_emu: (i64, i64) = (0, 0);
let mut extent_emu: (i64, i64) = (0, 0);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"graphicFrame" => break,
Event::Start(start) if start.local_name().as_ref() == b"nvGraphicFramePr" => {}
Event::End(end) if end.local_name().as_ref() == b"nvGraphicFramePr" => {}
Event::Empty(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
}
Event::Start(start) if start.local_name().as_ref() == b"cNvPr" => {
id = attribute_by_qname(&start, b"id")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
name = attribute_by_qname(&start, b"name").unwrap_or_default();
skip_subtree(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"cNvGraphicFramePr" => {}
Event::Start(start) if start.local_name().as_ref() == b"cNvGraphicFramePr" => {
skip_subtree(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"nvPr" => {}
Event::Start(start) if start.local_name().as_ref() == b"nvPr" => {
skip_subtree(reader)?;
}
Event::Start(start) if start.local_name().as_ref() == b"xfrm" => {
let (parsed_offset, parsed_extent) = parse_frame_transform(reader)?;
offset_emu = parsed_offset;
extent_emu = parsed_extent;
}
Event::Start(start) if start.local_name().as_ref() == b"graphic" => {}
Event::End(end) if end.local_name().as_ref() == b"graphic" => {}
Event::Start(start) if start.local_name().as_ref() == b"graphicData" => {
let uri = attribute_by_qname(&start, b"uri");
return match uri.as_deref() {
Some(TABLE_NAMESPACE) => Ok(Shape::Table(parse_table(
reader, id, name, offset_emu, extent_emu,
)?)),
_ => Ok(Shape::Chart(Box::new(parse_chart(
reader, context, id, name, offset_emu, extent_emu,
)?))),
};
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(Shape::Table(SlideTable {
id,
name,
offset_emu,
extent_emu,
column_widths_emu: Vec::new(),
rows: Vec::new(),
style_id: None,
style_first_row: false,
style_first_column: false,
style_last_row: false,
style_last_column: false,
style_band_rows: false,
style_band_columns: false,
}))
}
fn parse_chart(
reader: &mut Reader,
context: &SlideContext,
id: u32,
name: String,
offset_emu: (i64, i64),
extent_emu: (i64, i64),
) -> Result<SlideChart> {
let mut chart_relationship_id: Option<String> = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"graphicFrame" => break,
Event::End(end) if end.local_name().as_ref() == b"graphicData" => {}
Event::Empty(start) if start.local_name().as_ref() == b"chart" => {
chart_relationship_id = attribute_by_qname(&start, b"r:id");
}
Event::Start(start) if start.local_name().as_ref() == b"chart" => {
chart_relationship_id = attribute_by_qname(&start, b"r:id");
skip_subtree(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
let relationship_id =
chart_relationship_id.ok_or_else(|| Error::MissingChartRelationship(String::new()))?;
let relationship = context
.relationships
.by_id(&relationship_id)
.ok_or_else(|| Error::MissingChartRelationship(relationship_id.clone()))?;
let chart_part_name = resolve_relative_target(context.slide_part_name, &relationship.target);
let chart_part = context
.package
.part(&chart_part_name)
.ok_or_else(|| Error::MissingChartPart(chart_part_name.clone()))?;
let chart_xml = std::str::from_utf8(&chart_part.data)
.map_err(|error| Error::InvalidUtf8(chart_part_name.clone(), error))?;
let chart_space = chart::read_chart_space(chart_xml)?;
Ok(SlideChart {
id,
name,
chart_space,
offset_emu,
extent_emu,
})
}
fn parse_table(
reader: &mut Reader,
id: u32,
name: String,
offset_emu: (i64, i64),
extent_emu: (i64, i64),
) -> Result<SlideTable> {
let mut column_widths_emu = Vec::new();
let mut rows = Vec::new();
let mut style_id = None;
let mut style_first_row = false;
let mut style_first_column = false;
let mut style_last_row = false;
let mut style_last_column = false;
let mut style_band_rows = false;
let mut style_band_columns = false;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"graphicFrame" => break,
Event::End(end) if end.local_name().as_ref() == b"graphicData" => {}
Event::Start(start) if start.local_name().as_ref() == b"tbl" => {}
Event::End(end) if end.local_name().as_ref() == b"tbl" => {}
Event::Empty(start) if start.local_name().as_ref() == b"tblPr" => {
style_first_row = attribute_by_qname(&start, b"firstRow").as_deref() == Some("1");
style_first_column =
attribute_by_qname(&start, b"firstCol").as_deref() == Some("1");
style_last_row = attribute_by_qname(&start, b"lastRow").as_deref() == Some("1");
style_last_column = attribute_by_qname(&start, b"lastCol").as_deref() == Some("1");
style_band_rows = attribute_by_qname(&start, b"bandRow").as_deref() == Some("1");
style_band_columns = attribute_by_qname(&start, b"bandCol").as_deref() == Some("1");
}
Event::Start(start) if start.local_name().as_ref() == b"tblPr" => {
style_first_row = attribute_by_qname(&start, b"firstRow").as_deref() == Some("1");
style_first_column =
attribute_by_qname(&start, b"firstCol").as_deref() == Some("1");
style_last_row = attribute_by_qname(&start, b"lastRow").as_deref() == Some("1");
style_last_column = attribute_by_qname(&start, b"lastCol").as_deref() == Some("1");
style_band_rows = attribute_by_qname(&start, b"bandRow").as_deref() == Some("1");
style_band_columns = attribute_by_qname(&start, b"bandCol").as_deref() == Some("1");
style_id = parse_table_style_id(reader)?;
}
Event::Start(start) if start.local_name().as_ref() == b"tblGrid" => {}
Event::End(end) if end.local_name().as_ref() == b"tblGrid" => {}
Event::Empty(start) if start.local_name().as_ref() == b"gridCol" => {
if let Some(width) =
attribute_by_qname(&start, b"w").and_then(|value| value.parse().ok())
{
column_widths_emu.push(width);
}
}
Event::Start(start) if start.local_name().as_ref() == b"gridCol" => {
if let Some(width) =
attribute_by_qname(&start, b"w").and_then(|value| value.parse().ok())
{
column_widths_emu.push(width);
}
skip_subtree(reader)?;
}
Event::Start(start) if start.local_name().as_ref() == b"tr" => {
rows.push(parse_table_row(reader, &start)?);
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(SlideTable {
id,
name,
offset_emu,
extent_emu,
column_widths_emu,
rows,
style_id,
style_first_row,
style_first_column,
style_last_row,
style_last_column,
style_band_rows,
style_band_columns,
})
}
fn parse_table_style_id(reader: &mut Reader) -> Result<Option<String>> {
let mut style_id = None;
let mut in_style_id = false;
loop {
match reader.read_event()? {
Event::Start(start) if start.local_name().as_ref() == b"tableStyleId" => {
in_style_id = true
}
Event::End(end) if end.local_name().as_ref() == b"tableStyleId" => in_style_id = false,
Event::Text(text) if in_style_id => {
let decoded = xml_core::decode_text(&text)?;
if decoded != TABLE_STYLE_GUID {
style_id = Some(decoded);
}
}
Event::Start(_) => skip_subtree(reader)?,
Event::End(end) if end.local_name().as_ref() == b"tblPr" => break,
Event::Eof => break,
_ => {}
}
}
Ok(style_id)
}
fn parse_table_row(reader: &mut Reader, start: &BytesStart) -> Result<TableRow> {
let height_emu = attribute_by_qname(start, b"h")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
let mut cells = Vec::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"tr" => break,
Event::Start(start) if start.local_name().as_ref() == b"tc" => {
cells.push(parse_table_cell(reader, &start)?);
}
Event::Empty(start) if start.local_name().as_ref() == b"tc" => {
cells.push(TableCell {
horizontal_span: attribute_by_qname(&start, b"gridSpan")
.and_then(|value| value.parse().ok()),
vertical_span: attribute_by_qname(&start, b"rowSpan")
.and_then(|value| value.parse().ok()),
horizontal_merge: attribute_by_qname(&start, b"hMerge").as_deref() == Some("1"),
vertical_merge: attribute_by_qname(&start, b"vMerge").as_deref() == Some("1"),
text_body: None,
properties: None,
});
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(TableRow { height_emu, cells })
}
fn parse_table_cell(reader: &mut Reader, start: &BytesStart) -> Result<TableCell> {
let horizontal_span =
attribute_by_qname(start, b"gridSpan").and_then(|value| value.parse().ok());
let vertical_span = attribute_by_qname(start, b"rowSpan").and_then(|value| value.parse().ok());
let horizontal_merge = attribute_by_qname(start, b"hMerge").as_deref() == Some("1");
let vertical_merge = attribute_by_qname(start, b"vMerge").as_deref() == Some("1");
let mut text_body = None;
let mut properties = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"tc" => break,
Event::Start(start) if start.local_name().as_ref() == b"txBody" => {
text_body = Some(drawing::read_text_body(reader)?);
}
Event::Start(start) if start.local_name().as_ref() == b"tcPr" => {
properties = Some(parse_table_cell_properties(reader, &start)?);
}
Event::Empty(start) if start.local_name().as_ref() == b"tcPr" => {
properties = Some(parse_table_cell_properties_attributes(&start));
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(TableCell {
text_body,
horizontal_span,
vertical_span,
horizontal_merge,
vertical_merge,
properties,
})
}
fn parse_table_cell_properties_attributes(start: &BytesStart) -> TableCellProperties {
TableCellProperties {
margin_left_emu: attribute_by_qname(start, b"marL").and_then(|value| value.parse().ok()),
margin_top_emu: attribute_by_qname(start, b"marT").and_then(|value| value.parse().ok()),
margin_right_emu: attribute_by_qname(start, b"marR").and_then(|value| value.parse().ok()),
margin_bottom_emu: attribute_by_qname(start, b"marB").and_then(|value| value.parse().ok()),
anchor: attribute_by_qname(start, b"anchor")
.and_then(|value| parse_table_cell_anchor(&value)),
..TableCellProperties::default()
}
}
fn parse_table_cell_properties(
reader: &mut Reader,
start: &BytesStart,
) -> Result<TableCellProperties> {
let mut properties = parse_table_cell_properties_attributes(start);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"tcPr" => break,
Event::Empty(start) if start.local_name().as_ref() == b"lnL" => {
properties.border_left = Some(drawing::read_line(reader, &start, false)?);
}
Event::Start(start) if start.local_name().as_ref() == b"lnL" => {
properties.border_left = Some(drawing::read_line(reader, &start, true)?);
}
Event::Empty(start) if start.local_name().as_ref() == b"lnR" => {
properties.border_right = Some(drawing::read_line(reader, &start, false)?);
}
Event::Start(start) if start.local_name().as_ref() == b"lnR" => {
properties.border_right = Some(drawing::read_line(reader, &start, true)?);
}
Event::Empty(start) if start.local_name().as_ref() == b"lnT" => {
properties.border_top = Some(drawing::read_line(reader, &start, false)?);
}
Event::Start(start) if start.local_name().as_ref() == b"lnT" => {
properties.border_top = Some(drawing::read_line(reader, &start, true)?);
}
Event::Empty(start) if start.local_name().as_ref() == b"lnB" => {
properties.border_bottom = Some(drawing::read_line(reader, &start, false)?);
}
Event::Start(start) if start.local_name().as_ref() == b"lnB" => {
properties.border_bottom = Some(drawing::read_line(reader, &start, true)?);
}
Event::Start(start) if drawing::is_fill_choice_element(&start) => {
properties.fill = Some(drawing::read_fill_choice(reader, &start, true)?);
}
Event::Empty(start) if drawing::is_fill_choice_element(&start) => {
properties.fill = Some(drawing::read_fill_choice(reader, &start, false)?);
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(properties)
}
fn parse_table_cell_anchor(token: &str) -> Option<TextAnchor> {
Some(match token {
"t" => TextAnchor::Top,
"ctr" => TextAnchor::Center,
"b" => TextAnchor::Bottom,
"just" => TextAnchor::Justified,
"dist" => TextAnchor::Distributed,
_ => return None,
})
}
fn parse_frame_transform(reader: &mut Reader) -> Result<((i64, i64), (i64, i64))> {
let mut offset = (0i64, 0i64);
let mut extent = (0i64, 0i64);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"xfrm" => break,
Event::Empty(start) if start.local_name().as_ref() == b"off" => {
let x = attribute_by_qname(&start, b"x")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
let y = attribute_by_qname(&start, b"y")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
offset = (x, y);
}
Event::Empty(start) if start.local_name().as_ref() == b"ext" => {
let cx = attribute_by_qname(&start, b"cx")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
let cy = attribute_by_qname(&start, b"cy")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
extent = (cx, cy);
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok((offset, extent))
}
fn parse_notes_slide_xml(xml: &str) -> Result<TextBody> {
let mut reader = Reader::from_xml_str(xml);
let mut body_text: Option<TextBody> = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"sp" => {
let shape = parse_auto_shape(&mut reader, None)?;
let is_body_placeholder = matches!(
shape
.placeholder
.as_ref()
.map(|placeholder| &placeholder.kind),
Some(PlaceholderKind::Body)
);
if is_body_placeholder {
if let Some(text_body) = shape.text_body {
body_text = Some(text_body);
}
}
}
_ => {}
}
}
Ok(body_text.unwrap_or_default())
}
fn read_presentation_theme(
package: &Package,
presentation_relationships: &Relationships,
) -> Result<Option<Theme>> {
let Some(master_relationship) = presentation_relationships
.iter()
.find(|relationship| relationship.rel_type == SLIDE_MASTER_RELATIONSHIP_TYPE)
else {
return Ok(None);
};
let master_part_name = format!("/ppt/{}", master_relationship.target);
let Some(master_part) = package.part(&master_part_name) else {
return Ok(None);
};
let Some(theme_relationship) = master_part
.relationships
.iter()
.find(|relationship| relationship.rel_type == THEME_RELATIONSHIP_TYPE)
else {
return Ok(None);
};
let theme_part_name = resolve_relative_target(&master_part_name, &theme_relationship.target);
let Some(theme_part) = package.part(&theme_part_name) else {
return Ok(None);
};
let xml = std::str::from_utf8(&theme_part.data)
.map_err(|error| Error::InvalidUtf8(theme_part_name.clone(), error))?;
Ok(Some(parse_theme_xml(xml)?))
}
fn resolve_embedded_font_variant(
package: &Package,
presentation_relationships: &Relationships,
raw_relationship_id: &Option<String>,
) -> Option<Vec<u8>> {
let relationship_id = raw_relationship_id.as_ref()?;
let relationship = presentation_relationships.by_id(relationship_id)?;
let part_name = format!("/ppt/{}", relationship.target);
package.part(&part_name).map(|part| part.data.clone())
}
fn read_document_properties(package: &Package) -> Result<DocumentProperties> {
let mut properties = DocumentProperties::new();
if let Some(relationship) = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == CORE_PROPERTIES_RELATIONSHIP_TYPE)
{
let part_name = format!("/{}", relationship.target);
if let Some(part) = package.part(&part_name) {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
let core_properties = parse_core_properties_xml(xml)?;
properties.title = core_properties.title;
properties.author = core_properties.author;
properties.subject = core_properties.subject;
properties.keywords = core_properties.keywords;
}
}
if let Some(relationship) = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == EXTENDED_PROPERTIES_RELATIONSHIP_TYPE)
{
let part_name = format!("/{}", relationship.target);
if let Some(part) = package.part(&part_name) {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
let (company, manager, hyperlink_base) = parse_extended_properties_xml(xml)?;
properties.company = company;
properties.manager = manager;
properties.hyperlink_base = hyperlink_base;
}
}
if let Some(relationship) = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == CUSTOM_PROPERTIES_RELATIONSHIP_TYPE)
{
let part_name = format!("/{}", relationship.target);
if let Some(part) = package.part(&part_name) {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
properties.custom_properties = parse_custom_properties_xml(xml)?;
}
}
Ok(properties)
}
struct ParsedCoreProperties {
title: Option<String>,
author: Option<String>,
subject: Option<String>,
keywords: Option<String>,
}
fn parse_core_properties_xml(xml: &str) -> Result<ParsedCoreProperties> {
let mut reader = Reader::from_xml_str(xml);
let mut title = None;
let mut author = None;
let mut subject = None;
let mut keywords = None;
let mut in_element: Option<&'static str> = None;
let mut buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"title" => {
in_element = Some("title");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"creator" => {
in_element = Some("creator");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"subject" => {
in_element = Some("subject");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"keywords" => {
in_element = Some("keywords");
buffer.clear();
}
Event::Text(text) if in_element.is_some() => {
buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_element.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
Event::End(end)
if in_element.is_some()
&& matches!(
end.local_name().as_ref(),
b"title" | b"creator" | b"subject" | b"keywords"
) =>
{
let text = std::mem::take(&mut buffer);
match in_element.take() {
Some("title") => title = Some(text),
Some("creator") => author = Some(text),
Some("subject") => subject = Some(text),
Some("keywords") => keywords = Some(text),
_ => {}
}
}
_ => {}
}
}
Ok(ParsedCoreProperties {
title,
author,
subject,
keywords,
})
}
fn parse_extended_properties_xml(
xml: &str,
) -> Result<(Option<String>, Option<String>, Option<String>)> {
let mut reader = Reader::from_xml_str(xml);
let mut company = None;
let mut manager = None;
let mut hyperlink_base = None;
let mut in_element: Option<&'static str> = None;
let mut buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"Company" => {
in_element = Some("Company");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"Manager" => {
in_element = Some("Manager");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"HyperlinkBase" => {
in_element = Some("HyperlinkBase");
buffer.clear();
}
Event::Text(text) if in_element.is_some() => {
buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_element.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
Event::End(end)
if in_element.is_some()
&& matches!(
end.local_name().as_ref(),
b"Company" | b"Manager" | b"HyperlinkBase"
) =>
{
let text = std::mem::take(&mut buffer);
match in_element.take() {
Some("Company") => company = Some(text),
Some("Manager") => manager = Some(text),
Some("HyperlinkBase") => hyperlink_base = Some(text),
_ => {}
}
}
_ => {}
}
}
Ok((company, manager, hyperlink_base))
}
fn parse_custom_properties_xml(xml: &str) -> Result<Vec<(String, CustomPropertyValue)>> {
let mut reader = Reader::from_xml_str(xml);
let mut custom_properties = Vec::new();
let mut current_name: Option<String> = None;
let mut current_variant: Option<Vec<u8>> = None;
let mut buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"property" => {
current_name = attribute_by_qname(&start, b"name");
}
Event::Start(start) | Event::Empty(start)
if current_name.is_some()
&& matches!(
start.local_name().as_ref(),
b"lpwstr" | b"bool" | b"i4" | b"r8"
) =>
{
current_variant = Some(start.local_name().as_ref().to_vec());
buffer.clear();
}
Event::Text(text) if current_variant.is_some() => {
buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if current_variant.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
Event::End(end) => {
if let Some(variant) = current_variant.take() {
if end.local_name().as_ref() == variant.as_slice() {
if let Some(name) = current_name.clone() {
let value = std::mem::take(&mut buffer);
let parsed = match variant.as_slice() {
b"lpwstr" => Some(CustomPropertyValue::Text(value)),
b"bool" => {
Some(CustomPropertyValue::Bool(value == "true" || value == "1"))
}
b"i4" => value.parse::<i32>().ok().map(CustomPropertyValue::Int),
b"r8" => value.parse::<f64>().ok().map(CustomPropertyValue::Number),
_ => None,
};
if let Some(value) = parsed {
custom_properties.push((name, value));
}
}
} else {
current_variant = Some(variant);
}
} else if end.local_name().as_ref() == b"property" {
current_name = None;
}
}
_ => {}
}
}
Ok(custom_properties)
}
fn parse_table_styles_xml(xml: &str) -> Result<Vec<SlideTableStyle>> {
let mut styles = Vec::new();
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"tblStyle" => {
let id = attribute_by_qname(&start, b"styleId").unwrap_or_default();
let name = attribute_by_qname(&start, b"styleName").unwrap_or_default();
styles.push(parse_table_style(&mut reader, id, name)?);
}
Event::Empty(start) if start.local_name().as_ref() == b"tblStyle" => {
let id = attribute_by_qname(&start, b"styleId").unwrap_or_default();
let name = attribute_by_qname(&start, b"styleName").unwrap_or_default();
styles.push(SlideTableStyle {
id,
name,
..Default::default()
});
}
_ => {}
}
}
Ok(styles)
}
fn parse_table_style(reader: &mut Reader, id: String, name: String) -> Result<SlideTableStyle> {
let mut style = SlideTableStyle {
id,
name,
..Default::default()
};
loop {
match reader.read_event()? {
Event::Start(start) => {
let wrapper_name = start.local_name().as_ref().to_vec();
let part = Some(parse_table_style_part(reader, &wrapper_name)?);
match wrapper_name.as_slice() {
b"wholeTbl" => style.whole_table = part,
b"band1H" => style.band1_horizontal = part,
b"band2H" => style.band2_horizontal = part,
b"band1V" => style.band1_vertical = part,
b"band2V" => style.band2_vertical = part,
b"firstRow" => style.first_row = part,
b"lastRow" => style.last_row = part,
b"firstCol" => style.first_column = part,
b"lastCol" => style.last_column = part,
_ => {}
}
}
Event::Empty(_) => {}
Event::End(end) if end.local_name().as_ref() == b"tblStyle" => break,
Event::Eof => break,
_ => {}
}
}
Ok(style)
}
fn parse_table_style_part(
reader: &mut Reader,
wrapper_local_name: &[u8],
) -> Result<TableStylePart> {
let mut part = TableStylePart::new();
loop {
match reader.read_event()? {
Event::Empty(start) if start.local_name().as_ref() == b"tcTxStyle" => {
part.bold = attribute_by_qname(&start, b"b").as_deref() == Some("on");
part.italic = attribute_by_qname(&start, b"i").as_deref() == Some("on");
}
Event::Start(start) if start.local_name().as_ref() == b"tcTxStyle" => {
part.bold = attribute_by_qname(&start, b"b").as_deref() == Some("on");
part.italic = attribute_by_qname(&start, b"i").as_deref() == Some("on");
part.text_color = drawing::read_color(reader)?;
}
Event::Start(start) if start.local_name().as_ref() == b"tcStyle" => {
part.fill = parse_table_style_cell_fill(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"tcStyle" => {}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
Event::End(end) if end.local_name().as_ref() == wrapper_local_name => break,
Event::Eof => break,
_ => {}
}
}
Ok(part)
}
fn parse_table_style_cell_fill(reader: &mut Reader) -> Result<Option<Fill>> {
let mut fill = None;
loop {
match reader.read_event()? {
Event::Start(start) if start.local_name().as_ref() == b"fill" => {
fill = drawing::read_fill(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"fill" => {}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
Event::End(end) if end.local_name().as_ref() == b"tcStyle" => break,
Event::Eof => break,
_ => {}
}
}
Ok(fill)
}
fn parse_theme_xml(xml: &str) -> Result<Theme> {
let mut reader = Reader::from_xml_str(xml);
let mut name = String::new();
let mut colors = ColorScheme::office_default();
let mut fonts = FontScheme::office_default();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"theme" => {
if let Some(value) = attribute_by_qname(&start, b"name") {
name = value;
}
}
Event::Start(start) if start.local_name().as_ref() == b"clrScheme" => {
colors = parse_color_scheme(&mut reader)?;
}
Event::Start(start) if start.local_name().as_ref() == b"fontScheme" => {
fonts = parse_font_scheme(&mut reader)?;
}
_ => {}
}
}
Ok(Theme {
name,
colors,
fonts,
})
}
fn parse_color_scheme(reader: &mut Reader) -> Result<ColorScheme> {
let mut colors = ColorScheme::office_default();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"clrScheme" => break,
Event::Start(start) => {
let slot = start.local_name().as_ref().to_vec();
if let Some(value) = read_color_slot_value(reader, &slot)? {
assign_color_slot(&mut colors, &slot, value);
}
}
_ => {}
}
}
Ok(colors)
}
fn read_color_slot_value(reader: &mut Reader, wrapper_local_name: &[u8]) -> Result<Option<String>> {
let mut value = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == wrapper_local_name => break,
Event::Empty(start) | Event::Start(start) => match start.local_name().as_ref() {
b"srgbClr" => value = attribute_by_qname(&start, b"val"),
b"sysClr" => {
value = attribute_by_qname(&start, b"lastClr")
.or_else(|| attribute_by_qname(&start, b"val"))
}
_ => {}
},
_ => {}
}
}
Ok(value)
}
fn assign_color_slot(colors: &mut ColorScheme, slot: &[u8], value: String) {
match slot {
b"dk1" => colors.dark1 = value,
b"lt1" => colors.light1 = value,
b"dk2" => colors.dark2 = value,
b"lt2" => colors.light2 = value,
b"accent1" => colors.accent1 = value,
b"accent2" => colors.accent2 = value,
b"accent3" => colors.accent3 = value,
b"accent4" => colors.accent4 = value,
b"accent5" => colors.accent5 = value,
b"accent6" => colors.accent6 = value,
b"hlink" => colors.hyperlink = value,
b"folHlink" => colors.followed_hyperlink = value,
_ => {}
}
}
fn parse_font_scheme(reader: &mut Reader) -> Result<FontScheme> {
let mut fonts = FontScheme::office_default();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"fontScheme" => break,
Event::Start(start) if start.local_name().as_ref() == b"majorFont" => {
if let Some(typeface) = parse_font_collection_latin(reader, b"majorFont")? {
fonts.major_latin = typeface;
}
}
Event::Start(start) if start.local_name().as_ref() == b"minorFont" => {
if let Some(typeface) = parse_font_collection_latin(reader, b"minorFont")? {
fonts.minor_latin = typeface;
}
}
_ => {}
}
}
Ok(fonts)
}
fn parse_font_collection_latin(
reader: &mut Reader,
wrapper_local_name: &[u8],
) -> Result<Option<String>> {
let mut typeface = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == wrapper_local_name => break,
Event::Empty(start) | Event::Start(start)
if start.local_name().as_ref() == b"latin" =>
{
typeface = attribute_by_qname(&start, b"typeface");
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(typeface)
}
fn parse_comment_authors_xml(xml: &str) -> Result<HashMap<u32, (String, String)>> {
let mut authors = HashMap::new();
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Empty(start) | Event::Start(start)
if start.local_name().as_ref() == b"cmAuthor" =>
{
if let Some(id) =
attribute_by_qname(&start, b"id").and_then(|value| value.parse().ok())
{
let name = attribute_by_qname(&start, b"name").unwrap_or_default();
let initials = attribute_by_qname(&start, b"initials").unwrap_or_default();
authors.insert(id, (name, initials));
}
}
_ => {}
}
}
Ok(authors)
}
fn parse_comments_xml(
xml: &str,
authors: &HashMap<u32, (String, String)>,
) -> Result<Vec<SlideComment>> {
let mut comments = Vec::new();
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"cm" => {
let author_id: u32 = attribute_by_qname(&start, b"authorId")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
let (author, initials) = authors.get(&author_id).cloned().unwrap_or_default();
let date = attribute_by_qname(&start, b"dt").unwrap_or_default();
comments.push(parse_comment_body(&mut reader, author, initials, date)?);
}
_ => {}
}
}
Ok(comments)
}
fn parse_comment_body(
reader: &mut Reader,
author: String,
initials: String,
date: String,
) -> Result<SlideComment> {
let mut position_emu = (0i64, 0i64);
let mut text = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"cm" => break,
Event::Empty(start) if start.local_name().as_ref() == b"pos" => {
position_emu = read_xy_attributes(&start, b"x", b"y");
}
Event::Start(start) if start.local_name().as_ref() == b"text" => {
text = read_element_text(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(_) => {}
_ => {}
}
}
Ok(SlideComment {
author,
initials,
date,
position_emu,
text,
})
}
fn read_element_text(reader: &mut Reader) -> Result<String> {
let mut text = String::new();
loop {
match reader.read_event()? {
Event::Eof | Event::End(_) => break,
Event::Text(bytes_text) => {
text.push_str(&xml_core::decode_text(&bytes_text)?);
}
Event::GeneralRef(bytes_ref) => {
if let Some(decoded) = xml_core::decode_general_ref(&bytes_ref)? {
text.push_str(&decoded);
}
}
_ => {}
}
}
Ok(text)
}
fn attribute_by_qname(start: &BytesStart, qname: &[u8]) -> Option<String> {
start
.attributes()
.flatten()
.find(|attribute| attribute.key.as_ref() == qname)
.and_then(|attribute| {
xml_core::decode_attribute_value(attribute.value.as_ref())
.ok()
.flatten()
})
}
fn skip_subtree(reader: &mut Reader) -> Result<()> {
let mut depth: u32 = 0;
loop {
match reader.read_event()? {
Event::Start(_) => depth += 1,
Event::End(_) => {
if depth == 0 {
return Ok(());
}
depth -= 1;
}
Event::Eof => return Ok(()),
_ => {}
}
}
}