use std::cell::Cell;
use std::io::Read;
use std::path::Path;
use std::rc::Rc;
use crate::notes_masters::{NotesMasterRef, NotesMasters};
use crate::opc::package::{ct, rels_partname_for};
use crate::opc::part::{new_part_name, Part, PartName};
use crate::opc::rels::{RelType, Relationship, Relationships};
use crate::opc::OpcPackage;
use crate::oxml::presentation::{PresentationRoot, SlideIdEntry};
use crate::oxml::slidelayout::SldLayout as OxmlSldLayout;
use crate::oxml::slidemaster::SldMaster as OxmlSldMaster;
use crate::oxml::theme::default_theme_xml;
use crate::slide::{Slide, SlideEntry, Slides};
use crate::slide_layouts::{SlideLayoutRef, SlideLayouts};
use crate::slide_masters::{SlideMasterRef, SlideMasters};
use crate::units::Emu;
pub const DEFAULT_WIDTH_EMU: i64 = 9_144_000;
pub const DEFAULT_HEIGHT_EMU: i64 = 6_858_000;
#[derive(Debug)]
pub struct Presentation {
pub(crate) slides: Slides,
pub(crate) slide_layouts: SlideLayouts,
pub(crate) slide_masters: SlideMasters,
pub(crate) notes_masters: NotesMasters,
pub(crate) theme: crate::oxml::theme::Theme,
pub(crate) themes: Vec<crate::oxml::theme::Theme>,
pub(crate) sld_master_ids: Vec<(u32, String)>,
pub(crate) notes_master_ids: Vec<(u32, String)>,
pub(crate) width: Emu,
pub(crate) height: Emu,
pub(crate) id_counter: Rc<Cell<u32>>,
pub(crate) core_properties: CoreProperties,
pub(crate) custom_properties: CustomProperties,
pub(crate) comment_authors: crate::oxml::comments::CommentAuthorList,
pub(crate) modify_protection: Option<crate::crypto::ModifyProtection>,
pub(crate) sections: crate::oxml::section::SectionList,
}
#[derive(Debug, Clone, Default)]
pub struct CoreProperties {
pub title: Option<String>,
pub creator: Option<String>,
pub subject: Option<String>,
pub last_modified_by: Option<String>,
pub application: Option<String>,
pub created: Option<String>,
pub modified: Option<String>,
pub keywords: Option<String>,
pub category: Option<String>,
pub description: Option<String>,
pub revision: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum CustomPropertyValue {
Text(String),
Int(i32),
Float(f64),
Bool(bool),
DateTime(String),
}
impl CustomPropertyValue {
fn vt_element(&self) -> &'static str {
match self {
CustomPropertyValue::Text(_) => "vt:lpwstr",
CustomPropertyValue::Int(_) => "vt:i4",
CustomPropertyValue::Float(_) => "vt:r8",
CustomPropertyValue::Bool(_) => "vt:bool",
CustomPropertyValue::DateTime(_) => "vt:filetime",
}
}
fn value_str(&self) -> String {
match self {
CustomPropertyValue::Text(s) => s.clone(),
CustomPropertyValue::Int(i) => i.to_string(),
CustomPropertyValue::Float(f) => f.to_string(),
CustomPropertyValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
CustomPropertyValue::DateTime(s) => s.clone(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CustomProperties {
entries: Vec<(String, CustomPropertyValue)>,
}
impl CustomProperties {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn set(&mut self, name: impl Into<String>, value: CustomPropertyValue) {
let name = name.into();
for entry in &mut self.entries {
if entry.0 == name {
entry.1 = value;
return;
}
}
self.entries.push((name, value));
}
pub fn get(&self, name: &str) -> Option<&CustomPropertyValue> {
self.entries.iter().find(|(k, _)| k == name).map(|(_, v)| v)
}
pub fn remove(&mut self, name: &str) -> Option<CustomPropertyValue> {
let idx = self.entries.iter().position(|(k, _)| k == name)?;
Some(self.entries.remove(idx).1)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &CustomPropertyValue)> {
self.entries.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn to_xml(&self) -> String {
if self.entries.is_empty() {
return String::new();
}
let mut out = String::with_capacity(256 * self.entries.len());
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
out.push_str("<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties\"");
out.push_str(
" xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\n",
);
for (i, (name, value)) in self.entries.iter().enumerate() {
let pid = (i + 2) as u32; let vt_elem = value.vt_element();
let val = xml_escape(&value.value_str());
out.push_str(&format!(
" <property fmtid=\"{{D5CDD505-2E9C-101B-9397-08002B2CF9AE}}\" pid=\"{}\" name=\"{}\">\n",
pid,
xml_escape(name)
));
out.push_str(&format!(" <{}>{}</{}>\n", vt_elem, val, vt_elem));
out.push_str(" </property>\n");
}
out.push_str("</Properties>");
out
}
pub fn from_xml(xml: &str) -> Self {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut props = CustomProperties::new();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut cur_name: Option<String> = None;
let mut cur_vt: Option<String> = None;
let mut cur_text = String::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let qname = String::from_utf8_lossy(name.as_ref()).to_string();
let local = local_name(name.as_ref());
if local == b"property" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"name" {
cur_name = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
} else if cur_name.is_some() && qname.starts_with("vt:") {
cur_vt = Some(qname);
cur_text.clear();
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"property" {
cur_name = None;
}
}
Ok(Event::Text(t)) => {
if cur_vt.is_some() {
cur_text.push_str(&t.decode().unwrap_or_default());
}
}
Ok(Event::End(e)) => {
let name = e.name();
let qname = String::from_utf8_lossy(name.as_ref()).to_string();
let local = local_name(name.as_ref());
if qname.starts_with("vt:") && cur_vt.is_some() {
if let (Some(prop_name), Some(vt)) = (cur_name.take(), cur_vt.take()) {
let value = parse_vt_value(&vt, &cur_text);
if let Some(v) = value {
props.set(prop_name, v);
}
}
cur_vt = None;
cur_text.clear();
} else if local == b"property" {
cur_name = None;
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
props
}
}
fn xml_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
fn local_name(name: &[u8]) -> &[u8] {
match name.iter().position(|&b| b == b':') {
Some(i) => &name[i + 1..],
None => name,
}
}
fn parse_core_xml(xml: &str) -> CoreProperties {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut cp = CoreProperties::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut cur_field: Option<&'static str> = None;
let mut cur_text = String::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
cur_field = match local {
b"title" => Some("title"),
b"creator" => Some("creator"),
b"subject" => Some("subject"),
b"lastModifiedBy" => Some("last_modified_by"),
b"keywords" => Some("keywords"),
b"category" => Some("category"),
b"description" => Some("description"),
b"revision" => Some("revision"),
b"created" => Some("created"),
b"modified" => Some("modified"),
_ => None,
};
if cur_field.is_some() {
cur_text.clear();
}
}
Ok(Event::Empty(_)) => {
cur_field = None;
}
Ok(Event::Text(t)) => {
if cur_field.is_some() {
cur_text.push_str(&t.decode().unwrap_or_default());
}
}
Ok(Event::End(_)) => {
if let Some(field) = cur_field.take() {
let value = cur_text.trim().to_string();
if !value.is_empty() {
match field {
"title" => cp.title = Some(value),
"creator" => cp.creator = Some(value),
"subject" => cp.subject = Some(value),
"last_modified_by" => cp.last_modified_by = Some(value),
"keywords" => cp.keywords = Some(value),
"category" => cp.category = Some(value),
"description" => cp.description = Some(value),
"revision" => cp.revision = Some(value),
"created" => cp.created = Some(value),
"modified" => cp.modified = Some(value),
_ => {}
}
}
cur_text.clear();
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
cp
}
fn parse_app_xml_application(xml: &str) -> Option<String> {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut in_application = false;
let mut text = String::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
if local_name(e.name().as_ref()) == b"Application" {
in_application = true;
text.clear();
}
}
Ok(Event::End(_)) => {
if in_application {
let v = text.trim().to_string();
if !v.is_empty() {
return Some(v);
}
in_application = false;
}
}
Ok(Event::Text(t)) => {
if in_application {
text.push_str(&t.decode().unwrap_or_default());
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
None
}
fn parse_vt_value(vt_elem: &str, text: &str) -> Option<CustomPropertyValue> {
let trimmed = text.trim();
match vt_elem {
"vt:lpwstr" | "vt:bstr" => Some(CustomPropertyValue::Text(trimmed.to_string())),
"vt:i4" | "vt:int" | "vt:i2" | "vt:ui1" | "vt:ui2" | "vt:ui4" => {
trimmed.parse::<i32>().ok().map(CustomPropertyValue::Int)
}
"vt:i8" | "vt:ui8" => {
trimmed.parse::<i64>().ok().and_then(|v| {
if v >= i32::MIN as i64 && v <= i32::MAX as i64 {
Some(CustomPropertyValue::Int(v as i32))
} else {
None
}
})
}
"vt:r4" | "vt:r8" | "vt:decimal" => {
trimmed.parse::<f64>().ok().map(CustomPropertyValue::Float)
}
"vt:bool" => {
let v = trimmed.eq_ignore_ascii_case("true") || trimmed == "1";
if v || trimmed.eq_ignore_ascii_case("false") || trimmed == "0" {
Some(CustomPropertyValue::Bool(v))
} else {
None
}
}
"vt:filetime" | "vt:date" => Some(CustomPropertyValue::DateTime(trimmed.to_string())),
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct MediaEntry {
pub partname: PartName,
pub content_type: String,
pub blob: Vec<u8>,
pub rid: String,
}
#[derive(Debug, Clone)]
pub struct ChartEntry {
pub partname: PartName,
pub chart: crate::oxml::chart::Chart,
pub rid: String,
pub xlsx_blob: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct OleEntry {
pub partname: PartName,
pub blob: Vec<u8>,
pub rid: String,
}
#[derive(Debug, Clone)]
pub struct VideoEntry {
pub partname: PartName,
pub blob: Vec<u8>,
pub rid: String,
}
#[derive(Debug, Clone)]
pub struct AudioEntry {
pub partname: PartName,
pub blob: Vec<u8>,
pub rid: String,
}
#[derive(Debug, Clone)]
pub struct DiagramEntry {
pub data_partname: PartName,
pub layout_partname: PartName,
pub quick_style_partname: PartName,
pub colors_partname: PartName,
pub data_xml: String,
pub layout_xml: String,
pub quick_style_xml: String,
pub colors_xml: String,
pub data_rid: String,
pub layout_rid: String,
pub quick_style_rid: String,
pub colors_rid: String,
}
impl DiagramEntry {
pub fn data_model(&self) -> crate::Result<crate::oxml::diagram::DataModel> {
crate::oxml::diagram::DataModel::parse_from_xml(&self.data_xml)
}
pub fn layout_def(&self) -> crate::Result<crate::oxml::diagram::LayoutDef> {
crate::oxml::diagram::LayoutDef::parse_from_xml(&self.layout_xml)
}
pub fn quick_style_def(&self) -> crate::Result<crate::oxml::diagram::QuickStyleDef> {
crate::oxml::diagram::QuickStyleDef::parse_from_xml(&self.quick_style_xml)
}
pub fn colors_def(&self) -> crate::Result<crate::oxml::diagram::ColorsDef> {
crate::oxml::diagram::ColorsDef::parse_from_xml(&self.colors_xml)
}
pub fn set_data_model(&mut self, data_model: &crate::oxml::diagram::DataModel) {
self.data_xml = data_model.to_xml();
}
pub fn set_point_text(
&mut self,
model_id: u32,
new_text: impl Into<String>,
) -> crate::Result<bool> {
let mut dm = self.data_model()?;
let ok = dm.set_point_text(model_id, new_text);
if ok {
self.data_xml = dm.to_xml();
}
Ok(ok)
}
}
impl Presentation {
pub fn new() -> crate::Result<Self> {
let id_counter = Rc::new(Cell::new(2));
let mut pres = Presentation {
slides: Slides::new(),
slide_layouts: SlideLayouts::new(),
slide_masters: SlideMasters::new(),
notes_masters: NotesMasters::new(),
theme: crate::oxml::theme::Theme::default(),
themes: Vec::new(),
sld_master_ids: Vec::new(),
notes_master_ids: Vec::new(),
width: Emu(DEFAULT_WIDTH_EMU),
height: Emu(DEFAULT_HEIGHT_EMU),
id_counter,
core_properties: CoreProperties {
application: Some("pptx-rs".to_string()),
..Default::default()
},
custom_properties: CustomProperties::default(),
comment_authors: crate::oxml::comments::CommentAuthorList::default(),
modify_protection: None,
sections: crate::oxml::section::SectionList::default(),
};
pres.ensure_default_master_and_layout()?;
Ok(pres)
}
pub fn open(path: impl AsRef<Path>) -> crate::Result<Self> {
Self::load(path.as_ref())
}
pub fn load(path: &Path) -> crate::Result<Self> {
let pkg = OpcPackage::load(path)?;
Self::from_opc(pkg)
}
pub fn load_bytes(bytes: &[u8]) -> crate::Result<Self> {
let mut pkg = OpcPackage::new();
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor)?;
let mut ct_xml = String::new();
zip.by_name("[Content_Types].xml")?
.read_to_string(&mut ct_xml)?;
pkg.content_types = crate::opc::package::parse_content_types_public(&ct_xml)?;
for i in 0..zip.len() {
let mut e = zip.by_index(i)?;
let name = e.name().to_string();
if name == "[Content_Types].xml" || e.is_dir() {
continue;
}
let mut blob = Vec::with_capacity(e.size() as usize);
e.read_to_end(&mut blob)?;
let ct_str: String = if name.ends_with(".rels") {
crate::opc::package::ct::RELATIONSHIPS.to_string()
} else {
let partname = format!("/{}", name);
crate::opc::package::derive_content_type(&pkg.content_types, &partname)
};
let partname = format!("/{}", name);
let p = Part::new(PartName::from_unchecked(partname), ct_str, blob);
pkg.parts.insert(p.partname.as_str().to_string(), p);
}
Self::from_opc(pkg)
}
fn from_opc(pkg: OpcPackage) -> crate::Result<Self> {
type DiagramTask = (
String,
String,
String,
String,
String,
String,
String,
String,
);
let id_counter = Rc::new(Cell::new(2));
let mut pres = Presentation {
slides: Slides::new(),
slide_layouts: SlideLayouts::new(),
slide_masters: SlideMasters::new(),
notes_masters: NotesMasters::new(),
theme: crate::oxml::theme::Theme::default(),
themes: Vec::new(),
sld_master_ids: Vec::new(),
notes_master_ids: Vec::new(),
width: Emu(DEFAULT_WIDTH_EMU),
height: Emu(DEFAULT_HEIGHT_EMU),
id_counter: id_counter.clone(),
core_properties: CoreProperties::default(),
custom_properties: CustomProperties::default(),
comment_authors: crate::oxml::comments::CommentAuthorList::default(),
modify_protection: None,
sections: crate::oxml::section::SectionList::default(),
};
pres.ensure_default_master_and_layout()?;
let pres_part = pkg
.get_part("/ppt/presentation.xml")
.ok_or_else(|| crate::Error::opc("presentation.xml not found in package"))?;
let pres_xml = String::from_utf8_lossy(&pres_part.blob).into_owned();
let (sld_id_list, sld_w, sld_h, sld_master_id_list, sections, notes_master_id_list) =
crate::oxml::parse_sld::parse_pres_root(&pres_xml)?;
pres.sld_master_ids = sld_master_id_list;
pres.sections = sections;
pres.notes_master_ids = notes_master_id_list;
if let (Some(w), Some(h)) = (sld_w, sld_h) {
pres.width = w;
pres.height = h;
}
let pres_rels_part = pkg
.get_part("/ppt/_rels/presentation.xml.rels")
.ok_or_else(|| crate::Error::opc("presentation.xml.rels not found"))?;
let pres_rels_xml = String::from_utf8_lossy(&pres_rels_part.blob).into_owned();
let pres_rels = Relationships::from_xml(&pres_rels_xml)?;
let mut parsed_masters: Vec<(String, String, String, String, OxmlSldMaster)> = Vec::new();
let mut parsed_layouts: Vec<(String, String, String, OxmlSldLayout)> = Vec::new();
for r in pres_rels.iter() {
if !matches!(r.reltype, RelType::SlideMaster) {
continue;
}
let master_partname =
resolve_relative_partname("/ppt/presentation.xml", r.target.as_str());
let master_xml = match pkg.get_part(master_partname.as_str()) {
Some(p) => String::from_utf8_lossy(&p.blob).into_owned(),
None => continue,
};
let master = match crate::oxml::parse_sld::parse_sld_master(&master_xml) {
Ok(m) => m,
Err(_e) => continue,
};
let mut current_theme_partname = String::new();
let mut current_theme_rid = String::new();
let master_rels_path = rels_partname_for(master_partname.as_str());
if let Some(mrp) = pkg.get_part(master_rels_path.as_str()) {
let mrp_xml = String::from_utf8_lossy(&mrp.blob).into_owned();
if let Ok(master_rels) = Relationships::from_xml(&mrp_xml) {
for mr in master_rels.iter() {
if matches!(mr.reltype, RelType::SlideLayout) {
let layout_partname = resolve_relative_partname(
master_partname.as_str(),
mr.target.as_str(),
);
let layout_xml = match pkg.get_part(layout_partname.as_str()) {
Some(p) => String::from_utf8_lossy(&p.blob).into_owned(),
None => continue,
};
if let Ok(layout) =
crate::oxml::parse_sld::parse_sld_layout(&layout_xml)
{
parsed_layouts.push((
mr.id.clone(),
layout_partname,
master_partname.clone(),
layout,
));
}
}
if matches!(mr.reltype, RelType::Theme) {
current_theme_partname = resolve_relative_partname(
master_partname.as_str(),
mr.target.as_str(),
);
current_theme_rid = mr.id.clone();
if let Some(tp) = pkg.get_part(current_theme_partname.as_str()) {
let theme_xml = String::from_utf8_lossy(&tp.blob).into_owned();
if let Ok(mut theme) =
crate::oxml::parse_sld::parse_theme(&theme_xml)
{
theme.partname = current_theme_partname.clone();
pres.themes.push(theme.clone());
if pres.theme.name.is_empty()
&& pres.theme.color_scheme.dk1.is_none()
{
pres.theme = theme;
}
}
}
}
}
}
}
parsed_masters.push((
r.id.clone(),
master_partname,
current_theme_partname,
current_theme_rid,
master,
));
}
if !parsed_masters.is_empty() {
pres.slide_masters.items.clear();
pres.slide_layouts.items.clear();
for (rid, partname, theme_partname, theme_rid, oxml) in parsed_masters {
let idx = pres.slide_masters.items.len();
pres.slide_masters.items.push(SlideMasterRef {
idx,
partname,
rid,
oxml: Rc::new(std::cell::RefCell::new(oxml)),
theme_partname,
theme_rid,
});
}
for (rid, partname, master_partname, oxml) in parsed_layouts {
let idx = pres.slide_layouts.items.len();
pres.slide_layouts.items.push(SlideLayoutRef {
idx,
partname,
rid,
oxml: Rc::new(std::cell::RefCell::new(oxml)),
master_partname,
});
}
}
for r in pres_rels.iter() {
if !matches!(r.reltype, RelType::NotesMaster) {
continue;
}
let nm_partname = resolve_relative_partname("/ppt/presentation.xml", r.target.as_str());
let nm_xml = match pkg.get_part(nm_partname.as_str()) {
Some(p) => String::from_utf8_lossy(&p.blob).into_owned(),
None => continue,
};
let nm = match crate::oxml::parse_sld::parse_notes_master(&nm_xml) {
Ok(m) => m,
Err(_e) => continue,
};
let idx = pres.notes_masters.items.len();
pres.notes_masters.items.push(NotesMasterRef {
idx,
partname: nm_partname,
rid: r.id.clone(),
oxml: Rc::new(std::cell::RefCell::new(nm)),
});
}
let mut max_id_seen: u32 = 2;
for (sld_id, rid) in &sld_id_list {
let rel = match pres_rels.get(rid) {
Some(r) => r,
None => {
continue;
}
};
let slide_partname =
resolve_relative_partname("/ppt/presentation.xml", rel.target.as_str());
let slide_part = match pkg.get_part(slide_partname.as_str()) {
Some(p) => p,
None => continue,
};
let slide_xml = String::from_utf8_lossy(&slide_part.blob).into_owned();
let mut sld = match crate::oxml::parse_sld::parse_sld(&slide_xml) {
Ok(s) => s,
Err(_e) => {
continue;
}
};
sld.id = *sld_id;
let rels_path = rels_partname_for(slide_partname.as_str());
let mut layout_rid = String::from("rId1");
let mut captured_layout_partname: Option<String> = None;
let mut notes_rid: Option<String> = None;
let mut notes_target_raw: Option<String> = None;
let mut comments_rid: Option<String> = None;
let mut comments_target_raw: Option<String> = None;
let mut image_rels: Vec<(String, String)> = Vec::new(); let mut diagram_rel_map: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
let mut chart_rel_map: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
if let Some(rels_part) = pkg.get_part(rels_path.as_str()) {
let rels_xml = String::from_utf8_lossy(&rels_part.blob).into_owned();
if let Ok(slide_rels) = Relationships::from_xml(&rels_xml) {
for r in slide_rels.iter() {
match r.reltype {
RelType::SlideLayout => {
layout_rid = r.id.clone();
let abs_layout = resolve_relative_partname(
slide_partname.as_str(),
r.target.as_str(),
);
captured_layout_partname = Some(abs_layout);
}
RelType::NotesSlide => {
notes_rid = Some(r.id.clone());
notes_target_raw = Some(r.target.as_str().to_string());
}
RelType::Comments => {
comments_rid = Some(r.id.clone());
comments_target_raw = Some(r.target.as_str().to_string());
}
RelType::Image => {
let rel_target = r.target.as_str();
let abs =
resolve_relative_partname(slide_partname.as_str(), rel_target);
image_rels.push((r.id.clone(), abs));
}
RelType::DiagramData
| RelType::DiagramLayout
| RelType::DiagramQuickStyle
| RelType::DiagramColors => {
let rel_target = r.target.as_str();
let abs =
resolve_relative_partname(slide_partname.as_str(), rel_target);
diagram_rel_map.insert(r.id.clone(), abs);
}
RelType::Chart => {
let rel_target = r.target.as_str();
let abs =
resolve_relative_partname(slide_partname.as_str(), rel_target);
chart_rel_map.insert(r.id.clone(), abs);
}
_ => {}
}
}
}
}
sld.set_layout_rid(layout_rid.clone());
let mut parsed_notes_partname: Option<String> = None;
let mut parsed_notes_rels_target: Option<String> = None;
if let Some(raw_target) = ¬es_target_raw {
let notes_partname =
resolve_relative_partname(slide_partname.as_str(), raw_target.as_str());
parsed_notes_partname = Some(notes_partname.clone());
let notes_rels_path = rels_partname_for(¬es_partname);
if let Some(nrp) = pkg.get_part(notes_rels_path.as_str()) {
let nrp_xml = String::from_utf8_lossy(&nrp.blob).into_owned();
if let Ok(nr) = Relationships::from_xml(&nrp_xml) {
for r in nr.iter() {
if matches!(r.reltype, RelType::Slide) {
parsed_notes_rels_target = Some(r.target.as_str().to_string());
break;
}
}
}
}
if let Some(np) = pkg.get_part(notes_partname.as_str()) {
let notes_xml = String::from_utf8_lossy(&np.blob).into_owned();
if let Ok(tb) = crate::oxml::parse_sld::parse_notes(¬es_xml) {
sld.notes = Some(tb);
}
}
}
let mut parsed_comments_partname: Option<String> = None;
let mut parsed_comments_lst: Option<crate::oxml::comments::CommentList> = None;
if let Some(raw_target) = &comments_target_raw {
let comments_partname =
resolve_relative_partname(slide_partname.as_str(), raw_target.as_str());
parsed_comments_partname = Some(comments_partname.clone());
if let Some(cp) = pkg.get_part(comments_partname.as_str()) {
let comments_xml = String::from_utf8_lossy(&cp.blob).into_owned();
if let Ok(lst) = crate::oxml::parse_sld::parse_comments(&comments_xml) {
parsed_comments_lst = Some(lst);
}
}
}
for shape in &sld.shapes {
let sp_id = match shape {
crate::oxml::SlideShape::Sp(sp) => sp.id,
crate::oxml::SlideShape::Pic(pic) => pic.id,
crate::oxml::SlideShape::CxnSp(c) => c.id,
crate::oxml::SlideShape::Group(g) => g.id,
crate::oxml::SlideShape::GraphicFrame(gf) => gf.id,
};
if sp_id > max_id_seen {
max_id_seen = sp_id;
}
}
let mut slide = Slide::from_sld(sld, id_counter.clone(), layout_rid);
if let Some(lp) = captured_layout_partname {
slide.set_layout_partname(lp);
}
if let Some(nrid) = notes_rid {
slide.set_notes_rid(nrid);
}
if let Some(np) = parsed_notes_partname {
slide.set_notes_partname(np);
}
if let Some(nrt) = parsed_notes_rels_target {
slide.set_notes_slide_rel_target(nrt);
}
if let Some(crid) = comments_rid {
slide.set_comments_rid(crid);
}
if let Some(cp) = parsed_comments_partname {
slide.set_comments_partname(cp);
}
if let Some(clst) = parsed_comments_lst {
slide.set_comments(Some(clst));
}
for (rid, target_partname) in &image_rels {
if let Some(media_part) = pkg.get_part(target_partname.as_str()) {
let partname = crate::opc::part::new_part_name(target_partname.as_str());
slide.register_media(MediaEntry {
partname,
content_type: media_part.content_type.clone(),
blob: media_part.blob.clone(),
rid: rid.clone(),
});
}
}
let mut diagram_tasks: Vec<DiagramTask> = Vec::new();
for shape in &slide.inner.shapes {
if let crate::oxml::SlideShape::GraphicFrame(gf) = shape {
if let crate::oxml::shape::Graphic::SmartArt(smart_ref) = &gf.graphic {
let (Some(dm_rid), Some(lo_rid), Some(qs_rid), Some(cs_rid)) = (
&smart_ref.dm_rid,
&smart_ref.lo_rid,
&smart_ref.qs_rid,
&smart_ref.cs_rid,
) else {
continue;
};
let (
Some(data_partname),
Some(layout_partname),
Some(quick_style_partname),
Some(colors_partname),
) = (
diagram_rel_map.get(dm_rid),
diagram_rel_map.get(lo_rid),
diagram_rel_map.get(qs_rid),
diagram_rel_map.get(cs_rid),
)
else {
continue;
};
diagram_tasks.push((
dm_rid.clone(),
lo_rid.clone(),
qs_rid.clone(),
cs_rid.clone(),
data_partname.clone(),
layout_partname.clone(),
quick_style_partname.clone(),
colors_partname.clone(),
));
}
}
}
for (
dm_rid,
lo_rid,
qs_rid,
cs_rid,
data_partname,
layout_partname,
quick_style_partname,
colors_partname,
) in diagram_tasks
{
let data_xml = pkg
.get_part(data_partname.as_str())
.map(|p| String::from_utf8_lossy(&p.blob).into_owned())
.unwrap_or_default();
let layout_xml = pkg
.get_part(layout_partname.as_str())
.map(|p| String::from_utf8_lossy(&p.blob).into_owned())
.unwrap_or_default();
let quick_style_xml = pkg
.get_part(quick_style_partname.as_str())
.map(|p| String::from_utf8_lossy(&p.blob).into_owned())
.unwrap_or_default();
let colors_xml = pkg
.get_part(colors_partname.as_str())
.map(|p| String::from_utf8_lossy(&p.blob).into_owned())
.unwrap_or_default();
let entry = DiagramEntry {
data_partname: crate::opc::part::new_part_name(data_partname.as_str()),
layout_partname: crate::opc::part::new_part_name(layout_partname.as_str()),
quick_style_partname: crate::opc::part::new_part_name(
quick_style_partname.as_str(),
),
colors_partname: crate::opc::part::new_part_name(colors_partname.as_str()),
data_xml,
layout_xml,
quick_style_xml,
colors_xml,
data_rid: dm_rid,
layout_rid: lo_rid,
quick_style_rid: qs_rid,
colors_rid: cs_rid,
};
slide.register_diagram(entry);
}
let mut chart_tasks: Vec<(String, String)> = Vec::new(); for shape in &slide.inner.shapes {
if let crate::oxml::SlideShape::GraphicFrame(gf) = shape {
if let crate::oxml::shape::Graphic::Chart(chart) = &gf.graphic {
let rid = chart.rid.as_str();
if rid.is_empty() {
continue;
}
if let Some(chart_partname) = chart_rel_map.get(rid) {
chart_tasks.push((rid.to_string(), chart_partname.clone()));
}
}
}
}
for (rid, chart_partname) in chart_tasks {
let Some(chart_part) = pkg.get_part(chart_partname.as_str()) else {
continue;
};
let chart_xml = String::from_utf8_lossy(&chart_part.blob).into_owned();
let Ok(mut parsed) = crate::oxml::chart::Chart::parse_from_xml(&chart_xml) else {
continue;
};
parsed.rid = rid.clone();
for s in slide.inner.shapes.iter_mut() {
if let crate::oxml::SlideShape::GraphicFrame(gf2) = s {
if let crate::oxml::shape::Graphic::Chart(c2) = &mut gf2.graphic {
if c2.rid == rid {
*c2 = parsed.clone();
break;
}
}
}
}
slide.register_chart(ChartEntry {
partname: crate::opc::part::new_part_name(chart_partname.as_str()),
chart: parsed,
rid,
xlsx_blob: None,
});
}
let entry = SlideEntry::new(slide, *sld_id, rid.clone(), slide_partname);
pres.slides.push_entry(entry);
}
pres.id_counter.set(max_id_seen);
if let Some(custom_part) = pkg.get_part("/docProps/custom.xml") {
let custom_xml = String::from_utf8_lossy(&custom_part.blob).into_owned();
pres.custom_properties = CustomProperties::from_xml(&custom_xml);
}
if let Some(authors_part) = pkg.get_part("/ppt/commentAuthors.xml") {
let authors_xml = String::from_utf8_lossy(&authors_part.blob).into_owned();
if let Ok(lst) = crate::oxml::parse_sld::parse_comment_authors(&authors_xml) {
pres.comment_authors = lst;
}
}
if let Some(core_part) = pkg.get_part("/docProps/core.xml") {
let core_xml = String::from_utf8_lossy(&core_part.blob).into_owned();
pres.core_properties = parse_core_xml(&core_xml);
}
if let Some(app_part) = pkg.get_part("/docProps/app.xml") {
let app_xml = String::from_utf8_lossy(&app_part.blob).into_owned();
if let Some(app) = parse_app_xml_application(&app_xml) {
pres.core_properties.application = Some(app);
}
}
Ok(pres)
}
pub fn slides(&self) -> &Slides {
&self.slides
}
pub fn slides_mut(&mut self) -> &mut Slides {
&mut self.slides
}
pub fn id_counter(&self) -> std::rc::Rc<std::cell::Cell<u32>> {
self.id_counter.clone()
}
pub fn slide_layouts(&self) -> &SlideLayouts {
&self.slide_layouts
}
pub fn slide_layouts_mut(&mut self) -> &mut SlideLayouts {
&mut self.slide_layouts
}
pub fn layout_for_slide(&self, slide_idx: usize) -> Option<SlideLayoutRef> {
let entry = self.slides.get(slide_idx)?;
let layout_rid = entry.sld.layout_rid();
self.slide_layouts
.items
.iter()
.find(|l| l.rid == layout_rid)
.cloned()
}
pub fn slides_using_layout(&self, layout_rid: &str) -> Vec<usize> {
self.slides
.iter()
.enumerate()
.filter(|(_, entry)| entry.sld.layout_rid() == layout_rid)
.map(|(i, _)| i)
.collect()
}
pub fn slide_masters(&self) -> &SlideMasters {
&self.slide_masters
}
pub fn slide_masters_mut(&mut self) -> &mut SlideMasters {
&mut self.slide_masters
}
pub fn slide_width(&self) -> Emu {
self.width
}
pub fn slide_height(&self) -> Emu {
self.height
}
pub fn set_slide_size(&mut self, width: Emu, height: Emu) {
self.width = width;
self.height = height;
}
pub fn core_properties(&self) -> &CoreProperties {
&self.core_properties
}
pub fn core_properties_mut(&mut self) -> &mut CoreProperties {
&mut self.core_properties
}
pub fn custom_properties(&self) -> &CustomProperties {
&self.custom_properties
}
pub fn custom_properties_mut(&mut self) -> &mut CustomProperties {
&mut self.custom_properties
}
pub fn comment_authors(&self) -> &crate::oxml::comments::CommentAuthorList {
&self.comment_authors
}
pub fn comment_authors_mut(&mut self) -> &mut crate::oxml::comments::CommentAuthorList {
&mut self.comment_authors
}
pub fn sections(&self) -> &crate::oxml::section::SectionList {
&self.sections
}
pub fn sections_mut(&mut self) -> &mut crate::oxml::section::SectionList {
&mut self.sections
}
pub fn notes_masters(&self) -> &NotesMasters {
&self.notes_masters
}
pub fn notes_masters_mut(&mut self) -> &mut NotesMasters {
&mut self.notes_masters
}
pub fn notes_master(&self) -> Option<&NotesMasterRef> {
self.notes_masters.first()
}
pub fn set_metadata(
&mut self,
title: Option<&str>,
creator: Option<&str>,
subject: Option<&str>,
keywords: Option<&str>,
) {
if let Some(t) = title {
self.core_properties.title = Some(t.to_string());
}
if let Some(c) = creator {
self.core_properties.creator = Some(c.to_string());
}
if let Some(s) = subject {
self.core_properties.subject = Some(s.to_string());
}
if let Some(k) = keywords {
self.core_properties.keywords = Some(k.to_string());
}
}
pub fn num_slides(&self) -> usize {
self.slides.len()
}
#[allow(clippy::field_reassign_with_default)]
pub fn add_watermark(
&mut self,
text: &str,
font_size_pt: Option<f64>,
color: Option<crate::units::RGBColor>,
rotation_deg: Option<i32>,
alpha: Option<i32>,
font_name: Option<&str>,
) -> crate::Result<()> {
let fs = font_size_pt.unwrap_or(36.0);
let clr = color.unwrap_or(crate::units::RGBColor(0xC0, 0xC0, 0xC0));
let rot = rotation_deg.unwrap_or(-30);
let alpha_val = alpha.unwrap_or(30_000);
let font = font_name.unwrap_or("Calibri");
let rot_emu = rot * 60000;
for i in 0..self.slides.len() {
let slide = self
.slides
.get_mut(i)
.ok_or(crate::Error::IndexOutOfRange(i))?;
let sld = &mut slide.sld;
let id = sld.next_shape_id();
let mut sp = crate::oxml::shape::Sp::default();
sp.id = id;
sp.name = format!("Watermark {}", i + 1);
sp.c_nv_sp_pr_tx_box = true;
let cx = self.width.0;
let cy = self.height.0;
sp.properties.xfrm.off_x = Some(Emu(cx / 4));
sp.properties.xfrm.off_y = Some(Emu(cy / 4));
sp.properties.xfrm.ext_cx = Some(Emu(cx / 2));
sp.properties.xfrm.ext_cy = Some(Emu(cy / 2));
sp.properties.xfrm.rot = Some(rot_emu);
sp.properties.geometry = Some(crate::oxml::sppr::Geometry::preset(
crate::oxml::simpletypes::PresetGeometry::Rectangle,
));
sp.properties.fill = crate::oxml::sppr::Fill::None;
sp.properties.line = None;
let mut tb = crate::oxml::txbody::TextBody::new();
let mut para = crate::oxml::txbody::Paragraph::default();
let mut run = crate::oxml::txbody::Run::default();
run.text = text.to_string();
run.properties.size = Some(crate::units::Pt(fs));
run.properties.color = crate::oxml::color::Color::RGB(clr);
run.properties.bold = true;
run.properties.alpha = Some(alpha_val);
run.properties.latin_font = Some(font.to_string());
run.properties.eastasia_font = Some("宋体".to_string());
para.runs.push(run);
para.properties.alignment = Some(crate::oxml::simpletypes::Alignment::Center);
tb.paragraphs.push(para);
sp.text = tb;
sld.inner.shapes.push(crate::oxml::SlideShape::Sp(sp));
}
Ok(())
}
#[allow(clippy::field_reassign_with_default)]
#[allow(clippy::too_many_arguments)]
pub fn add_image_watermark(
&mut self,
image_bytes: &[u8],
ext: &str,
alpha: Option<i32>,
left: Option<Emu>,
top: Option<Emu>,
width: Option<Emu>,
height: Option<Emu>,
) -> crate::Result<()> {
if image_bytes.is_empty() {
return Err(crate::Error::encryption("image bytes must not be empty"));
}
let alpha_val = alpha.unwrap_or(30_000);
let cx = self.width.0;
let cy = self.height.0;
let wm_left = left.unwrap_or(Emu(cx / 4));
let wm_top = top.unwrap_or(Emu(cy / 4));
let wm_width = width.unwrap_or(Emu(cx / 2));
let wm_height = height.unwrap_or(Emu(cy / 2));
let ext_norm = if ext.starts_with('.') {
ext.to_string()
} else {
format!(".{}", ext)
};
let global_media_idx = self
.slides
.get(0)
.map(|s| s.sld.next_media_index())
.unwrap_or(1);
let shared_partname = crate::opc::part::new_part_name(
format!("/ppt/media/image{}{}", global_media_idx, ext_norm).as_str(),
);
let ct = crate::shape::picture::content_type_for(&ext_norm);
for i in 0..self.slides.len() {
let slide = self
.slides
.get_mut(i)
.ok_or(crate::Error::IndexOutOfRange(i))?;
let id = slide.sld.next_shape_id();
let rid = slide.sld.allocate_image_rid();
let mut pic = crate::oxml::shape::Pic::default();
pic.id = id;
pic.name = format!("WatermarkImage {}", i + 1);
pic.rid = rid.clone();
pic.alpha = Some(alpha_val);
pic.fill_mode = crate::oxml::sppr::BlipFillMode::Stretch;
pic.properties.xfrm.off_x = Some(wm_left);
pic.properties.xfrm.off_y = Some(wm_top);
pic.properties.xfrm.ext_cx = Some(wm_width);
pic.properties.xfrm.ext_cy = Some(wm_height);
pic.properties.fill = crate::oxml::sppr::Fill::None;
pic.properties.line = None;
slide.sld.register_media(MediaEntry {
partname: shared_partname.clone(),
content_type: ct.to_string(),
blob: image_bytes.to_vec(),
rid,
});
slide
.sld
.inner
.shapes
.push(crate::oxml::SlideShape::Pic(pic));
}
Ok(())
}
pub fn set_write_protection(&mut self, password: &str) -> crate::Result<()> {
if password.is_empty() {
return Err(crate::Error::encryption("password must not be empty"));
}
let salt = crate::crypto::generate_random_bytes(crate::crypto::SALT_LEN);
self.modify_protection = Some(crate::crypto::ModifyProtection::from_password(
password,
&salt,
crate::crypto::MODIFY_SPIN_COUNT,
));
Ok(())
}
pub fn verify_write_protection(&self, password: &str) -> crate::Result<bool> {
match &self.modify_protection {
Some(mp) => Ok(mp.verify_password(password)),
None => Ok(false),
}
}
pub fn remove_write_protection(&mut self) {
self.modify_protection = None;
}
pub fn is_write_protected(&self) -> bool {
self.modify_protection.is_some()
}
pub fn save_encrypted(&self, path: impl AsRef<Path>, password: &str) -> crate::Result<()> {
let pkg = self.to_opc_package()?;
let zip_bytes = pkg.to_bytes()?;
let encrypted = crate::crypto::encrypt_package(&zip_bytes, password)?;
std::fs::write(path.as_ref(), &encrypted)?;
Ok(())
}
pub fn to_encrypted_bytes(&self, password: &str) -> crate::Result<Vec<u8>> {
let pkg = self.to_opc_package()?;
let zip_bytes = pkg.to_bytes()?;
crate::crypto::encrypt_package(&zip_bytes, password)
}
pub fn open_encrypted(path: impl AsRef<Path>, password: &str) -> crate::Result<Self> {
let bytes = std::fs::read(path.as_ref())?;
Self::load_encrypted_bytes(&bytes, password)
}
pub fn load_encrypted_bytes(bytes: &[u8], password: &str) -> crate::Result<Self> {
let decrypted = crate::crypto::decrypt_package(bytes, password)?;
Self::load_bytes(&decrypted)
}
pub fn is_encrypted_file(path: impl AsRef<Path>) -> crate::Result<bool> {
let bytes = std::fs::read(path.as_ref())?;
Ok(crate::crypto::is_encrypted_package(&bytes))
}
pub fn is_encrypted_bytes(bytes: &[u8]) -> bool {
crate::crypto::is_encrypted_package(bytes)
}
pub fn save(&self, path: impl AsRef<Path>) -> crate::Result<()> {
let pkg = self.to_opc_package()?;
Ok(pkg.save(path)?)
}
pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
let pkg = self.to_opc_package()?;
Ok(pkg.to_bytes()?)
}
fn to_opc_package(&self) -> crate::Result<OpcPackage> {
let mut pkg = OpcPackage::new();
let mut root_rels = Relationships::new();
root_rels.add(Relationship::internal(
"rId1",
RelType::OfficeDocument,
new_part_name("/ppt/presentation.xml"),
))?;
root_rels.add(Relationship::internal(
"rId2",
crate::opc::rels::RelType::Other(
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties".to_string(),
),
new_part_name("/docProps/core.xml"),
))?;
root_rels.add(Relationship::internal(
"rId3",
crate::opc::rels::RelType::Other(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties".to_string(),
),
new_part_name("/docProps/app.xml"),
))?;
if !self.custom_properties.is_empty() {
root_rels.add(Relationship::internal(
"rId4",
crate::opc::rels::RelType::Other(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties".to_string(),
),
new_part_name("/docProps/custom.xml"),
))?;
}
let root_rels_xml = root_rels.to_xml();
let root_rels_part = Part::new(
new_part_name("/_rels/.rels"),
ct::RELATIONSHIPS,
root_rels_xml.into_bytes(),
);
pkg.put_part(root_rels_part);
let cp = &self.core_properties;
let title = xml_escape(cp.title.as_deref().unwrap_or("pptx-rs"));
let creator = xml_escape(cp.creator.as_deref().unwrap_or("pptx-rs"));
let last_modified_by = xml_escape(cp.last_modified_by.as_deref().unwrap_or("pptx-rs"));
let mut core_xml = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:dcmitype="http://purl.org/dc/dcmitype/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dc:title>{}</dc:title>
<dc:creator>{}</dc:creator>
<cp:lastModifiedBy>{}</cp:lastModifiedBy>"#,
title, creator, last_modified_by
);
if let Some(s) = &cp.subject {
core_xml.push_str(&format!("\n <dc:subject>{}</dc:subject>", xml_escape(s)));
}
if let Some(k) = &cp.keywords {
core_xml.push_str(&format!("\n <cp:keywords>{}</cp:keywords>", xml_escape(k)));
}
if let Some(c) = &cp.category {
core_xml.push_str(&format!("\n <cp:category>{}</cp:category>", xml_escape(c)));
}
if let Some(d) = &cp.description {
core_xml.push_str(&format!(
"\n <dc:description>{}</dc:description>",
xml_escape(d)
));
}
if let Some(r) = &cp.revision {
core_xml.push_str(&format!("\n <cp:revision>{}</cp:revision>", xml_escape(r)));
}
if let Some(c) = &cp.created {
core_xml.push_str(&format!(
"\n <dcterms:created xsi:type=\"dcterms:W3CDTF\">{}</dcterms:created>",
xml_escape(c)
));
}
if let Some(m) = &cp.modified {
core_xml.push_str(&format!(
"\n <dcterms:modified xsi:type=\"dcterms:W3CDTF\">{}</dcterms:modified>",
xml_escape(m)
));
}
core_xml.push_str("\n</cp:coreProperties>");
pkg.put_part(Part::new(
new_part_name("/docProps/core.xml"),
ct::CORE_PROPS,
core_xml.into_bytes(),
));
let app = xml_escape(cp.application.as_deref().unwrap_or("pptx-rs"));
let app_xml = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
<Application>{}</Application>
<AppVersion>1.0</AppVersion>
</Properties>"#,
app
);
pkg.put_part(Part::new(
new_part_name("/docProps/app.xml"),
ct::APP_PROPS,
app_xml.into_bytes(),
));
if !self.custom_properties.is_empty() {
let custom_xml = self.custom_properties.to_xml();
if !custom_xml.is_empty() {
pkg.put_part(Part::new(
new_part_name("/docProps/custom.xml"),
ct::CUSTOM_PROPS,
custom_xml.into_bytes(),
));
}
}
if self.themes.is_empty() {
let theme_xml = if self.theme.name.is_empty()
&& self.theme.color_scheme.dk1.is_none()
&& self.theme.font_scheme.major_latin.is_empty()
{
default_theme_xml()
} else {
self.theme.to_xml()
};
pkg.put_part(Part::new(
new_part_name("/ppt/theme/theme1.xml"),
ct::THEME,
theme_xml.into_bytes(),
));
} else {
for theme in &self.themes {
let theme_partname = if theme.partname.is_empty() {
new_part_name("/ppt/theme/theme1.xml")
} else {
new_part_name(theme.partname.as_str())
};
pkg.put_part(Part::new(
theme_partname,
ct::THEME,
theme.to_xml().into_bytes(),
));
}
}
if self.slide_masters.items.is_empty() {
let master_xml = OxmlSldMaster::default().to_xml();
pkg.put_part(Part::new(
new_part_name("/ppt/slideMasters/slideMaster1.xml"),
ct::SLIDE_MASTER,
master_xml.into_bytes(),
));
} else {
for (i, master_ref) in self.slide_masters.items.iter().enumerate() {
let master_partname_str = if master_ref.partname.is_empty() {
format!("/ppt/slideMasters/slideMaster{}.xml", i + 1)
} else {
master_ref.partname.clone()
};
let master_partname = new_part_name(master_partname_str.as_str());
let master_xml = master_ref.oxml.borrow().to_xml();
let mut master_rels = Relationships::new();
let master_partname_for_rel = master_partname_str.clone();
for layout_ref in &self.slide_layouts.items {
let layout_belongs_to_this_master = layout_ref.master_partname
== master_ref.partname
|| (layout_ref.master_partname.is_empty() && i == 0);
if layout_belongs_to_this_master {
let layout_target =
relative_part_path(&master_partname_for_rel, &layout_ref.partname);
master_rels.add(Relationship::internal_str(
layout_ref.rid.as_str(),
RelType::SlideLayout,
layout_target.as_str(),
))?;
}
}
let master_layout_rids_empty = master_ref.oxml.borrow().layout_rids.is_empty();
let has_matching_layout = self.slide_layouts.items.iter().any(|l| {
l.master_partname == master_ref.partname
|| (l.master_partname.is_empty() && i == 0)
});
if master_layout_rids_empty && !has_matching_layout && i == 0 {
master_rels.add(Relationship::internal_str(
"rId1",
RelType::SlideLayout,
"../slideLayouts/slideLayout1.xml",
))?;
}
let theme_rid = if master_ref.theme_rid.is_empty() {
"rId2".to_string()
} else {
master_ref.theme_rid.clone()
};
let theme_target = if master_ref.theme_partname.is_empty() {
"../theme/theme1.xml".to_string()
} else {
relative_part_path(&master_partname_for_rel, &master_ref.theme_partname)
};
master_rels.add(Relationship::internal_str(
theme_rid.as_str(),
RelType::Theme,
theme_target.as_str(),
))?;
let master_rels_xml = master_rels.to_xml();
let master_rels_partname = rels_partname_for(master_partname.as_str());
pkg.put_part(Part::new(
PartName::from_unchecked(master_rels_partname),
ct::RELATIONSHIPS,
master_rels_xml.into_bytes(),
));
pkg.put_part(Part::new(
master_partname,
ct::SLIDE_MASTER,
master_xml.into_bytes(),
));
}
}
if self.slide_layouts.items.is_empty() {
let layout_xml = OxmlSldLayout::default().to_xml();
pkg.put_part(Part::new(
new_part_name("/ppt/slideLayouts/slideLayout1.xml"),
ct::SLIDE_LAYOUT,
layout_xml.into_bytes(),
));
} else {
for (i, layout_ref) in self.slide_layouts.items.iter().enumerate() {
let layout_partname_str = if layout_ref.partname.is_empty() {
format!("/ppt/slideLayouts/slideLayout{}.xml", i + 1)
} else {
layout_ref.partname.clone()
};
let layout_partname = new_part_name(layout_partname_str.as_str());
let layout_xml = layout_ref.oxml.borrow().to_xml();
let mut layout_rels = Relationships::new();
let master_target = if layout_ref.master_partname.is_empty() {
"../slideMasters/slideMaster1.xml".to_string()
} else {
relative_part_path(&layout_partname_str, &layout_ref.master_partname)
};
layout_rels.add(Relationship::internal_str(
"rId1",
RelType::SlideMaster,
master_target.as_str(),
))?;
let layout_rels_partname = rels_partname_for(layout_partname.as_str());
pkg.put_part(Part::new(
PartName::from_unchecked(layout_rels_partname),
ct::RELATIONSHIPS,
layout_rels.to_xml().into_bytes(),
));
pkg.put_part(Part::new(
layout_partname,
ct::SLIDE_LAYOUT,
layout_xml.into_bytes(),
));
}
}
if !self.notes_masters.items.is_empty() {
for (i, nm_ref) in self.notes_masters.items.iter().enumerate() {
let nm_partname_str = if nm_ref.partname.is_empty() {
format!("/ppt/notesMasters/notesMaster{}.xml", i + 1)
} else {
nm_ref.partname.clone()
};
let nm_partname = new_part_name(nm_partname_str.as_str());
let nm_xml = nm_ref.oxml.borrow().to_xml();
pkg.put_part(Part::new(
nm_partname,
ct::NOTES_MASTER,
nm_xml.into_bytes(),
));
}
}
let mut pres_rels = Relationships::new();
if !self.sld_master_ids.is_empty() {
for (i, (_id, rid)) in self.sld_master_ids.iter().enumerate() {
let target = self
.slide_masters
.items
.get(i)
.map(|m| {
m.partname
.strip_prefix("/ppt/")
.unwrap_or(&m.partname)
.to_string()
})
.unwrap_or_else(|| format!("slideMasters/slideMaster{}.xml", i + 1));
pres_rels.add(Relationship::internal_str(
rid,
RelType::SlideMaster,
&target,
))?;
}
} else {
pres_rels.add(Relationship::internal_str(
"rIdP1",
RelType::SlideMaster,
"slideMasters/slideMaster1.xml",
))?;
}
for (i, (_id, rid)) in self.notes_master_ids.iter().enumerate() {
let target = self
.notes_masters
.items
.get(i)
.map(|nm| {
nm.partname
.strip_prefix("/ppt/")
.unwrap_or(&nm.partname)
.to_string()
})
.unwrap_or_else(|| format!("notesMasters/notesMaster{}.xml", i + 1));
pres_rels.add(Relationship::internal_str(
rid,
RelType::NotesMaster,
&target,
))?;
}
pres_rels.add(Relationship::internal_str(
"rIdP4",
crate::opc::rels::RelType::Other(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"
.to_string(),
),
"presProps.xml",
))?;
pres_rels.add(Relationship::internal_str(
"rIdP5",
crate::opc::rels::RelType::Other(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"
.to_string(),
),
"viewProps.xml",
))?;
pres_rels.add(Relationship::internal_str(
"rIdP6",
crate::opc::rels::RelType::Other(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"
.to_string(),
),
"tableStyles.xml",
))?;
let sld_master_ids: Vec<crate::oxml::presentation::SldMasterIdEntry> =
if !self.sld_master_ids.is_empty() {
self.sld_master_ids
.iter()
.map(|(id, rid)| crate::oxml::presentation::SldMasterIdEntry {
id: *id,
rid: rid.clone(),
})
.collect()
} else {
Vec::new()
};
let mut pres_root = PresentationRoot {
slide_width: Some(self.width),
slide_height: Some(self.height),
slide_ids: Vec::new(),
sld_master_ids,
notes_master_ids: self
.notes_master_ids
.iter()
.map(|(id, rid)| crate::oxml::presentation::NotesMasterIdEntry {
id: *id,
rid: rid.clone(),
})
.collect(),
sections: self.sections.clone(),
..Default::default()
};
let mut notes_index: u32 = 0;
let mut comments_index: u32 = 0;
let mut chart_global_index: u32 = 0;
let mut chart_xlsx_global_index: u32 = 0;
let mut ole_global_index: u32 = 0;
let mut video_global_index: u32 = 0;
let mut audio_global_index: u32 = 0;
let mut diagram_global_index: u32 = 0;
for (i, entry) in self.slides.iter().enumerate() {
let partname = PartName::from_unchecked(entry.partname.clone());
let rid = entry.rid.clone();
let mut sld = entry.sld.clone();
let slide_layout_rid = {
let lr = entry.sld.layout_rid();
if lr.is_empty() {
"rId1".to_string()
} else {
lr
}
};
let slide_layout_target = match entry.sld.layout_partname() {
Some(lp) => relative_part_path(&entry.partname, lp),
None => "../slideLayouts/slideLayout1.xml".to_string(),
};
sld.set_layout_rid(slide_layout_rid.clone());
let mut extra_rels = Relationships::new();
extra_rels.add(Relationship::internal_str(
slide_layout_rid.as_str(),
RelType::SlideLayout,
slide_layout_target.as_str(),
))?;
for media in &entry.sld.media_entries {
let media_target = format!(
"../media/{}",
media.partname.as_str().trim_start_matches("/ppt/media/")
);
extra_rels.add(Relationship::internal_str(
media.rid.clone(),
RelType::Image,
media_target,
))?;
}
let mut this_notes_rid: Option<String> = None;
if let Some(notes_tb) = sld.notes() {
notes_index += 1;
let notes_rid = format!("rIdNotes{}", notes_index);
let notes_partname = match entry.sld.notes_partname() {
Some(p) => p.to_string(),
None => format!("/ppt/notesSlides/notesSlide{}.xml", notes_index),
};
let notes_slide_target = match entry.sld.notes_slide_rel_target() {
Some(t) => t.to_string(),
None => format!("../slides/slide{}.xml", i + 1),
};
this_notes_rid = Some(notes_rid.clone());
let notes_target_rel = {
let trimmed = notes_partname.trim_start_matches("/ppt/");
format!("../{}", trimmed)
};
extra_rels.add(Relationship::internal_str(
notes_rid.clone(),
RelType::NotesSlide,
notes_target_rel,
))?;
let pres_target_rel = {
let trimmed = notes_partname.trim_start_matches("/ppt/");
trimmed.to_string()
};
pres_rels.add(Relationship::internal_str(
format!("rIdNotesPres{}", notes_index),
RelType::NotesSlide,
pres_target_rel,
))?;
let mut notes_rels = Relationships::new();
notes_rels.add(Relationship::internal_str(
"rId1",
RelType::Slide,
notes_slide_target,
))?;
let notes_rels_partname = rels_partname_for(¬es_partname);
pkg.put_part(Part::new(
PartName::from_unchecked(notes_rels_partname),
ct::RELATIONSHIPS,
notes_rels.to_xml().into_bytes(),
));
let notes_xml_str = crate::oxml::notes_xml(notes_tb);
pkg.put_part(Part::new(
PartName::from_unchecked(notes_partname),
ct::NOTES_SLIDE,
notes_xml_str.into_bytes(),
));
}
if let Some(rid) = this_notes_rid {
sld.set_notes_rid(rid);
}
let mut this_comments_rid: Option<String> = None;
if let Some(comment_lst) = sld.comments() {
comments_index += 1;
let comments_rid = format!("rIdComments{}", comments_index);
let comments_partname = match sld.comments_partname() {
Some(p) => p.to_string(),
None => format!("/ppt/comments/comment{}.xml", comments_index),
};
this_comments_rid = Some(comments_rid.clone());
let comments_target_rel = {
let trimmed = comments_partname.trim_start_matches("/ppt/");
format!("../{}", trimmed)
};
extra_rels.add(Relationship::internal_str(
comments_rid.clone(),
RelType::Comments,
comments_target_rel,
))?;
let comments_xml_str = comment_lst.to_xml();
pkg.put_part(Part::new(
PartName::from_unchecked(comments_partname),
ct::COMMENTS,
comments_xml_str.into_bytes(),
));
}
if let Some(rid) = this_comments_rid {
sld.set_comments_rid(rid);
}
for chart_entry in &entry.sld.chart_entries {
chart_global_index += 1;
let chart_partname = format!("/ppt/charts/chart{}.xml", chart_global_index);
let chart_target_rel = format!("../charts/chart{}.xml", chart_global_index);
extra_rels.add(Relationship::internal_str(
chart_entry.rid.clone(),
RelType::Chart,
chart_target_rel,
))?;
let mut chart_model = chart_entry.chart.clone();
if let Some(xlsx_blob) = &chart_entry.xlsx_blob {
chart_xlsx_global_index += 1;
let xlsx_partname = format!(
"/ppt/embeddings/Microsoft_Excel_Worksheet{}.xlsx",
chart_xlsx_global_index
);
let xlsx_target_rel = format!(
"../embeddings/Microsoft_Excel_Worksheet{}.xlsx",
chart_xlsx_global_index
);
let xlsx_rid = format!("rIdXlsx{}", chart_xlsx_global_index);
pkg.put_part(Part::new(
PartName::from_unchecked(xlsx_partname.clone()),
ct::SPREADSHEET_XLSX,
xlsx_blob.clone(),
));
let mut chart_rels = crate::opc::rels::Relationships::new();
chart_rels.add(Relationship::internal_str(
xlsx_rid.clone(),
RelType::Package,
xlsx_target_rel,
))?;
let chart_rels_partname =
format!("/ppt/charts/_rels/chart{}.xml.rels", chart_global_index);
pkg.put_part(Part::new(
PartName::from_unchecked(chart_rels_partname),
ct::RELATIONSHIPS,
chart_rels.to_xml().into_bytes(),
));
chart_model.external_data_rid = Some(xlsx_rid);
}
let chart_xml = chart_model.to_xml();
pkg.put_part(Part::new(
PartName::from_unchecked(chart_partname),
ct::CHART,
chart_xml.into_bytes(),
));
}
for ole_entry in &entry.sld.ole_entries {
ole_global_index += 1;
let ole_partname = format!("/ppt/embeddings/oleObject{}.bin", ole_global_index);
let ole_target_rel = format!("../embeddings/oleObject{}.bin", ole_global_index);
extra_rels.add(Relationship::internal_str(
ole_entry.rid.clone(),
RelType::OleObject,
ole_target_rel,
))?;
pkg.put_part(Part::new(
PartName::from_unchecked(ole_partname),
ct::OLE_OBJECT,
ole_entry.blob.clone(),
));
}
for video_entry in &entry.sld.video_entries {
video_global_index += 1;
let video_partname = format!("/ppt/media/media{}.mp4", video_global_index);
let video_target_rel = format!("../media/media{}.mp4", video_global_index);
extra_rels.add(Relationship::internal_str(
video_entry.rid.clone(),
RelType::Video,
video_target_rel,
))?;
pkg.put_part(Part::new(
PartName::from_unchecked(video_partname),
ct::VIDEO_MP4,
video_entry.blob.clone(),
));
}
for audio_entry in &entry.sld.audio_entries {
audio_global_index += 1;
let audio_partname = format!("/ppt/media/media{}.mp3", audio_global_index);
let audio_target_rel = format!("../media/media{}.mp3", audio_global_index);
extra_rels.add(Relationship::internal_str(
audio_entry.rid.clone(),
RelType::Audio,
audio_target_rel,
))?;
pkg.put_part(Part::new(
PartName::from_unchecked(audio_partname),
ct::AUDIO_MP3,
audio_entry.blob.clone(),
));
}
for diagram_entry in &entry.sld.diagram_entries {
diagram_global_index += 1;
let idx = diagram_global_index;
let data_partname = format!("/ppt/diagrams/data{}.xml", idx);
let layout_partname = format!("/ppt/diagrams/layout{}.xml", idx);
let quick_style_partname = format!("/ppt/diagrams/quickStyles{}.xml", idx);
let colors_partname = format!("/ppt/diagrams/colors{}.xml", idx);
let data_target_rel = format!("../diagrams/data{}.xml", idx);
let layout_target_rel = format!("../diagrams/layout{}.xml", idx);
let quick_style_target_rel = format!("../diagrams/quickStyles{}.xml", idx);
let colors_target_rel = format!("../diagrams/colors{}.xml", idx);
extra_rels.add(Relationship::internal_str(
diagram_entry.data_rid.clone(),
RelType::DiagramData,
data_target_rel,
))?;
extra_rels.add(Relationship::internal_str(
diagram_entry.layout_rid.clone(),
RelType::DiagramLayout,
layout_target_rel,
))?;
extra_rels.add(Relationship::internal_str(
diagram_entry.quick_style_rid.clone(),
RelType::DiagramQuickStyle,
quick_style_target_rel,
))?;
extra_rels.add(Relationship::internal_str(
diagram_entry.colors_rid.clone(),
RelType::DiagramColors,
colors_target_rel,
))?;
pkg.put_part(Part::new(
PartName::from_unchecked(data_partname),
ct::DIAGRAM_DATA,
diagram_entry.data_xml.clone().into_bytes(),
));
pkg.put_part(Part::new(
PartName::from_unchecked(layout_partname),
ct::DIAGRAM_LAYOUT,
diagram_entry.layout_xml.clone().into_bytes(),
));
pkg.put_part(Part::new(
PartName::from_unchecked(quick_style_partname),
ct::DIAGRAM_QUICK_STYLE,
diagram_entry.quick_style_xml.clone().into_bytes(),
));
pkg.put_part(Part::new(
PartName::from_unchecked(colors_partname),
ct::DIAGRAM_COLORS,
diagram_entry.colors_xml.clone().into_bytes(),
));
}
let extra_rels_xml = extra_rels.to_xml();
let extra_rels_partname = rels_partname_for(partname.as_str());
pkg.put_part(Part::new(
PartName::from_unchecked(extra_rels_partname),
ct::RELATIONSHIPS,
extra_rels_xml.into_bytes(),
));
pkg.put_part(Part::new(partname, ct::SLIDE, sld.to_xml().into_bytes()));
pres_rels.add(Relationship::internal_str(
rid.clone(),
RelType::Slide,
format!("slides/slide{}.xml", i + 1),
))?;
pres_root.slide_ids.push(SlideIdEntry {
id: entry.sld_id,
rid: rid.clone(),
});
}
if !self.comment_authors.is_empty() {
let authors_xml = self.comment_authors.to_xml();
pkg.put_part(Part::new(
new_part_name("/ppt/commentAuthors.xml"),
ct::COMMENT_AUTHORS,
authors_xml.into_bytes(),
));
pres_rels.add(Relationship::internal_str(
"rIdCommentAuthors",
RelType::CommentAuthors,
"commentAuthors.xml",
))?;
}
let pres_xml = if let Some(ref mp) = self.modify_protection {
inject_modify_verifier(&pres_root.to_xml(), &mp.to_xml_element())
} else {
pres_root.to_xml()
};
pkg.put_part(Part::new(
new_part_name("/ppt/presentation.xml"),
ct::PRESENTATION,
pres_xml.into_bytes(),
));
pkg.put_part(Part::new(
new_part_name("/ppt/_rels/presentation.xml.rels"),
ct::RELATIONSHIPS,
pres_rels.to_xml().into_bytes(),
));
use std::collections::BTreeMap;
let mut seen: BTreeMap<String, ()> = BTreeMap::new();
for entry in self.slides.iter() {
for m in &entry.sld.media_entries {
if seen.insert(m.partname.as_str().to_string(), ()).is_none() {
pkg.put_part(Part::new(
m.partname.clone(),
m.content_type.clone(),
m.blob.clone(),
));
}
}
}
let pres_props_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:presentationPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:showPr/>
</p:presentationPr>"#;
pkg.put_part(Part::new(
new_part_name("/ppt/presProps.xml"),
"application/vnd.openxmlformats-officedocument.presentationml.presProps+xml",
pres_props_xml.as_bytes().to_vec(),
));
let view_props_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:viewProps xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:normalViewPr><p:restoredLeft sz="15620"/><p:restoredTop sz="94660"/></p:normalViewPr>
</p:viewProps>"#;
pkg.put_part(Part::new(
new_part_name("/ppt/viewProps.xml"),
"application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml",
view_props_xml.as_bytes().to_vec(),
));
let table_styles_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" defStyleStyle=""/>
"#;
pkg.put_part(Part::new(
new_part_name("/ppt/tableStyles.xml"),
"application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml",
table_styles_xml.as_bytes().to_vec(),
));
Ok(pkg)
}
fn ensure_default_master_and_layout(&mut self) -> crate::Result<()> {
if self.slide_masters.is_empty() {
self.slide_masters.items.push(SlideMasterRef {
idx: 0,
partname: "/ppt/slideMasters/slideMaster1.xml".to_string(),
rid: "rIdMaster1".to_string(),
oxml: Rc::new(std::cell::RefCell::new(OxmlSldMaster::default())),
theme_partname: String::new(),
theme_rid: String::new(),
});
}
if self.slide_layouts.is_empty() {
self.slide_layouts.items.push(SlideLayoutRef {
idx: 0,
partname: "/ppt/slideLayouts/slideLayout1.xml".to_string(),
rid: "rIdLayout1".to_string(),
oxml: Rc::new(std::cell::RefCell::new(OxmlSldLayout::default())),
master_partname: String::new(),
});
}
Ok(())
}
}
fn resolve_relative_partname(parent_partname: &str, rel_target: &str) -> String {
if rel_target.starts_with('/') {
return rel_target.to_string();
}
let parent_dir = match parent_partname.rfind('/') {
Some(i) => &parent_partname[..i],
None => "",
};
let mut stack: Vec<&str> = Vec::new();
let combined = if parent_dir.is_empty() {
format!("/{}", rel_target)
} else {
format!("{}/{}", parent_dir, rel_target)
};
for seg in combined.split('/') {
match seg {
"" | "." => continue,
".." => {
stack.pop();
}
_ => stack.push(seg),
}
}
let mut out = String::from("/");
out.push_str(&stack.join("/"));
if out == "/" && !stack.is_empty() {
}
out
}
fn relative_part_path(src_partname: &str, dst_partname: &str) -> String {
let src_owned;
let src = if src_partname.starts_with('/') {
src_partname
} else {
src_owned = format!("/{}", src_partname);
&src_owned
};
let dst_owned;
let dst = if dst_partname.starts_with('/') {
dst_partname
} else {
dst_owned = format!("/{}", dst_partname);
&dst_owned
};
let src_dir = match src.rfind('/') {
Some(i) => &src[..i],
None => "",
};
let dst_dir = match dst.rfind('/') {
Some(i) => &dst[..i],
None => "",
};
let dst_basename = match dst.rfind('/') {
Some(i) => &dst[i + 1..],
None => dst,
};
let src_segs: Vec<&str> = src_dir.split('/').filter(|s| !s.is_empty()).collect();
let dst_segs: Vec<&str> = dst_dir.split('/').filter(|s| !s.is_empty()).collect();
let common = src_segs
.iter()
.zip(dst_segs.iter())
.take_while(|(a, b)| a == b)
.count();
let up_count = src_segs.len() - common;
let mut result = String::new();
for _ in 0..up_count {
result.push_str("../");
}
for seg in &dst_segs[common..] {
result.push_str(seg);
result.push('/');
}
result.push_str(dst_basename);
result
}
fn inject_modify_verifier(pres_xml: &str, verifier_xml: &str) -> String {
if pres_xml.contains("p:modifyVerifier") {
return pres_xml.to_string();
}
if let Some(pos) = pres_xml.find("<p:extLst") {
let mut out = String::with_capacity(pres_xml.len() + verifier_xml.len());
out.push_str(&pres_xml[..pos]);
out.push_str(verifier_xml);
out.push_str(&pres_xml[pos..]);
return out;
}
if let Some(pos) = pres_xml.rfind("</p:presentation>") {
let mut out = String::with_capacity(pres_xml.len() + verifier_xml.len());
out.push_str(&pres_xml[..pos]);
out.push_str(verifier_xml);
out.push_str(&pres_xml[pos..]);
return out;
}
pres_xml.to_string()
}
impl Default for Presentation {
#[allow(clippy::expect_used)]
fn default() -> Self {
Self::new().expect("Presentation::new() is infallible in current implementation")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::units::Inches;
#[test]
fn resolve_relative_partname_examples() {
assert_eq!(
resolve_relative_partname("/ppt/slides/slide1.xml", "../notesSlides/notesSlide1.xml"),
"/ppt/notesSlides/notesSlide1.xml"
);
assert_eq!(
resolve_relative_partname("/ppt/slides/slide1.xml", "media/image1.png"),
"/ppt/slides/media/image1.png"
);
assert_eq!(
resolve_relative_partname("/ppt/slides/slide1.xml", "../media/image1.png"),
"/ppt/media/image1.png"
);
assert_eq!(
resolve_relative_partname("/ppt/slides/slide1.xml", "/ppt/notesSlides/n1.xml"),
"/ppt/notesSlides/n1.xml"
);
assert_eq!(
resolve_relative_partname("/ppt/slides/slide1.xml", "slide2.xml"),
"/ppt/slides/slide2.xml"
);
}
#[test]
fn relative_part_path_examples() {
assert_eq!(
relative_part_path(
"/ppt/slideMasters/slideMaster1.xml",
"/ppt/slideLayouts/slideLayout1.xml"
),
"../slideLayouts/slideLayout1.xml"
);
assert_eq!(
relative_part_path(
"/ppt/slideMasters/slideMaster1.xml",
"/ppt/theme/theme1.xml"
),
"../theme/theme1.xml"
);
assert_eq!(
relative_part_path(
"/ppt/slides/slide1.xml",
"/ppt/slideLayouts/slideLayout1.xml"
),
"../slideLayouts/slideLayout1.xml"
);
assert_eq!(
relative_part_path(
"/ppt/slideLayouts/slideLayout1.xml",
"/ppt/slideMasters/slideMaster1.xml"
),
"../slideMasters/slideMaster1.xml"
);
assert_eq!(
relative_part_path("/ppt/slides/slide1.xml", "/ppt/slides/slide2.xml"),
"slide2.xml"
);
assert_eq!(
relative_part_path("ppt/slides/slide1.xml", "ppt/slideLayouts/slideLayout1.xml"),
"../slideLayouts/slideLayout1.xml"
);
}
#[test]
fn roundtrip_presentation_loads_back() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let slide = p.slides_mut().add_slide(counter).expect("add slide");
slide
.shapes_mut()
.add_textbox_with_text(
Inches(1.0),
Inches(1.0),
Inches(4.0),
Inches(1.0),
"Hello roundtrip",
)
.expect("textbox");
slide.set_notes_text(Some("speaker note line 1\nline 2"));
p.set_slide_size(Emu(7_000_000), Emu(5_000_000));
let bytes = p.to_bytes().expect("to_bytes ok");
assert!(!bytes.is_empty());
let p2 = Presentation::load_bytes(&bytes).expect("load_bytes ok");
assert_eq!(p2.slides().len(), 1);
let s = p2.slides().get(0).expect("slide 0");
assert_eq!(s.sld.layout_rid(), "rIdLayout1");
let inner_shapes = &s.sld.inner.shapes;
assert_eq!(inner_shapes.len(), 1);
if let crate::oxml::SlideShape::Sp(sp) = &inner_shapes[0] {
assert_eq!(sp.text.paragraphs.len(), 1);
assert_eq!(sp.text.paragraphs[0].runs[0].text, "Hello roundtrip");
} else {
panic!("expected Sp");
}
let notes = s.sld.notes_text().expect("notes present");
assert!(notes.contains("speaker note line 1"));
assert_eq!(p2.slide_width().0, 7_000_000);
assert_eq!(p2.slide_height().0, 5_000_000);
}
#[test]
fn load_then_resave_preserves_content() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let slide = p.slides_mut().add_slide(counter).expect("add slide");
slide
.shapes_mut()
.add_textbox_with_text(Inches(0.5), Inches(0.5), Inches(6.0), Inches(0.8), "stable")
.expect("textbox");
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load 1");
let bytes2 = p2.to_bytes().expect("to_bytes 2");
let p3 = Presentation::load_bytes(&bytes2).expect("load 2");
assert_eq!(p3.slides().len(), 1);
let s = p3.slides().get(0).expect("slide 0");
let inner_shapes = &s.sld.inner.shapes;
if let crate::oxml::SlideShape::Sp(sp) = &inner_shapes[0] {
assert_eq!(sp.text.paragraphs[0].runs[0].text, "stable");
} else {
panic!("expected Sp");
}
}
#[test]
fn notes_roundtrip_preserves_partname_and_rels_target() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let slide = p.slides_mut().add_slide(counter).expect("add slide");
slide
.shapes_mut()
.add_textbox_with_text(
Inches(0.5),
Inches(0.5),
Inches(6.0),
Inches(0.8),
"roundtrip notes test",
)
.expect("textbox");
slide.set_notes_text(Some("first notes\nline 2"));
let bytes1 = p.to_bytes().expect("to_bytes 1");
let mut p2 = Presentation::load_bytes(&bytes1).expect("load 1");
p2.slides_mut()
.get_mut(0)
.expect("slide 0")
.sld
.set_notes_text(Some("updated notes"));
let bytes2 = p2.to_bytes().expect("to_bytes 2");
let p3 = Presentation::load_bytes(&bytes2).expect("load 2");
let s = p3.slides().get(0).expect("slide 0");
let notes_text = s.sld.notes_text().expect("notes present");
assert!(notes_text.contains("updated notes"));
let cursor = std::io::Cursor::new(bytes2);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
let mut nrels_xml = String::new();
zip.by_name("ppt/notesSlides/_rels/notesSlide1.xml.rels")
.expect("notesSlide1.xml.rels exists")
.read_to_string(&mut nrels_xml)
.expect("read notes rels");
assert!(
nrels_xml.contains("../slides/slide1.xml"),
"notesSlide1.xml.rels missing slide rels, got: {nrels_xml}"
);
}
#[test]
fn new_slide_with_notes_allocates_fresh_partname() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let s = p.slides_mut().add_slide(counter).expect("add slide");
s.set_notes_text(Some("brand new"));
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load");
let s2 = p2.slides().get(0).expect("slide 0");
let notes = s2.sld.notes_text().expect("notes present");
assert!(notes.contains("brand new"));
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
assert!(zip.by_name("ppt/notesSlides/notesSlide1.xml").is_ok());
}
#[test]
fn notes_roundtrip_multiple_slides() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
for i in 0..3 {
let s = p
.slides_mut()
.add_slide(counter.clone())
.expect("add slide");
if i != 1 {
s.set_notes_text(Some(&format!("note for slide {}", i + 1)));
}
}
let bytes1 = p.to_bytes().expect("to_bytes 1");
let mut p2 = Presentation::load_bytes(&bytes1).expect("load");
{
let sm = p2.slides_mut();
sm.get_mut(0).unwrap().sld.set_notes_text(Some("edited 1"));
sm.get_mut(2).unwrap().sld.set_notes_text(Some("edited 3"));
}
let bytes2 = p2.to_bytes().expect("to_bytes 2");
let p3 = Presentation::load_bytes(&bytes2).expect("load 2");
assert_eq!(p3.slides().len(), 3);
let s0 = p3.slides().get(0).expect("s0");
let s1 = p3.slides().get(1).expect("s1");
let s2 = p3.slides().get(2).expect("s2");
assert_eq!(s0.sld.notes_text().as_deref(), Some("edited 1"));
assert_eq!(s1.sld.notes_text(), None);
assert_eq!(s2.sld.notes_text().as_deref(), Some("edited 3"));
let cursor = std::io::Cursor::new(bytes2);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
assert!(zip.by_name("ppt/notesSlides/notesSlide1.xml").is_ok());
assert!(zip.by_name("ppt/notesSlides/notesSlide2.xml").is_ok());
assert!(zip
.by_name("ppt/notesSlides/_rels/notesSlide1.xml.rels")
.is_ok());
assert!(zip
.by_name("ppt/notesSlides/_rels/notesSlide2.xml.rels")
.is_ok());
}
#[test]
fn layout_rid_preserved_on_save() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let slide = p.slides_mut().add_slide(counter).expect("add slide");
slide
.shapes_mut()
.add_textbox_with_text(
Inches(0.5),
Inches(0.5),
Inches(4.0),
Inches(0.8),
"layout_rid preserve test",
)
.expect("textbox");
slide.set_layout_rid("rIdLayout999".to_string());
let bytes1 = p.to_bytes().expect("to_bytes 1");
let p2 = Presentation::load_bytes(&bytes1).expect("load");
let s = p2.slides().get(0).expect("slide 0");
assert_eq!(
s.sld.layout_rid(),
"rIdLayout999",
"to_opc_package 应保留 layout_rid 原值(round-trip 保真)"
);
let cursor = std::io::Cursor::new(bytes1);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
let mut srels_xml = String::new();
zip.by_name("ppt/slides/_rels/slide1.xml.rels")
.expect("slide1.xml.rels exists")
.read_to_string(&mut srels_xml)
.expect("read slide rels");
assert!(
srels_xml.contains("Id=\"rIdLayout999\""),
"slide1.xml.rels 中 layout 关系应使用 rIdLayout999,实际:{srels_xml}"
);
}
fn read_zip_entry(bytes: &[u8], path: &str) -> String {
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
let mut s = String::new();
match zip.by_name(path) {
Ok(mut f) => {
f.read_to_string(&mut s).expect("read zip entry");
}
Err(_) => panic!("zip entry not found: {}", path),
};
s
}
fn zip_contains(bytes: &[u8], path: &str) -> bool {
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
let result = zip.by_name(path);
result.is_ok()
}
fn build_multi_master_presentation() -> Presentation {
let mut p = Presentation::new().expect("new ok");
p.slide_masters.items.clear();
p.slide_layouts.items.clear();
p.themes.clear();
p.sld_master_ids.clear();
let theme1 = crate::oxml::theme::Theme {
partname: "/ppt/theme/theme1.xml".to_string(),
name: "Office Theme 1".to_string(),
..Default::default()
};
p.themes.push(theme1);
let theme2 = crate::oxml::theme::Theme {
partname: "/ppt/theme/theme2.xml".to_string(),
name: "Office Theme 2".to_string(),
..Default::default()
};
p.themes.push(theme2);
let master1_oxml = crate::oxml::slidemaster::SldMaster {
layout_rids: vec!["rIdL1".to_string(), "rIdL2".to_string()],
..Default::default()
};
p.slide_masters
.items
.push(crate::slide_masters::SlideMasterRef {
idx: 0,
partname: "/ppt/slideMasters/slideMaster1.xml".to_string(),
rid: "rIdM1".to_string(),
oxml: std::rc::Rc::new(std::cell::RefCell::new(master1_oxml)),
theme_partname: "/ppt/theme/theme1.xml".to_string(),
theme_rid: "rIdT1".to_string(),
});
let master2_oxml = crate::oxml::slidemaster::SldMaster {
layout_rids: vec!["rIdL3".to_string(), "rIdL4".to_string()],
..Default::default()
};
p.slide_masters
.items
.push(crate::slide_masters::SlideMasterRef {
idx: 1,
partname: "/ppt/slideMasters/slideMaster2.xml".to_string(),
rid: "rIdM2".to_string(),
oxml: std::rc::Rc::new(std::cell::RefCell::new(master2_oxml)),
theme_partname: "/ppt/theme/theme2.xml".to_string(),
theme_rid: "rIdT2".to_string(),
});
let mk_layout = |idx: usize, partname: &str, rid: &str, master_partname: &str| {
crate::slide_layouts::SlideLayoutRef {
idx,
partname: partname.to_string(),
rid: rid.to_string(),
oxml: std::rc::Rc::new(std::cell::RefCell::new(
crate::oxml::slidelayout::SldLayout::default(),
)),
master_partname: master_partname.to_string(),
}
};
p.slide_layouts.items.push(mk_layout(
0,
"/ppt/slideLayouts/slideLayout1.xml",
"rIdL1",
"/ppt/slideMasters/slideMaster1.xml",
));
p.slide_layouts.items.push(mk_layout(
1,
"/ppt/slideLayouts/slideLayout2.xml",
"rIdL2",
"/ppt/slideMasters/slideMaster1.xml",
));
p.slide_layouts.items.push(mk_layout(
2,
"/ppt/slideLayouts/slideLayout3.xml",
"rIdL3",
"/ppt/slideMasters/slideMaster2.xml",
));
p.slide_layouts.items.push(mk_layout(
3,
"/ppt/slideLayouts/slideLayout4.xml",
"rIdL4",
"/ppt/slideMasters/slideMaster2.xml",
));
p.sld_master_ids = vec![
(2147483648u32, "rIdM1".to_string()),
(2147483649u32, "rIdM2".to_string()),
];
let slide_specs = [
("rIdL1", "/ppt/slideLayouts/slideLayout1.xml"),
("rIdL3", "/ppt/slideLayouts/slideLayout3.xml"),
("rIdL2", "/ppt/slideLayouts/slideLayout2.xml"),
];
for (rid, partname) in slide_specs.iter() {
let counter = p.id_counter();
let slide = p.slides_mut().add_slide(counter).expect("add slide");
slide.set_layout_rid(rid.to_string());
slide.set_layout_partname(partname.to_string());
}
p
}
#[test]
fn single_master_round_trip_preserves_rid() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let _ = p.slides_mut().add_slide(counter).expect("add slide");
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load");
assert!(!p2.sld_master_ids.is_empty(), "应有至少 1 个 master id");
let (_, rid) = &p2.sld_master_ids[0];
let pres_rels = read_zip_entry(&bytes, "ppt/_rels/presentation.xml.rels");
assert!(
pres_rels.contains(&format!("Id=\"{}\"", rid)),
"presentation.xml.rels 应包含 rid={},实际:{}",
rid,
pres_rels
);
}
#[test]
fn single_master_round_trip_preserves_partname() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let _ = p.slides_mut().add_slide(counter).expect("add slide");
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load");
assert_eq!(
p2.slide_masters.items[0].partname, "/ppt/slideMasters/slideMaster1.xml",
"master partname 应保持不变"
);
assert_eq!(
p2.slide_layouts.items[0].partname, "/ppt/slideLayouts/slideLayout1.xml",
"layout partname 应保持不变"
);
}
#[test]
fn multi_master_writes_all_masters() {
let p = build_multi_master_presentation();
let bytes = p.to_bytes().expect("to_bytes");
assert!(
zip_contains(&bytes, "ppt/slideMasters/slideMaster1.xml"),
"应写出 slideMaster1.xml"
);
assert!(
zip_contains(&bytes, "ppt/slideMasters/slideMaster2.xml"),
"应写出 slideMaster2.xml"
);
}
#[test]
fn multi_master_pres_rels_matches_sld_master_ids() {
let p = build_multi_master_presentation();
let bytes = p.to_bytes().expect("to_bytes");
let pres_rels = read_zip_entry(&bytes, "ppt/_rels/presentation.xml.rels");
assert!(
pres_rels.contains("Id=\"rIdM1\""),
"presentation.xml.rels 应包含 rIdM1,实际:{}",
pres_rels
);
assert!(
pres_rels.contains("Id=\"rIdM2\""),
"presentation.xml.rels 应包含 rIdM2,实际:{}",
pres_rels
);
}
#[test]
fn multi_master_master_rels_uses_layout_rids() {
let p = build_multi_master_presentation();
let bytes = p.to_bytes().expect("to_bytes");
let m1_rels = read_zip_entry(&bytes, "ppt/slideMasters/_rels/slideMaster1.xml.rels");
assert!(
m1_rels.contains("Id=\"rIdL1\""),
"master1.rels 应含 rIdL1:{}",
m1_rels
);
assert!(
m1_rels.contains("Id=\"rIdL2\""),
"master1.rels 应含 rIdL2:{}",
m1_rels
);
let m2_rels = read_zip_entry(&bytes, "ppt/slideMasters/_rels/slideMaster2.xml.rels");
assert!(
m2_rels.contains("Id=\"rIdL3\""),
"master2.rels 应含 rIdL3:{}",
m2_rels
);
assert!(
m2_rels.contains("Id=\"rIdL4\""),
"master2.rels 应含 rIdL4:{}",
m2_rels
);
}
#[test]
fn multi_master_layout_rels_points_to_correct_master() {
let p = build_multi_master_presentation();
let bytes = p.to_bytes().expect("to_bytes");
let l1_rels = read_zip_entry(&bytes, "ppt/slideLayouts/_rels/slideLayout1.xml.rels");
assert!(
l1_rels.contains("slideMaster1.xml"),
"layout1.rels 应指向 slideMaster1.xml:{}",
l1_rels
);
let l3_rels = read_zip_entry(&bytes, "ppt/slideLayouts/_rels/slideLayout3.xml.rels");
assert!(
l3_rels.contains("slideMaster2.xml"),
"layout3.rels 应指向 slideMaster2.xml:{}",
l3_rels
);
}
#[test]
fn slide_layout_rel_uses_real_partname() {
let p = build_multi_master_presentation();
let bytes = p.to_bytes().expect("to_bytes");
let s2_rels = read_zip_entry(&bytes, "ppt/slides/_rels/slide2.xml.rels");
assert!(
s2_rels.contains("slideLayout3.xml"),
"slide2.rels 应指向 slideLayout3.xml:{}",
s2_rels
);
}
#[test]
fn sldmaster_to_xml_uses_layout_rids() {
let m = crate::oxml::slidemaster::SldMaster {
layout_rids: vec!["rIdA".to_string(), "rIdB".to_string()],
..Default::default()
};
let xml = m.to_xml();
assert!(
xml.contains("r:id=\"rIdA\""),
"sldLayoutIdLst 应含 r:id=\"rIdA\":{}",
xml
);
assert!(
xml.contains("r:id=\"rIdB\""),
"sldLayoutIdLst 应含 r:id=\"rIdB\":{}",
xml
);
}
#[test]
fn field_level_round_trip_master_partname() {
let p = build_multi_master_presentation();
let before_m1 = p.slide_masters.items[0].partname.clone();
let before_m2 = p.slide_masters.items[1].partname.clone();
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load");
assert_eq!(
p2.slide_masters.items[0].partname, before_m1,
"master1 partname round-trip 后应保持"
);
assert_eq!(
p2.slide_masters.items[1].partname, before_m2,
"master2 partname round-trip 后应保持"
);
}
#[test]
fn field_level_round_trip_layout_count() {
let p = build_multi_master_presentation();
let before = p.slide_layouts.items.len();
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load");
assert_eq!(
p2.slide_layouts.items.len(),
before,
"layout 数量 round-trip 后应保持为 {},实际 {}",
before,
p2.slide_layouts.items.len()
);
}
#[test]
fn field_level_round_trip_theme_count() {
let p = build_multi_master_presentation();
let before = p.themes.len();
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load");
assert_eq!(
p2.themes.len(),
before,
"theme 数量 round-trip 后应保持为 {},实际 {}",
before,
p2.themes.len()
);
}
#[test]
fn custom_properties_set_get_remove() {
let mut props = CustomProperties::new();
assert!(props.is_empty());
assert_eq!(props.len(), 0);
props.set("Project", CustomPropertyValue::Text("Demo".to_string()));
props.set("Version", CustomPropertyValue::Int(42));
props.set("Active", CustomPropertyValue::Bool(true));
assert_eq!(props.len(), 3);
match props.get("Project") {
Some(CustomPropertyValue::Text(s)) => assert_eq!(s, "Demo"),
other => panic!("期望 Text,得到 {:?}", other),
}
match props.get("Version") {
Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 42),
other => panic!("期望 Int,得到 {:?}", other),
}
assert!(props.get("NonExistent").is_none());
props.set("Version", CustomPropertyValue::Int(100));
match props.get("Version") {
Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 100),
other => panic!("期望 Int(100),得到 {:?}", other),
}
assert_eq!(props.len(), 3, "覆盖不应增加条目数");
let removed = props.remove("Active");
assert!(matches!(removed, Some(CustomPropertyValue::Bool(true))));
assert_eq!(props.len(), 2);
assert!(props.get("Active").is_none());
}
#[test]
fn custom_properties_to_xml() {
let mut props = CustomProperties::new();
props.set(
"Text",
CustomPropertyValue::Text("Hello & World".to_string()),
);
props.set("Int", CustomPropertyValue::Int(42));
props.set("Float", CustomPropertyValue::Float(3.15));
props.set("Bool", CustomPropertyValue::Bool(true));
let xml = props.to_xml();
assert!(xml.contains("<Properties"));
assert!(xml.contains("custom-properties"));
assert!(xml.contains("Hello & World"));
assert!(xml.contains("pid=\"2\""));
assert!(xml.contains("pid=\"3\""));
assert!(xml.contains("<vt:lpwstr>Hello & World</vt:lpwstr>"));
assert!(xml.contains("<vt:i4>42</vt:i4>"));
assert!(xml.contains("<vt:r8>3.15</vt:r8>"));
assert!(xml.contains("<vt:bool>true</vt:bool>"));
assert!(xml.contains("fmtid=\"{D5CDD505-2E9C-101B-9397-08002B2CF9AE}\""));
}
#[test]
fn custom_properties_from_xml() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="2" name="Text">
<vt:lpwstr>Value</vt:lpwstr>
</property>
<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="3" name="Int">
<vt:i4>42</vt:i4>
</property>
<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="4" name="Bool">
<vt:bool>false</vt:bool>
</property>
<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="5" name="Float">
<vt:r8>3.15</vt:r8>
</property>
</Properties>"#;
let props = CustomProperties::from_xml(xml);
assert_eq!(props.len(), 4);
match props.get("Text") {
Some(CustomPropertyValue::Text(s)) => assert_eq!(s, "Value"),
other => panic!("期望 Text,得到 {:?}", other),
}
match props.get("Int") {
Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 42),
other => panic!("期望 Int,得到 {:?}", other),
}
match props.get("Bool") {
Some(CustomPropertyValue::Bool(b)) => assert!(!*b),
other => panic!("期望 Bool(false),得到 {:?}", other),
}
match props.get("Float") {
Some(CustomPropertyValue::Float(f)) => assert!((f - 3.15).abs() < 1e-6),
other => panic!("期望 Float,得到 {:?}", other),
}
}
#[test]
fn custom_properties_round_trip() {
let mut props = CustomProperties::new();
props.set("Key1", CustomPropertyValue::Text("Value1".to_string()));
props.set("Key2", CustomPropertyValue::Int(123));
props.set("Key3", CustomPropertyValue::Bool(true));
let xml = props.to_xml();
let parsed = CustomProperties::from_xml(&xml);
assert_eq!(parsed.len(), 3);
assert_eq!(props.get("Key1"), parsed.get("Key1"));
assert_eq!(props.get("Key2"), parsed.get("Key2"));
assert_eq!(props.get("Key3"), parsed.get("Key3"));
}
#[test]
fn custom_properties_empty_not_serialized() {
let p = Presentation::new().expect("new ok");
let bytes = p.to_bytes().expect("to_bytes");
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
assert!(zip.by_name("docProps/custom.xml").is_err());
}
#[test]
fn custom_properties_serialized_to_pptx() {
let mut p = Presentation::new().expect("new ok");
p.custom_properties_mut()
.set("Project", CustomPropertyValue::Text("Test".to_string()));
p.custom_properties_mut()
.set("Version", CustomPropertyValue::Int(1));
let bytes = p.to_bytes().expect("to_bytes");
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
let mut custom_xml = String::new();
zip.by_name("docProps/custom.xml")
.expect("custom.xml 应存在")
.read_to_string(&mut custom_xml)
.expect("read custom.xml");
assert!(custom_xml.contains("name=\"Project\""));
assert!(custom_xml.contains("<vt:lpwstr>Test</vt:lpwstr>"));
assert!(custom_xml.contains("name=\"Version\""));
assert!(custom_xml.contains("<vt:i4>1</vt:i4>"));
let mut rels_xml = String::new();
zip.by_name("_rels/.rels")
.expect(".rels 应存在")
.read_to_string(&mut rels_xml)
.expect("read .rels");
assert!(rels_xml.contains("custom-properties"), "rels: {}", rels_xml);
assert!(
rels_xml.contains("docProps/custom.xml"),
"rels: {}",
rels_xml
);
}
#[test]
fn custom_properties_round_trip_through_pptx() {
let mut p = Presentation::new().expect("new ok");
p.custom_properties_mut()
.set("Author", CustomPropertyValue::Text("TestUser".to_string()));
p.custom_properties_mut()
.set("Count", CustomPropertyValue::Int(99));
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load");
assert_eq!(p2.custom_properties().len(), 2);
match p2.custom_properties().get("Author") {
Some(CustomPropertyValue::Text(s)) => assert_eq!(s, "TestUser"),
other => panic!("期望 Text,得到 {:?}", other),
}
match p2.custom_properties().get("Count") {
Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 99),
other => panic!("期望 Int,得到 {:?}", other),
}
}
#[test]
fn slide_add_comment_basic() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let author_id = p.comment_authors_mut().get_or_insert_id("张三", "ZS");
let slide = p.slides_mut().add_slide(counter).expect("add slide");
let idx = slide.add_comment(
author_id,
crate::Emu::new(914400),
crate::Emu::new(914400),
"测试评论",
);
assert_eq!(idx, 1);
let comments = slide.comments().expect("comments should exist");
assert_eq!(comments.len(), 1);
assert_eq!(comments.comments[0].text, "测试评论");
assert_eq!(comments.comments[0].author_id, author_id);
assert_eq!(comments.comments[0].idx, 1);
assert_eq!(comments.comments[0].pos_x, 914400);
assert_eq!(comments.comments[0].pos_y, 914400);
}
#[test]
fn slide_add_comment_multiple_idx_increments() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let author_id = p.comment_authors_mut().get_or_insert_id("作者", "ZZ");
let slide = p.slides_mut().add_slide(counter).expect("add slide");
let idx1 = slide.add_comment(author_id, 0, 0, "第一条");
let idx2 = slide.add_comment(author_id, 100, 100, "第二条");
let idx3 = slide.add_comment(author_id, 200, 200, "第三条");
assert_eq!(idx1, 1);
assert_eq!(idx2, 2);
assert_eq!(idx3, 3);
assert_eq!(slide.comments().unwrap().len(), 3);
}
#[test]
fn slide_clear_comments() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let author_id = p.comment_authors_mut().get_or_insert_id("A", "A");
let slide = p.slides_mut().add_slide(counter).expect("add slide");
slide.add_comment(author_id, 0, 0, "评论");
assert!(slide.comments().is_some());
slide.clear_comments();
assert!(slide.comments().is_none());
}
#[test]
fn comments_serialized_to_pptx() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let author_id = p.comment_authors_mut().get_or_insert_id("张三", "ZS");
let slide = p.slides_mut().add_slide(counter).expect("add slide");
slide.add_comment(
author_id,
crate::Emu::new(914400),
crate::Emu::new(914400),
"PPTX 评论",
);
let bytes = p.to_bytes().expect("to_bytes");
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
let mut cxml = String::new();
zip.by_name("ppt/comments/comment1.xml")
.expect("comment1.xml 应存在")
.read_to_string(&mut cxml)
.expect("read comment1.xml");
assert!(cxml.contains("<p:cmLst"), "comment xml: {}", cxml);
assert!(cxml.contains("PPTX 评论"), "comment text: {}", cxml);
assert!(cxml.contains("authorId=\""), "comment authorId: {}", cxml);
let mut axml = String::new();
zip.by_name("ppt/commentAuthors.xml")
.expect("commentAuthors.xml 应存在")
.read_to_string(&mut axml)
.expect("read commentAuthors.xml");
assert!(axml.contains("<p:cmAuthorLst"), "authors xml: {}", axml);
assert!(axml.contains("张三"), "author name: {}", axml);
let mut srels = String::new();
zip.by_name("ppt/slides/_rels/slide1.xml.rels")
.expect("slide1.xml.rels exists")
.read_to_string(&mut srels)
.expect("read slide rels");
assert!(
srels.contains("comments"),
"slide rels should contain comments: {}",
srels
);
assert!(
srels.contains("../comments/comment1.xml"),
"slide rels target: {}",
srels
);
let mut prels = String::new();
zip.by_name("ppt/_rels/presentation.xml.rels")
.expect("pres rels exists")
.read_to_string(&mut prels)
.expect("read pres rels");
assert!(prels.contains("commentAuthors"), "pres rels: {}", prels);
}
#[test]
fn comments_round_trip_through_pptx() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let author_id = p.comment_authors_mut().get_or_insert_id("李四", "LS");
let slide = p.slides_mut().add_slide(counter).expect("add slide");
slide.add_comment(
author_id,
crate::Emu::new(100),
crate::Emu::new(200),
"往返评论",
);
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load");
let s2 = p2.slides().get(0).expect("slide 0");
let comments = s2.sld.comments().expect("comments should exist");
assert_eq!(comments.len(), 1);
let c = &comments.comments[0];
assert_eq!(c.text, "往返评论");
assert_eq!(c.pos_x, 100);
assert_eq!(c.pos_y, 200);
assert_eq!(c.idx, 1);
let authors = p2.comment_authors();
assert_eq!(authors.len(), 1);
assert_eq!(authors.authors[0].name, "李四");
assert_eq!(authors.authors[0].initials, "LS");
assert_eq!(authors.authors[0].id, c.author_id);
}
#[test]
fn no_comments_no_part() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
p.slides_mut().add_slide(counter).expect("add slide");
let bytes = p.to_bytes().expect("to_bytes");
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
assert!(zip.by_name("ppt/comments/comment1.xml").is_err());
assert!(zip.by_name("ppt/commentAuthors.xml").is_err());
}
#[test]
fn comments_multiple_slides_partname_stable() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let aid = p.comment_authors_mut().get_or_insert_id("A", "A");
let s1 = p.slides_mut().add_slide(counter.clone()).expect("slide 1");
s1.add_comment(aid, 0, 0, "slide1 评论");
let s2 = p.slides_mut().add_slide(counter).expect("slide 2");
s2.add_comment(aid, 0, 0, "slide2 评论");
let bytes = p.to_bytes().expect("to_bytes");
let p2 = Presentation::load_bytes(&bytes).expect("load");
let s1b = p2.slides().get(0).expect("slide 0");
let s2b = p2.slides().get(1).expect("slide 1");
assert_eq!(s1b.sld.comments().unwrap().comments[0].text, "slide1 评论");
assert_eq!(s2b.sld.comments().unwrap().comments[0].text, "slide2 评论");
let bytes2 = p2.to_bytes().expect("to_bytes 2");
let cursor = std::io::Cursor::new(bytes2);
let mut zip = zip::ZipArchive::new(cursor).expect("zip 2");
assert!(zip.by_name("ppt/comments/comment1.xml").is_ok());
assert!(zip.by_name("ppt/comments/comment2.xml").is_ok());
}
#[test]
fn slide_allocate_diagram_rids_increments() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let slide = p.slides_mut().add_slide(counter).expect("add slide");
let (d1, l1, q1, c1) = slide.allocate_diagram_rids();
assert_eq!(d1, "rIdDgmData1");
assert_eq!(l1, "rIdDgmLayout1");
assert_eq!(q1, "rIdDgmQs1");
assert_eq!(c1, "rIdDgmColors1");
let (d2, l2, q2, c2) = slide.allocate_diagram_rids();
assert_eq!(d2, "rIdDgmData2");
assert_eq!(l2, "rIdDgmLayout2");
assert_eq!(q2, "rIdDgmQs2");
assert_eq!(c2, "rIdDgmColors2");
}
#[test]
fn slide_next_diagram_index_increments() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let slide = p.slides_mut().add_slide(counter).expect("add slide");
assert_eq!(slide.next_diagram_index(), 1);
assert_eq!(slide.next_diagram_index(), 2);
assert_eq!(slide.next_diagram_index(), 3);
}
#[test]
fn slide_register_diagram_stores_entry() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let slide = p.slides_mut().add_slide(counter).expect("add slide");
let entry = DiagramEntry {
data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
data_xml: "<dgm:dataModel/>".to_string(),
layout_xml: "<dgm:layoutDef/>".to_string(),
quick_style_xml: "<dgm:styleData/>".to_string(),
colors_xml: "<dgm:colorsDef/>".to_string(),
data_rid: "rIdDgmData1".to_string(),
layout_rid: "rIdDgmLayout1".to_string(),
quick_style_rid: "rIdDgmQs1".to_string(),
colors_rid: "rIdDgmColors1".to_string(),
};
slide.register_diagram(entry);
assert_eq!(slide.diagram_entries.len(), 1);
assert_eq!(
slide.diagram_entries[0].data_partname.as_str(),
"/ppt/diagrams/data1.xml"
);
assert_eq!(slide.diagram_entries[0].data_xml, "<dgm:dataModel/>");
}
#[test]
fn diagram_parts_written_to_zip_after_register() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let slide = p.slides_mut().add_slide(counter).expect("add slide");
let entry = DiagramEntry {
data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
data_xml: "<?xml version=\"1.0\"?><dgm:dataModel xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
layout_xml: "<?xml version=\"1.0\"?><dgm:layoutDef xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
quick_style_xml: "<?xml version=\"1.0\"?><dgm:styleData xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
colors_xml: "<?xml version=\"1.0\"?><dgm:colorsDef xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
data_rid: "rIdDgmData1".to_string(),
layout_rid: "rIdDgmLayout1".to_string(),
quick_style_rid: "rIdDgmQs1".to_string(),
colors_rid: "rIdDgmColors1".to_string(),
};
slide.register_diagram(entry);
let bytes = p.to_bytes().expect("to_bytes");
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
assert!(
zip.by_name("ppt/diagrams/data1.xml").is_ok(),
"data1.xml missing"
);
assert!(
zip.by_name("ppt/diagrams/layout1.xml").is_ok(),
"layout1.xml missing"
);
assert!(
zip.by_name("ppt/diagrams/quickStyles1.xml").is_ok(),
"quickStyles1.xml missing"
);
assert!(
zip.by_name("ppt/diagrams/colors1.xml").is_ok(),
"colors1.xml missing"
);
let mut srels_xml = String::new();
zip.by_name("ppt/slides/_rels/slide1.xml.rels")
.expect("slide1.xml.rels exists")
.read_to_string(&mut srels_xml)
.expect("read slide rels");
assert!(
srels_xml.contains("/diagramData"),
"slide1.xml.rels 缺 diagramData 关系,实际:{srels_xml}"
);
assert!(
srels_xml.contains("/diagramLayout"),
"slide1.xml.rels 缺 diagramLayout 关系,实际:{srels_xml}"
);
assert!(
srels_xml.contains("/diagramQuickStyle"),
"slide1.xml.rels 缺 diagramQuickStyle 关系,实际:{srels_xml}"
);
assert!(
srels_xml.contains("/diagramColors"),
"slide1.xml.rels 缺 diagramColors 关系,实际:{srels_xml}"
);
assert!(srels_xml.contains("rIdDgmData1"), "缺 rIdDgmData1");
assert!(srels_xml.contains("rIdDgmLayout1"), "缺 rIdDgmLayout1");
assert!(srels_xml.contains("rIdDgmQs1"), "缺 rIdDgmQs1");
assert!(srels_xml.contains("rIdDgmColors1"), "缺 rIdDgmColors1");
}
#[test]
fn diagram_parts_global_index_across_slides() {
let mut p = Presentation::new().expect("new ok");
let counter = p.id_counter();
let s1 = p.slides_mut().add_slide(counter.clone()).expect("s1");
let e1 = DiagramEntry {
data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
data_xml: "<dgm:dataModel/>".to_string(),
layout_xml: "<dgm:layoutDef/>".to_string(),
quick_style_xml: "<dgm:styleData/>".to_string(),
colors_xml: "<dgm:colorsDef/>".to_string(),
data_rid: "rIdDgmData1".to_string(),
layout_rid: "rIdDgmLayout1".to_string(),
quick_style_rid: "rIdDgmQs1".to_string(),
colors_rid: "rIdDgmColors1".to_string(),
};
s1.register_diagram(e1);
let s2 = p.slides_mut().add_slide(counter).expect("s2");
let e2 = DiagramEntry {
data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
data_xml: "<dgm:dataModel/>".to_string(),
layout_xml: "<dgm:layoutDef/>".to_string(),
quick_style_xml: "<dgm:styleData/>".to_string(),
colors_xml: "<dgm:colorsDef/>".to_string(),
data_rid: "rIdDgmData1".to_string(),
layout_rid: "rIdDgmLayout1".to_string(),
quick_style_rid: "rIdDgmQs1".to_string(),
colors_rid: "rIdDgmColors1".to_string(),
};
s2.register_diagram(e2);
let bytes = p.to_bytes().expect("to_bytes");
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
assert!(zip.by_name("ppt/diagrams/data1.xml").is_ok());
assert!(zip.by_name("ppt/diagrams/layout1.xml").is_ok());
assert!(
zip.by_name("ppt/diagrams/data2.xml").is_ok(),
"data2.xml missing"
);
assert!(
zip.by_name("ppt/diagrams/layout2.xml").is_ok(),
"layout2.xml missing"
);
assert!(zip.by_name("ppt/diagrams/quickStyles2.xml").is_ok());
assert!(zip.by_name("ppt/diagrams/colors2.xml").is_ok());
}
}