1use std::cell::Cell;
51use std::io::Read;
52use std::path::Path;
53use std::rc::Rc;
54
55use crate::notes_masters::{NotesMasterRef, NotesMasters};
56use crate::opc::package::{ct, rels_partname_for};
57use crate::opc::part::{new_part_name, Part, PartName};
58use crate::opc::rels::{RelType, Relationship, Relationships};
59use crate::opc::OpcPackage;
60use crate::oxml::presentation::{PresentationRoot, SlideIdEntry};
61use crate::oxml::slidelayout::SldLayout as OxmlSldLayout;
62use crate::oxml::slidemaster::SldMaster as OxmlSldMaster;
63use crate::oxml::theme::default_theme_xml;
64use crate::slide::{Slide, SlideEntry, Slides};
65use crate::slide_layouts::{SlideLayoutRef, SlideLayouts};
66use crate::slide_masters::{SlideMasterRef, SlideMasters};
67use crate::units::Emu;
68
69pub const DEFAULT_WIDTH_EMU: i64 = 9_144_000;
71
72pub const DEFAULT_HEIGHT_EMU: i64 = 6_858_000;
74
75#[derive(Debug)]
85pub struct Presentation {
86 pub(crate) slides: Slides,
88 pub(crate) slide_layouts: SlideLayouts,
90 pub(crate) slide_masters: SlideMasters,
92 pub(crate) notes_masters: NotesMasters,
98 pub(crate) theme: crate::oxml::theme::Theme,
103 pub(crate) sld_master_ids: Vec<(u32, String)>,
108 pub(crate) width: Emu,
110 pub(crate) height: Emu,
112 pub(crate) id_counter: Rc<Cell<u32>>,
114 pub(crate) core_properties: CoreProperties,
116 pub(crate) custom_properties: CustomProperties,
120 pub(crate) comment_authors: crate::oxml::comments::CommentAuthorList,
125 pub(crate) modify_protection: Option<crate::crypto::ModifyProtection>,
130 pub(crate) sections: crate::oxml::section::SectionList,
136}
137
138#[derive(Debug, Clone, Default)]
156pub struct CoreProperties {
157 pub title: Option<String>,
159 pub creator: Option<String>,
161 pub subject: Option<String>,
163 pub last_modified_by: Option<String>,
165 pub application: Option<String>,
167 pub created: Option<String>,
169 pub modified: Option<String>,
171 pub keywords: Option<String>,
173 pub category: Option<String>,
175 pub description: Option<String>,
177 pub revision: Option<String>,
179}
180
181#[derive(Clone, Debug, PartialEq)]
194pub enum CustomPropertyValue {
195 Text(String),
197 Int(i32),
199 Float(f64),
201 Bool(bool),
203 DateTime(String),
205}
206
207impl CustomPropertyValue {
208 fn vt_element(&self) -> &'static str {
210 match self {
211 CustomPropertyValue::Text(_) => "vt:lpwstr",
212 CustomPropertyValue::Int(_) => "vt:i4",
213 CustomPropertyValue::Float(_) => "vt:r8",
214 CustomPropertyValue::Bool(_) => "vt:bool",
215 CustomPropertyValue::DateTime(_) => "vt:filetime",
216 }
217 }
218
219 fn value_str(&self) -> String {
221 match self {
222 CustomPropertyValue::Text(s) => s.clone(),
223 CustomPropertyValue::Int(i) => i.to_string(),
224 CustomPropertyValue::Float(f) => f.to_string(),
225 CustomPropertyValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
226 CustomPropertyValue::DateTime(s) => s.clone(),
227 }
228 }
229}
230
231#[derive(Debug, Clone, Default)]
253pub struct CustomProperties {
254 entries: Vec<(String, CustomPropertyValue)>,
256}
257
258impl CustomProperties {
259 pub fn new() -> Self {
261 Self::default()
262 }
263
264 pub fn is_empty(&self) -> bool {
266 self.entries.is_empty()
267 }
268
269 pub fn len(&self) -> usize {
271 self.entries.len()
272 }
273
274 pub fn set(&mut self, name: impl Into<String>, value: CustomPropertyValue) {
280 let name = name.into();
281 for entry in &mut self.entries {
282 if entry.0 == name {
283 entry.1 = value;
284 return;
285 }
286 }
287 self.entries.push((name, value));
288 }
289
290 pub fn get(&self, name: &str) -> Option<&CustomPropertyValue> {
292 self.entries.iter().find(|(k, _)| k == name).map(|(_, v)| v)
293 }
294
295 pub fn remove(&mut self, name: &str) -> Option<CustomPropertyValue> {
299 let idx = self.entries.iter().position(|(k, _)| k == name)?;
300 Some(self.entries.remove(idx).1)
301 }
302
303 pub fn iter(&self) -> impl Iterator<Item = (&str, &CustomPropertyValue)> {
305 self.entries.iter().map(|(k, v)| (k.as_str(), v))
306 }
307
308 pub fn to_xml(&self) -> String {
323 if self.entries.is_empty() {
324 return String::new();
325 }
326 let mut out = String::with_capacity(256 * self.entries.len());
327 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
328 out.push_str("<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties\"");
329 out.push_str(
330 " xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\n",
331 );
332 for (i, (name, value)) in self.entries.iter().enumerate() {
333 let pid = (i + 2) as u32; let vt_elem = value.vt_element();
335 let val = xml_escape(&value.value_str());
336 out.push_str(&format!(
337 " <property fmtid=\"{{D5CDD505-2E9C-101B-9397-08002B2CF9AE}}\" pid=\"{}\" name=\"{}\">\n",
338 pid,
339 xml_escape(name)
340 ));
341 out.push_str(&format!(" <{}>{}</{}>\n", vt_elem, val, vt_elem));
342 out.push_str(" </property>\n");
343 }
344 out.push_str("</Properties>");
345 out
346 }
347
348 pub fn from_xml(xml: &str) -> Self {
356 use quick_xml::events::Event;
357 use quick_xml::Reader;
358
359 let mut props = CustomProperties::new();
360 let mut rd = Reader::from_str(xml);
361 rd.config_mut().trim_text(true);
362 let mut buf = Vec::new();
363 let mut cur_name: Option<String> = None;
365 let mut cur_vt: Option<String> = None;
367 let mut cur_text = String::new();
369
370 loop {
371 match rd.read_event_into(&mut buf) {
372 Ok(Event::Start(e)) => {
373 let name = e.name();
374 let qname = String::from_utf8_lossy(name.as_ref()).to_string();
375 let local = local_name(name.as_ref());
376 if local == b"property" {
377 for a in e.attributes().flatten() {
379 if a.key.as_ref() == b"name" {
380 cur_name = Some(
381 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
382 .unwrap_or_default()
383 .to_string(),
384 );
385 }
386 }
387 } else if cur_name.is_some() && qname.starts_with("vt:") {
388 cur_vt = Some(qname);
390 cur_text.clear();
391 }
392 }
393 Ok(Event::Empty(e)) => {
394 let name = e.name();
395 let local = local_name(name.as_ref());
396 if local == b"property" {
397 cur_name = None;
399 }
400 }
401 Ok(Event::Text(t)) => {
402 if cur_vt.is_some() {
403 cur_text.push_str(&t.decode().unwrap_or_default());
404 }
405 }
406 Ok(Event::End(e)) => {
407 let name = e.name();
408 let qname = String::from_utf8_lossy(name.as_ref()).to_string();
409 let local = local_name(name.as_ref());
410 if qname.starts_with("vt:") && cur_vt.is_some() {
411 if let Some(prop_name) = cur_name.take() {
413 let value = parse_vt_value(&cur_vt.take().unwrap(), &cur_text);
414 if let Some(v) = value {
415 props.set(prop_name, v);
416 }
417 }
418 cur_vt = None;
419 cur_text.clear();
420 } else if local == b"property" {
421 cur_name = None;
422 }
423 }
424 Ok(Event::Eof) => break,
425 _ => {}
426 }
427 buf.clear();
428 }
429 props
430 }
431}
432
433fn xml_escape(s: &str) -> String {
435 let mut out = String::with_capacity(s.len());
436 for c in s.chars() {
437 match c {
438 '&' => out.push_str("&"),
439 '<' => out.push_str("<"),
440 '>' => out.push_str(">"),
441 '"' => out.push_str("""),
442 '\'' => out.push_str("'"),
443 _ => out.push(c),
444 }
445 }
446 out
447}
448
449fn local_name(name: &[u8]) -> &[u8] {
451 match name.iter().position(|&b| b == b':') {
452 Some(i) => &name[i + 1..],
453 None => name,
454 }
455}
456
457fn parse_vt_value(vt_elem: &str, text: &str) -> Option<CustomPropertyValue> {
459 let trimmed = text.trim();
460 match vt_elem {
461 "vt:lpwstr" | "vt:bstr" => Some(CustomPropertyValue::Text(trimmed.to_string())),
462 "vt:i4" | "vt:int" | "vt:i2" | "vt:ui1" | "vt:ui2" | "vt:ui4" => {
463 trimmed.parse::<i32>().ok().map(CustomPropertyValue::Int)
464 }
465 "vt:i8" | "vt:ui8" => {
466 trimmed.parse::<i64>().ok().and_then(|v| {
468 if v >= i32::MIN as i64 && v <= i32::MAX as i64 {
469 Some(CustomPropertyValue::Int(v as i32))
470 } else {
471 None
472 }
473 })
474 }
475 "vt:r4" | "vt:r8" | "vt:decimal" => {
476 trimmed.parse::<f64>().ok().map(CustomPropertyValue::Float)
477 }
478 "vt:bool" => {
479 let v = trimmed.eq_ignore_ascii_case("true") || trimmed == "1";
480 if v || trimmed.eq_ignore_ascii_case("false") || trimmed == "0" {
481 Some(CustomPropertyValue::Bool(v))
482 } else {
483 None
484 }
485 }
486 "vt:filetime" | "vt:date" => Some(CustomPropertyValue::DateTime(trimmed.to_string())),
487 _ => None,
488 }
489}
490
491#[derive(Debug, Clone)]
497pub struct MediaEntry {
498 pub partname: PartName,
500 pub content_type: String,
502 pub blob: Vec<u8>,
504 pub rid: String,
506}
507
508#[derive(Debug, Clone)]
520pub struct ChartEntry {
521 pub partname: PartName,
523 pub chart: crate::oxml::chart::Chart,
525 pub rid: String,
527 pub xlsx_blob: Option<Vec<u8>>,
538}
539
540#[derive(Debug, Clone)]
559pub struct OleEntry {
560 pub partname: PartName,
562 pub blob: Vec<u8>,
564 pub rid: String,
566}
567
568#[derive(Debug, Clone)]
590pub struct VideoEntry {
591 pub partname: PartName,
593 pub blob: Vec<u8>,
595 pub rid: String,
601}
602
603#[derive(Debug, Clone)]
612pub struct AudioEntry {
613 pub partname: PartName,
615 pub blob: Vec<u8>,
617 pub rid: String,
619}
620
621#[derive(Debug, Clone)]
648pub struct DiagramEntry {
649 pub data_partname: PartName,
651 pub layout_partname: PartName,
653 pub quick_style_partname: PartName,
655 pub colors_partname: PartName,
657 pub data_xml: String,
659 pub layout_xml: String,
661 pub quick_style_xml: String,
663 pub colors_xml: String,
665 pub data_rid: String,
669 pub layout_rid: String,
673 pub quick_style_rid: String,
677 pub colors_rid: String,
681}
682
683impl DiagramEntry {
684 pub fn data_model(&self) -> crate::Result<crate::oxml::diagram::DataModel> {
705 crate::oxml::diagram::DataModel::parse_from_xml(&self.data_xml)
706 }
707
708 pub fn layout_def(&self) -> crate::Result<crate::oxml::diagram::LayoutDef> {
712 crate::oxml::diagram::LayoutDef::parse_from_xml(&self.layout_xml)
713 }
714
715 pub fn quick_style_def(&self) -> crate::Result<crate::oxml::diagram::QuickStyleDef> {
719 crate::oxml::diagram::QuickStyleDef::parse_from_xml(&self.quick_style_xml)
720 }
721
722 pub fn colors_def(&self) -> crate::Result<crate::oxml::diagram::ColorsDef> {
726 crate::oxml::diagram::ColorsDef::parse_from_xml(&self.colors_xml)
727 }
728
729 pub fn set_data_model(&mut self, data_model: &crate::oxml::diagram::DataModel) {
748 self.data_xml = data_model.to_xml();
749 }
750
751 pub fn set_point_text(
764 &mut self,
765 model_id: u32,
766 new_text: impl Into<String>,
767 ) -> crate::Result<bool> {
768 let mut dm = self.data_model()?;
769 let ok = dm.set_point_text(model_id, new_text);
770 if ok {
771 self.data_xml = dm.to_xml();
772 }
773 Ok(ok)
774 }
775}
776
777impl Presentation {
778 pub fn new() -> crate::Result<Self> {
789 let id_counter = Rc::new(Cell::new(2));
790 let mut pres = Presentation {
791 slides: Slides::new(),
792 slide_layouts: SlideLayouts::new(),
793 slide_masters: SlideMasters::new(),
794 notes_masters: NotesMasters::new(),
795 theme: crate::oxml::theme::Theme::default(),
796 sld_master_ids: Vec::new(),
797 width: Emu(DEFAULT_WIDTH_EMU),
798 height: Emu(DEFAULT_HEIGHT_EMU),
799 id_counter,
800 core_properties: CoreProperties {
801 application: Some("pptx-rs".to_string()),
802 ..Default::default()
803 },
804 custom_properties: CustomProperties::default(),
805 comment_authors: crate::oxml::comments::CommentAuthorList::default(),
806 modify_protection: None,
807 sections: crate::oxml::section::SectionList::default(),
808 };
809 pres.ensure_default_master_and_layout()?;
810 Ok(pres)
811 }
812
813 pub fn open(path: impl AsRef<Path>) -> crate::Result<Self> {
823 Self::load(path.as_ref())
824 }
825
826 pub fn load(path: &Path) -> crate::Result<Self> {
828 let pkg = OpcPackage::load(path)?;
829 Self::from_opc(pkg)
830 }
831
832 pub fn load_bytes(bytes: &[u8]) -> crate::Result<Self> {
844 let mut pkg = OpcPackage::new();
845 let cursor = std::io::Cursor::new(bytes);
846 let mut zip = zip::ZipArchive::new(cursor)?;
847 let mut ct_xml = String::new();
849 zip.by_name("[Content_Types].xml")?
850 .read_to_string(&mut ct_xml)?;
851 pkg.content_types = crate::opc::package::parse_content_types_public(&ct_xml)?;
852 for i in 0..zip.len() {
854 let mut e = zip.by_index(i)?;
855 let name = e.name().to_string();
856 if name == "[Content_Types].xml" || e.is_dir() {
857 continue;
858 }
859 let mut blob = Vec::with_capacity(e.size() as usize);
860 e.read_to_end(&mut blob)?;
861 let ct_str: String = if name.ends_with(".rels") {
862 crate::opc::package::ct::RELATIONSHIPS.to_string()
863 } else {
864 let partname = format!("/{}", name);
869 crate::opc::package::derive_content_type(&pkg.content_types, &partname)
870 };
871 let partname = format!("/{}", name);
872 let p = Part::new(PartName::from_unchecked(partname), ct_str, blob);
873 pkg.parts.insert(p.partname.as_str().to_string(), p);
874 }
875 Self::from_opc(pkg)
876 }
877
878 fn from_opc(pkg: OpcPackage) -> crate::Result<Self> {
902 type DiagramTask = (
906 String,
907 String,
908 String,
909 String,
910 String,
911 String,
912 String,
913 String,
914 );
915 let id_counter = Rc::new(Cell::new(2));
917 let mut pres = Presentation {
918 slides: Slides::new(),
919 slide_layouts: SlideLayouts::new(),
920 slide_masters: SlideMasters::new(),
921 notes_masters: NotesMasters::new(),
922 theme: crate::oxml::theme::Theme::default(),
923 sld_master_ids: Vec::new(),
924 width: Emu(DEFAULT_WIDTH_EMU),
925 height: Emu(DEFAULT_HEIGHT_EMU),
926 id_counter: id_counter.clone(),
927 core_properties: CoreProperties::default(),
928 custom_properties: CustomProperties::default(),
929 comment_authors: crate::oxml::comments::CommentAuthorList::default(),
930 modify_protection: None,
931 sections: crate::oxml::section::SectionList::default(),
932 };
933 pres.ensure_default_master_and_layout()?;
934
935 let pres_part = pkg
937 .get_part("/ppt/presentation.xml")
938 .ok_or_else(|| crate::Error::opc("presentation.xml not found in package"))?;
939 let pres_xml = String::from_utf8_lossy(&pres_part.blob).into_owned();
940 let (sld_id_list, sld_w, sld_h, sld_master_id_list, sections) =
941 crate::oxml::parse_sld::parse_pres_root(&pres_xml)?;
942 pres.sld_master_ids = sld_master_id_list;
944 pres.sections = sections;
946 if let (Some(w), Some(h)) = (sld_w, sld_h) {
947 pres.width = w;
948 pres.height = h;
949 }
950
951 let pres_rels_part = pkg
953 .get_part("/ppt/_rels/presentation.xml.rels")
954 .ok_or_else(|| crate::Error::opc("presentation.xml.rels not found"))?;
955 let pres_rels_xml = String::from_utf8_lossy(&pres_rels_part.blob).into_owned();
956 let pres_rels = Relationships::from_xml(&pres_rels_xml)?;
957
958 let mut parsed_masters: Vec<(String, String, OxmlSldMaster)> = Vec::new(); let mut parsed_layouts: Vec<(String, String, OxmlSldLayout)> = Vec::new(); for r in pres_rels.iter() {
964 if !matches!(r.reltype, RelType::SlideMaster) {
965 continue;
966 }
967 let master_partname =
968 resolve_relative_partname("/ppt/presentation.xml", r.target.as_str());
969 let master_xml = match pkg.get_part(master_partname.as_str()) {
970 Some(p) => String::from_utf8_lossy(&p.blob).into_owned(),
971 None => continue,
972 };
973 let master = match crate::oxml::parse_sld::parse_sld_master(&master_xml) {
974 Ok(m) => m,
975 Err(_e) => continue,
976 };
977 let master_rels_path = rels_partname_for(master_partname.as_str());
979 if let Some(mrp) = pkg.get_part(master_rels_path.as_str()) {
980 let mrp_xml = String::from_utf8_lossy(&mrp.blob).into_owned();
981 if let Ok(master_rels) = Relationships::from_xml(&mrp_xml) {
982 for mr in master_rels.iter() {
983 if matches!(mr.reltype, RelType::SlideLayout) {
984 let layout_partname = resolve_relative_partname(
985 master_partname.as_str(),
986 mr.target.as_str(),
987 );
988 let layout_xml = match pkg.get_part(layout_partname.as_str()) {
989 Some(p) => String::from_utf8_lossy(&p.blob).into_owned(),
990 None => continue,
991 };
992 if let Ok(layout) =
993 crate::oxml::parse_sld::parse_sld_layout(&layout_xml)
994 {
995 parsed_layouts.push((mr.id.clone(), layout_partname, layout));
996 }
997 }
998 if matches!(mr.reltype, RelType::Theme) {
1000 let theme_partname = resolve_relative_partname(
1001 master_partname.as_str(),
1002 mr.target.as_str(),
1003 );
1004 if let Some(tp) = pkg.get_part(theme_partname.as_str()) {
1005 let theme_xml = String::from_utf8_lossy(&tp.blob).into_owned();
1006 if let Ok(theme) = crate::oxml::parse_sld::parse_theme(&theme_xml) {
1008 pres.theme = theme;
1009 }
1010 }
1011 }
1012 }
1013 }
1014 }
1015 parsed_masters.push((r.id.clone(), master_partname, master));
1016 }
1017 if !parsed_masters.is_empty() {
1019 pres.slide_masters.items.clear();
1020 pres.slide_layouts.items.clear();
1021 for (rid, partname, oxml) in parsed_masters {
1022 let idx = pres.slide_masters.items.len();
1023 pres.slide_masters.items.push(SlideMasterRef {
1024 idx,
1025 partname,
1026 rid,
1027 oxml: Rc::new(std::cell::RefCell::new(oxml)),
1028 });
1029 }
1030 for (rid, partname, oxml) in parsed_layouts {
1031 let idx = pres.slide_layouts.items.len();
1032 pres.slide_layouts.items.push(SlideLayoutRef {
1033 idx,
1034 partname,
1035 rid,
1036 oxml: Rc::new(std::cell::RefCell::new(oxml)),
1037 });
1038 }
1039 }
1040
1041 for r in pres_rels.iter() {
1045 if !matches!(r.reltype, RelType::NotesMaster) {
1046 continue;
1047 }
1048 let nm_partname = resolve_relative_partname("/ppt/presentation.xml", r.target.as_str());
1049 let nm_xml = match pkg.get_part(nm_partname.as_str()) {
1050 Some(p) => String::from_utf8_lossy(&p.blob).into_owned(),
1051 None => continue,
1052 };
1053 let nm = match crate::oxml::parse_sld::parse_notes_master(&nm_xml) {
1054 Ok(m) => m,
1055 Err(_e) => continue,
1056 };
1057 let idx = pres.notes_masters.items.len();
1058 pres.notes_masters.items.push(NotesMasterRef {
1059 idx,
1060 partname: nm_partname,
1061 rid: r.id.clone(),
1062 oxml: Rc::new(std::cell::RefCell::new(nm)),
1063 });
1064 }
1065
1066 let mut max_id_seen: u32 = 2;
1069 for (sld_id, rid) in &sld_id_list {
1070 let rel = match pres_rels.get(rid) {
1072 Some(r) => r,
1073 None => {
1074 continue;
1076 }
1077 };
1078 let slide_partname =
1081 resolve_relative_partname("/ppt/presentation.xml", rel.target.as_str());
1082 let slide_part = match pkg.get_part(slide_partname.as_str()) {
1084 Some(p) => p,
1085 None => continue,
1086 };
1087 let slide_xml = String::from_utf8_lossy(&slide_part.blob).into_owned();
1088 let mut sld = match crate::oxml::parse_sld::parse_sld(&slide_xml) {
1089 Ok(s) => s,
1090 Err(_e) => {
1091 continue;
1093 }
1094 };
1095 sld.id = *sld_id;
1097
1098 let rels_path = rels_partname_for(slide_partname.as_str());
1100 let mut layout_rid = String::from("rId1");
1101 let mut notes_rid: Option<String> = None;
1102 let mut notes_target_raw: Option<String> = None;
1105 let mut comments_rid: Option<String> = None;
1107 let mut comments_target_raw: Option<String> = None;
1108 let mut image_rels: Vec<(String, String)> = Vec::new(); let mut diagram_rel_map: std::collections::HashMap<String, String> =
1114 std::collections::HashMap::new();
1115 let mut chart_rel_map: std::collections::HashMap<String, String> =
1118 std::collections::HashMap::new();
1119 if let Some(rels_part) = pkg.get_part(rels_path.as_str()) {
1120 let rels_xml = String::from_utf8_lossy(&rels_part.blob).into_owned();
1121 if let Ok(slide_rels) = Relationships::from_xml(&rels_xml) {
1122 for r in slide_rels.iter() {
1123 match r.reltype {
1124 RelType::SlideLayout => {
1125 layout_rid = r.id.clone();
1126 }
1127 RelType::NotesSlide => {
1128 notes_rid = Some(r.id.clone());
1129 notes_target_raw = Some(r.target.as_str().to_string());
1131 }
1132 RelType::Comments => {
1133 comments_rid = Some(r.id.clone());
1134 comments_target_raw = Some(r.target.as_str().to_string());
1135 }
1136 RelType::Image => {
1137 let rel_target = r.target.as_str();
1138 let abs =
1139 resolve_relative_partname(slide_partname.as_str(), rel_target);
1140 image_rels.push((r.id.clone(), abs));
1141 }
1142 RelType::DiagramData
1143 | RelType::DiagramLayout
1144 | RelType::DiagramQuickStyle
1145 | RelType::DiagramColors => {
1146 let rel_target = r.target.as_str();
1148 let abs =
1149 resolve_relative_partname(slide_partname.as_str(), rel_target);
1150 diagram_rel_map.insert(r.id.clone(), abs);
1151 }
1152 RelType::Chart => {
1153 let rel_target = r.target.as_str();
1157 let abs =
1158 resolve_relative_partname(slide_partname.as_str(), rel_target);
1159 chart_rel_map.insert(r.id.clone(), abs);
1160 }
1161 _ => {}
1162 }
1163 }
1164 }
1165 }
1166 sld.set_layout_rid(layout_rid.clone());
1168
1169 let mut parsed_notes_partname: Option<String> = None;
1175 let mut parsed_notes_rels_target: Option<String> = None;
1176 if let Some(raw_target) = ¬es_target_raw {
1177 let notes_partname =
1180 resolve_relative_partname(slide_partname.as_str(), raw_target.as_str());
1181 parsed_notes_partname = Some(notes_partname.clone());
1182 let notes_rels_path = rels_partname_for(¬es_partname);
1186 if let Some(nrp) = pkg.get_part(notes_rels_path.as_str()) {
1187 let nrp_xml = String::from_utf8_lossy(&nrp.blob).into_owned();
1188 if let Ok(nr) = Relationships::from_xml(&nrp_xml) {
1189 for r in nr.iter() {
1190 if matches!(r.reltype, RelType::Slide) {
1191 parsed_notes_rels_target = Some(r.target.as_str().to_string());
1192 break;
1193 }
1194 }
1195 }
1196 }
1197 if let Some(np) = pkg.get_part(notes_partname.as_str()) {
1198 let notes_xml = String::from_utf8_lossy(&np.blob).into_owned();
1199 if let Ok(tb) = crate::oxml::parse_sld::parse_notes(¬es_xml) {
1200 sld.notes = Some(tb);
1201 }
1202 }
1203 }
1204
1205 let mut parsed_comments_partname: Option<String> = None;
1208 let mut parsed_comments_lst: Option<crate::oxml::comments::CommentList> = None;
1209 if let Some(raw_target) = &comments_target_raw {
1210 let comments_partname =
1213 resolve_relative_partname(slide_partname.as_str(), raw_target.as_str());
1214 parsed_comments_partname = Some(comments_partname.clone());
1215 if let Some(cp) = pkg.get_part(comments_partname.as_str()) {
1217 let comments_xml = String::from_utf8_lossy(&cp.blob).into_owned();
1218 if let Ok(lst) = crate::oxml::parse_sld::parse_comments(&comments_xml) {
1219 parsed_comments_lst = Some(lst);
1220 }
1221 }
1222 }
1223
1224 for shape in &sld.shapes {
1228 let sp_id = match shape {
1229 crate::oxml::SlideShape::Sp(sp) => sp.id,
1230 crate::oxml::SlideShape::Pic(pic) => pic.id,
1231 crate::oxml::SlideShape::CxnSp(c) => c.id,
1232 crate::oxml::SlideShape::Group(g) => g.id,
1233 crate::oxml::SlideShape::GraphicFrame(gf) => gf.id,
1234 };
1235 if sp_id > max_id_seen {
1236 max_id_seen = sp_id;
1237 }
1238 }
1239
1240 let mut slide = Slide::from_sld(sld, id_counter.clone(), layout_rid);
1241 if let Some(nrid) = notes_rid {
1242 slide.set_notes_rid(nrid);
1243 }
1244 if let Some(np) = parsed_notes_partname {
1248 slide.set_notes_partname(np);
1249 }
1250 if let Some(nrt) = parsed_notes_rels_target {
1251 slide.set_notes_slide_rel_target(nrt);
1252 }
1253 if let Some(crid) = comments_rid {
1257 slide.set_comments_rid(crid);
1258 }
1259 if let Some(cp) = parsed_comments_partname {
1260 slide.set_comments_partname(cp);
1261 }
1262 if let Some(clst) = parsed_comments_lst {
1263 slide.set_comments(Some(clst));
1264 }
1265
1266 for (rid, target_partname) in &image_rels {
1270 if let Some(media_part) = pkg.get_part(target_partname.as_str()) {
1271 let partname = crate::opc::part::new_part_name(target_partname.as_str());
1272 slide.register_media(MediaEntry {
1273 partname,
1274 content_type: media_part.content_type.clone(),
1275 blob: media_part.blob.clone(),
1276 rid: rid.clone(),
1277 });
1278 }
1279 }
1280
1281 let mut diagram_tasks: Vec<DiagramTask> = Vec::new();
1296 for shape in &slide.inner.shapes {
1297 if let crate::oxml::SlideShape::GraphicFrame(gf) = shape {
1298 if let crate::oxml::shape::Graphic::SmartArt(smart_ref) = &gf.graphic {
1299 let (Some(dm_rid), Some(lo_rid), Some(qs_rid), Some(cs_rid)) = (
1300 &smart_ref.dm_rid,
1301 &smart_ref.lo_rid,
1302 &smart_ref.qs_rid,
1303 &smart_ref.cs_rid,
1304 ) else {
1305 continue;
1307 };
1308 let (
1309 Some(data_partname),
1310 Some(layout_partname),
1311 Some(quick_style_partname),
1312 Some(colors_partname),
1313 ) = (
1314 diagram_rel_map.get(dm_rid),
1315 diagram_rel_map.get(lo_rid),
1316 diagram_rel_map.get(qs_rid),
1317 diagram_rel_map.get(cs_rid),
1318 )
1319 else {
1320 continue;
1322 };
1323 diagram_tasks.push((
1324 dm_rid.clone(),
1325 lo_rid.clone(),
1326 qs_rid.clone(),
1327 cs_rid.clone(),
1328 data_partname.clone(),
1329 layout_partname.clone(),
1330 quick_style_partname.clone(),
1331 colors_partname.clone(),
1332 ));
1333 }
1334 }
1335 }
1336 for (
1337 dm_rid,
1338 lo_rid,
1339 qs_rid,
1340 cs_rid,
1341 data_partname,
1342 layout_partname,
1343 quick_style_partname,
1344 colors_partname,
1345 ) in diagram_tasks
1346 {
1347 let data_xml = pkg
1349 .get_part(data_partname.as_str())
1350 .map(|p| String::from_utf8_lossy(&p.blob).into_owned())
1351 .unwrap_or_default();
1352 let layout_xml = pkg
1353 .get_part(layout_partname.as_str())
1354 .map(|p| String::from_utf8_lossy(&p.blob).into_owned())
1355 .unwrap_or_default();
1356 let quick_style_xml = pkg
1357 .get_part(quick_style_partname.as_str())
1358 .map(|p| String::from_utf8_lossy(&p.blob).into_owned())
1359 .unwrap_or_default();
1360 let colors_xml = pkg
1361 .get_part(colors_partname.as_str())
1362 .map(|p| String::from_utf8_lossy(&p.blob).into_owned())
1363 .unwrap_or_default();
1364 let entry = DiagramEntry {
1365 data_partname: crate::opc::part::new_part_name(data_partname.as_str()),
1366 layout_partname: crate::opc::part::new_part_name(layout_partname.as_str()),
1367 quick_style_partname: crate::opc::part::new_part_name(
1368 quick_style_partname.as_str(),
1369 ),
1370 colors_partname: crate::opc::part::new_part_name(colors_partname.as_str()),
1371 data_xml,
1372 layout_xml,
1373 quick_style_xml,
1374 colors_xml,
1375 data_rid: dm_rid,
1376 layout_rid: lo_rid,
1377 quick_style_rid: qs_rid,
1378 colors_rid: cs_rid,
1379 };
1380 slide.register_diagram(entry);
1381 }
1382
1383 let mut chart_tasks: Vec<(String, String)> = Vec::new(); for shape in &slide.inner.shapes {
1397 if let crate::oxml::SlideShape::GraphicFrame(gf) = shape {
1398 if let crate::oxml::shape::Graphic::Chart(chart) = &gf.graphic {
1399 let rid = chart.rid.as_str();
1400 if rid.is_empty() {
1401 continue;
1402 }
1403 if let Some(chart_partname) = chart_rel_map.get(rid) {
1404 chart_tasks.push((rid.to_string(), chart_partname.clone()));
1405 }
1406 }
1407 }
1408 }
1409 for (rid, chart_partname) in chart_tasks {
1410 let Some(chart_part) = pkg.get_part(chart_partname.as_str()) else {
1411 continue;
1412 };
1413 let chart_xml = String::from_utf8_lossy(&chart_part.blob).into_owned();
1414 let Ok(mut parsed) = crate::oxml::chart::Chart::parse_from_xml(&chart_xml) else {
1415 continue;
1416 };
1417 parsed.rid = rid.clone();
1418 for s in slide.inner.shapes.iter_mut() {
1420 if let crate::oxml::SlideShape::GraphicFrame(gf2) = s {
1421 if let crate::oxml::shape::Graphic::Chart(c2) = &mut gf2.graphic {
1422 if c2.rid == rid {
1423 *c2 = parsed.clone();
1424 break;
1425 }
1426 }
1427 }
1428 }
1429 slide.register_chart(ChartEntry {
1432 partname: crate::opc::part::new_part_name(chart_partname.as_str()),
1433 chart: parsed,
1434 rid,
1435 xlsx_blob: None,
1436 });
1437 }
1438
1439 let entry = SlideEntry::new(slide, *sld_id, rid.clone(), slide_partname);
1440 pres.slides.push_entry(entry);
1441 }
1442 pres.id_counter.set(max_id_seen);
1444
1445 if let Some(custom_part) = pkg.get_part("/docProps/custom.xml") {
1447 let custom_xml = String::from_utf8_lossy(&custom_part.blob).into_owned();
1448 pres.custom_properties = CustomProperties::from_xml(&custom_xml);
1449 }
1450
1451 if let Some(authors_part) = pkg.get_part("/ppt/commentAuthors.xml") {
1453 let authors_xml = String::from_utf8_lossy(&authors_part.blob).into_owned();
1454 if let Ok(lst) = crate::oxml::parse_sld::parse_comment_authors(&authors_xml) {
1455 pres.comment_authors = lst;
1456 }
1457 }
1458
1459 Ok(pres)
1460 }
1461
1462 pub fn slides(&self) -> &Slides {
1464 &self.slides
1465 }
1466 pub fn slides_mut(&mut self) -> &mut Slides {
1468 &mut self.slides
1469 }
1470
1471 pub fn id_counter(&self) -> std::rc::Rc<std::cell::Cell<u32>> {
1475 self.id_counter.clone()
1476 }
1477
1478 pub fn slide_layouts(&self) -> &SlideLayouts {
1480 &self.slide_layouts
1481 }
1482 pub fn slide_layouts_mut(&mut self) -> &mut SlideLayouts {
1484 &mut self.slide_layouts
1485 }
1486
1487 pub fn layout_for_slide(&self, slide_idx: usize) -> Option<SlideLayoutRef> {
1511 let entry = self.slides.get(slide_idx)?;
1512 let layout_rid = entry.sld.layout_rid();
1513 self.slide_layouts
1514 .items
1515 .iter()
1516 .find(|l| l.rid == layout_rid)
1517 .cloned()
1518 }
1519
1520 pub fn slides_using_layout(&self, layout_rid: &str) -> Vec<usize> {
1530 self.slides
1531 .iter()
1532 .enumerate()
1533 .filter(|(_, entry)| entry.sld.layout_rid() == layout_rid)
1534 .map(|(i, _)| i)
1535 .collect()
1536 }
1537
1538 pub fn slide_masters(&self) -> &SlideMasters {
1540 &self.slide_masters
1541 }
1542 pub fn slide_masters_mut(&mut self) -> &mut SlideMasters {
1544 &mut self.slide_masters
1545 }
1546
1547 pub fn slide_width(&self) -> Emu {
1549 self.width
1550 }
1551 pub fn slide_height(&self) -> Emu {
1553 self.height
1554 }
1555
1556 pub fn set_slide_size(&mut self, width: Emu, height: Emu) {
1561 self.width = width;
1562 self.height = height;
1563 }
1564
1565 pub fn core_properties(&self) -> &CoreProperties {
1569 &self.core_properties
1570 }
1571
1572 pub fn core_properties_mut(&mut self) -> &mut CoreProperties {
1577 &mut self.core_properties
1578 }
1579
1580 pub fn custom_properties(&self) -> &CustomProperties {
1584 &self.custom_properties
1585 }
1586
1587 pub fn custom_properties_mut(&mut self) -> &mut CustomProperties {
1602 &mut self.custom_properties
1603 }
1604
1605 pub fn comment_authors(&self) -> &crate::oxml::comments::CommentAuthorList {
1609 &self.comment_authors
1610 }
1611
1612 pub fn comment_authors_mut(&mut self) -> &mut crate::oxml::comments::CommentAuthorList {
1625 &mut self.comment_authors
1626 }
1627
1628 pub fn sections(&self) -> &crate::oxml::section::SectionList {
1637 &self.sections
1638 }
1639
1640 pub fn sections_mut(&mut self) -> &mut crate::oxml::section::SectionList {
1660 &mut self.sections
1661 }
1662
1663 pub fn notes_masters(&self) -> &NotesMasters {
1683 &self.notes_masters
1684 }
1685
1686 pub fn notes_masters_mut(&mut self) -> &mut NotesMasters {
1690 &mut self.notes_masters
1691 }
1692
1693 pub fn notes_master(&self) -> Option<&NotesMasterRef> {
1697 self.notes_masters.first()
1698 }
1699
1700 pub fn set_metadata(
1718 &mut self,
1719 title: Option<&str>,
1720 creator: Option<&str>,
1721 subject: Option<&str>,
1722 keywords: Option<&str>,
1723 ) {
1724 if let Some(t) = title {
1725 self.core_properties.title = Some(t.to_string());
1726 }
1727 if let Some(c) = creator {
1728 self.core_properties.creator = Some(c.to_string());
1729 }
1730 if let Some(s) = subject {
1731 self.core_properties.subject = Some(s.to_string());
1732 }
1733 if let Some(k) = keywords {
1734 self.core_properties.keywords = Some(k.to_string());
1735 }
1736 }
1737
1738 pub fn num_slides(&self) -> usize {
1742 self.slides.len()
1743 }
1744
1745 #[allow(clippy::field_reassign_with_default)]
1767 pub fn add_watermark(
1768 &mut self,
1769 text: &str,
1770 font_size_pt: Option<f64>,
1771 color: Option<crate::units::RGBColor>,
1772 rotation_deg: Option<i32>,
1773 alpha: Option<i32>,
1774 font_name: Option<&str>,
1775 ) -> crate::Result<()> {
1776 let fs = font_size_pt.unwrap_or(36.0);
1777 let clr = color.unwrap_or(crate::units::RGBColor(0xC0, 0xC0, 0xC0));
1778 let rot = rotation_deg.unwrap_or(-30);
1779 let alpha_val = alpha.unwrap_or(30_000);
1780 let font = font_name.unwrap_or("Calibri");
1781 let rot_emu = rot * 60000; for i in 0..self.slides.len() {
1784 let slide = self
1786 .slides
1787 .get_mut(i)
1788 .ok_or(crate::Error::IndexOutOfRange(i))?;
1789 let sld = &mut slide.sld;
1790 let id = sld.next_shape_id();
1791
1792 let mut sp = crate::oxml::shape::Sp::default();
1793 sp.id = id;
1794 sp.name = format!("Watermark {}", i + 1);
1795 sp.c_nv_sp_pr_tx_box = true;
1796
1797 let cx = self.width.0;
1799 let cy = self.height.0;
1800 sp.properties.xfrm.off_x = Some(Emu(cx / 4));
1801 sp.properties.xfrm.off_y = Some(Emu(cy / 4));
1802 sp.properties.xfrm.ext_cx = Some(Emu(cx / 2));
1803 sp.properties.xfrm.ext_cy = Some(Emu(cy / 2));
1804 sp.properties.xfrm.rot = Some(rot_emu);
1805 sp.properties.geometry = Some(crate::oxml::sppr::Geometry::preset(
1806 crate::oxml::simpletypes::PresetGeometry::Rectangle,
1807 ));
1808 sp.properties.fill = crate::oxml::sppr::Fill::None;
1810 sp.properties.line = None;
1811
1812 let mut tb = crate::oxml::txbody::TextBody::new();
1814 let mut para = crate::oxml::txbody::Paragraph::default();
1815 let mut run = crate::oxml::txbody::Run::default();
1816 run.text = text.to_string();
1817 run.properties.size = Some(crate::units::Pt(fs));
1818 run.properties.color = crate::oxml::color::Color::RGB(clr);
1819 run.properties.bold = true;
1820 run.properties.alpha = Some(alpha_val);
1821 run.properties.latin_font = Some(font.to_string());
1823 run.properties.eastasia_font = Some("宋体".to_string());
1824 para.runs.push(run);
1825 para.properties.alignment = Some(crate::oxml::simpletypes::Alignment::Center);
1827 tb.paragraphs.push(para);
1828 sp.text = tb;
1829
1830 sld.inner.shapes.push(crate::oxml::SlideShape::Sp(sp));
1831 }
1832 Ok(())
1833 }
1834
1835 #[allow(clippy::field_reassign_with_default)]
1860 #[allow(clippy::too_many_arguments)]
1861 pub fn add_image_watermark(
1862 &mut self,
1863 image_bytes: &[u8],
1864 ext: &str,
1865 alpha: Option<i32>,
1866 left: Option<Emu>,
1867 top: Option<Emu>,
1868 width: Option<Emu>,
1869 height: Option<Emu>,
1870 ) -> crate::Result<()> {
1871 if image_bytes.is_empty() {
1872 return Err(crate::Error::encryption("image bytes must not be empty"));
1873 }
1874 let alpha_val = alpha.unwrap_or(30_000);
1875 let cx = self.width.0;
1876 let cy = self.height.0;
1877 let wm_left = left.unwrap_or(Emu(cx / 4));
1878 let wm_top = top.unwrap_or(Emu(cy / 4));
1879 let wm_width = width.unwrap_or(Emu(cx / 2));
1880 let wm_height = height.unwrap_or(Emu(cy / 2));
1881
1882 let ext_norm = if ext.starts_with('.') {
1884 ext.to_string()
1885 } else {
1886 format!(".{}", ext)
1887 };
1888 let global_media_idx = self
1890 .slides
1891 .get(0)
1892 .map(|s| s.sld.next_media_index())
1893 .unwrap_or(1);
1894 let shared_partname = crate::opc::part::new_part_name(
1895 format!("/ppt/media/image{}{}", global_media_idx, ext_norm).as_str(),
1896 );
1897 let ct = crate::shape::picture::content_type_for(&ext_norm);
1898
1899 for i in 0..self.slides.len() {
1900 let slide = self
1902 .slides
1903 .get_mut(i)
1904 .ok_or(crate::Error::IndexOutOfRange(i))?;
1905 let id = slide.sld.next_shape_id();
1906 let rid = slide.sld.allocate_image_rid();
1907
1908 let mut pic = crate::oxml::shape::Pic::default();
1910 pic.id = id;
1911 pic.name = format!("WatermarkImage {}", i + 1);
1912 pic.rid = rid.clone();
1913 pic.alpha = Some(alpha_val);
1914 pic.fill_mode = crate::oxml::sppr::BlipFillMode::Stretch;
1915 pic.properties.xfrm.off_x = Some(wm_left);
1916 pic.properties.xfrm.off_y = Some(wm_top);
1917 pic.properties.xfrm.ext_cx = Some(wm_width);
1918 pic.properties.xfrm.ext_cy = Some(wm_height);
1919 pic.properties.fill = crate::oxml::sppr::Fill::None;
1921 pic.properties.line = None;
1922
1923 slide.sld.register_media(MediaEntry {
1925 partname: shared_partname.clone(),
1926 content_type: ct.to_string(),
1927 blob: image_bytes.to_vec(),
1928 rid,
1929 });
1930
1931 slide
1932 .sld
1933 .inner
1934 .shapes
1935 .push(crate::oxml::SlideShape::Pic(pic));
1936 }
1937 Ok(())
1938 }
1939
1940 pub fn set_write_protection(&mut self, password: &str) -> crate::Result<()> {
1954 if password.is_empty() {
1955 return Err(crate::Error::encryption("password must not be empty"));
1956 }
1957 let salt = crate::crypto::generate_random_bytes(crate::crypto::SALT_LEN);
1958 self.modify_protection = Some(crate::crypto::ModifyProtection::from_password(
1959 password,
1960 &salt,
1961 crate::crypto::MODIFY_SPIN_COUNT,
1962 ));
1963 Ok(())
1964 }
1965
1966 pub fn verify_write_protection(&self, password: &str) -> crate::Result<bool> {
1976 match &self.modify_protection {
1977 Some(mp) => Ok(mp.verify_password(password)),
1978 None => Ok(false),
1979 }
1980 }
1981
1982 pub fn remove_write_protection(&mut self) {
1984 self.modify_protection = None;
1985 }
1986
1987 pub fn is_write_protected(&self) -> bool {
1989 self.modify_protection.is_some()
1990 }
1991
1992 pub fn save_encrypted(&self, path: impl AsRef<Path>, password: &str) -> crate::Result<()> {
2005 let pkg = self.to_opc_package()?;
2006 let zip_bytes = pkg.to_bytes()?;
2007 let encrypted = crate::crypto::encrypt_package(&zip_bytes, password)?;
2008 std::fs::write(path.as_ref(), &encrypted)?;
2009 Ok(())
2010 }
2011
2012 pub fn to_encrypted_bytes(&self, password: &str) -> crate::Result<Vec<u8>> {
2016 let pkg = self.to_opc_package()?;
2017 let zip_bytes = pkg.to_bytes()?;
2018 crate::crypto::encrypt_package(&zip_bytes, password)
2019 }
2020
2021 pub fn open_encrypted(path: impl AsRef<Path>, password: &str) -> crate::Result<Self> {
2031 let bytes = std::fs::read(path.as_ref())?;
2032 Self::load_encrypted_bytes(&bytes, password)
2033 }
2034
2035 pub fn load_encrypted_bytes(bytes: &[u8], password: &str) -> crate::Result<Self> {
2037 let decrypted = crate::crypto::decrypt_package(bytes, password)?;
2038 Self::load_bytes(&decrypted)
2039 }
2040
2041 pub fn is_encrypted_file(path: impl AsRef<Path>) -> crate::Result<bool> {
2045 let bytes = std::fs::read(path.as_ref())?;
2046 Ok(crate::crypto::is_encrypted_package(&bytes))
2047 }
2048
2049 pub fn is_encrypted_bytes(bytes: &[u8]) -> bool {
2051 crate::crypto::is_encrypted_package(bytes)
2052 }
2053
2054 pub fn save(&self, path: impl AsRef<Path>) -> crate::Result<()> {
2059 let pkg = self.to_opc_package()?;
2060 pkg.save(path)
2061 }
2062
2063 pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
2067 let pkg = self.to_opc_package()?;
2068 pkg.to_bytes()
2069 }
2070
2071 fn to_opc_package(&self) -> crate::Result<OpcPackage> {
2086 let mut pkg = OpcPackage::new();
2087
2088 let mut root_rels = Relationships::new();
2091 root_rels.add(Relationship::internal(
2092 "rId1",
2093 RelType::OfficeDocument,
2094 new_part_name("/ppt/presentation.xml"),
2095 ))?;
2096 root_rels.add(Relationship::internal(
2097 "rId2",
2098 crate::opc::rels::RelType::Other(
2099 "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties".to_string(),
2100 ),
2101 new_part_name("/docProps/core.xml"),
2102 ))?;
2103 root_rels.add(Relationship::internal(
2104 "rId3",
2105 crate::opc::rels::RelType::Other(
2106 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties".to_string(),
2107 ),
2108 new_part_name("/docProps/app.xml"),
2109 ))?;
2110 if !self.custom_properties.is_empty() {
2112 root_rels.add(Relationship::internal(
2113 "rId4",
2114 crate::opc::rels::RelType::Other(
2115 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties".to_string(),
2116 ),
2117 new_part_name("/docProps/custom.xml"),
2118 ))?;
2119 }
2120 let root_rels_xml = root_rels.to_xml();
2121 let root_rels_part = Part::new(
2122 new_part_name("/_rels/.rels"),
2123 ct::RELATIONSHIPS,
2124 root_rels_xml.into_bytes(),
2125 );
2126 pkg.put_part(root_rels_part);
2127
2128 let core_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2131<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
2132 xmlns:dc="http://purl.org/dc/elements/1.1/"
2133 xmlns:dcterms="http://purl.org/dc/terms/"
2134 xmlns:dcmitype="http://purl.org/dc/dcmitype/"
2135 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2136 <dc:title>pptx-rs</dc:title>
2137 <dc:creator>pptx-rs</dc:creator>
2138 <cp:lastModifiedBy>pptx-rs</cp:lastModifiedBy>
2139</cp:coreProperties>"#;
2140 pkg.put_part(Part::new(
2141 new_part_name("/docProps/core.xml"),
2142 ct::CORE_PROPS,
2143 core_xml.as_bytes().to_vec(),
2144 ));
2145
2146 let app_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2149<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
2150 xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
2151 <Application>pptx-rs</Application>
2152 <AppVersion>1.0</AppVersion>
2153</Properties>"#;
2154 pkg.put_part(Part::new(
2155 new_part_name("/docProps/app.xml"),
2156 ct::APP_PROPS,
2157 app_xml.as_bytes().to_vec(),
2158 ));
2159
2160 if !self.custom_properties.is_empty() {
2163 let custom_xml = self.custom_properties.to_xml();
2164 if !custom_xml.is_empty() {
2165 pkg.put_part(Part::new(
2166 new_part_name("/docProps/custom.xml"),
2167 ct::CUSTOM_PROPS,
2168 custom_xml.into_bytes(),
2169 ));
2170 }
2171 }
2172
2173 let theme_xml = if self.theme.name.is_empty()
2177 && self.theme.color_scheme.dk1.is_none()
2178 && self.theme.font_scheme.major_latin.is_empty()
2179 {
2180 default_theme_xml()
2182 } else {
2183 self.theme.to_xml()
2184 };
2185 pkg.put_part(Part::new(
2186 new_part_name("/ppt/theme/theme1.xml"),
2187 ct::THEME,
2188 theme_xml.into_bytes(),
2189 ));
2190
2191 let master_xml = if let Some(master_ref) = self.slide_masters.items.first() {
2195 master_ref.oxml.borrow().to_xml()
2196 } else {
2197 OxmlSldMaster::default().to_xml()
2198 };
2199 let master_partname = new_part_name("/ppt/slideMasters/slideMaster1.xml");
2200 let mut master_rels = Relationships::new();
2201 master_rels.add(Relationship::internal_str(
2202 "rId1",
2203 RelType::SlideLayout,
2204 "../slideLayouts/slideLayout1.xml",
2205 ))?;
2206 master_rels.add(Relationship::internal_str(
2207 "rId2",
2208 RelType::Theme,
2209 "../theme/theme1.xml",
2210 ))?;
2211 let master_rels_xml = master_rels.to_xml();
2212 let master_rels_partname = rels_partname_for(master_partname.as_str());
2213 pkg.put_part(Part::new(
2214 PartName::from_unchecked(master_rels_partname),
2215 ct::RELATIONSHIPS,
2216 master_rels_xml.into_bytes(),
2217 ));
2218 pkg.put_part(Part::new(
2219 master_partname,
2220 ct::SLIDE_MASTER,
2221 master_xml.into_bytes(),
2222 ));
2223
2224 let layout_xml = if let Some(layout_ref) = self.slide_layouts.items.first() {
2228 layout_ref.oxml.borrow().to_xml()
2229 } else {
2230 OxmlSldLayout::default().to_xml()
2231 };
2232 let layout_partname = new_part_name("/ppt/slideLayouts/slideLayout1.xml");
2233 let mut layout_rels = Relationships::new();
2234 layout_rels.add(Relationship::internal_str(
2235 "rId1",
2236 RelType::SlideMaster,
2237 "../slideMasters/slideMaster1.xml",
2238 ))?;
2239 let layout_rels_partname = rels_partname_for(layout_partname.as_str());
2240 pkg.put_part(Part::new(
2241 PartName::from_unchecked(layout_rels_partname),
2242 ct::RELATIONSHIPS,
2243 layout_rels.to_xml().into_bytes(),
2244 ));
2245 pkg.put_part(Part::new(
2246 layout_partname,
2247 ct::SLIDE_LAYOUT,
2248 layout_xml.into_bytes(),
2249 ));
2250
2251 let mut pres_rels = Relationships::new();
2256 pres_rels.add(Relationship::internal_str(
2257 "rIdP1",
2258 RelType::SlideMaster,
2259 "slideMasters/slideMaster1.xml",
2260 ))?;
2261 pres_rels.add(Relationship::internal_str(
2262 "rIdP2",
2263 RelType::SlideLayout,
2264 "slideLayouts/slideLayout1.xml",
2265 ))?;
2266 pres_rels.add(Relationship::internal_str(
2267 "rIdP3",
2268 RelType::Theme,
2269 "theme/theme1.xml",
2270 ))?;
2271 pres_rels.add(Relationship::internal_str(
2272 "rIdP4",
2273 crate::opc::rels::RelType::Other(
2274 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"
2275 .to_string(),
2276 ),
2277 "presProps.xml",
2278 ))?;
2279 pres_rels.add(Relationship::internal_str(
2280 "rIdP5",
2281 crate::opc::rels::RelType::Other(
2282 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"
2283 .to_string(),
2284 ),
2285 "viewProps.xml",
2286 ))?;
2287 pres_rels.add(Relationship::internal_str(
2288 "rIdP6",
2289 crate::opc::rels::RelType::Other(
2290 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"
2291 .to_string(),
2292 ),
2293 "tableStyles.xml",
2294 ))?;
2295
2296 let sld_master_ids: Vec<crate::oxml::presentation::SldMasterIdEntry> =
2299 if !self.sld_master_ids.is_empty() {
2300 self.sld_master_ids
2301 .iter()
2302 .map(|(id, rid)| crate::oxml::presentation::SldMasterIdEntry {
2303 id: *id,
2304 rid: rid.clone(),
2305 })
2306 .collect()
2307 } else {
2308 Vec::new()
2310 };
2311 let mut pres_root = PresentationRoot {
2312 slide_width: Some(self.width),
2313 slide_height: Some(self.height),
2314 slide_ids: Vec::new(),
2315 sld_master_ids,
2316 sections: self.sections.clone(),
2319 ..Default::default()
2320 };
2321
2322 let mut notes_index: u32 = 0;
2324 let mut comments_index: u32 = 0;
2325 let mut chart_global_index: u32 = 0;
2327 let mut chart_xlsx_global_index: u32 = 0;
2329 let mut ole_global_index: u32 = 0;
2331 let mut video_global_index: u32 = 0;
2335 let mut audio_global_index: u32 = 0;
2336 let mut diagram_global_index: u32 = 0;
2338 for (i, entry) in self.slides.iter().enumerate() {
2339 let partname = PartName::from_unchecked(entry.partname.clone());
2340 let rid = entry.rid.clone();
2341 let mut sld = entry.sld.clone();
2343 sld.set_layout_rid("rId1".to_string());
2344
2345 let mut extra_rels = Relationships::new();
2347 extra_rels.add(Relationship::internal_str(
2348 "rId1",
2349 RelType::SlideLayout,
2350 "../slideLayouts/slideLayout1.xml",
2351 ))?;
2352 for media in &entry.sld.media_entries {
2354 let media_target = format!(
2355 "../media/{}",
2356 media.partname.as_str().trim_start_matches("/ppt/media/")
2357 );
2358 extra_rels.add(Relationship::internal_str(
2359 media.rid.clone(),
2360 RelType::Image,
2361 media_target,
2362 ))?;
2363 }
2364 let mut this_notes_rid: Option<String> = None;
2372 if let Some(notes_tb) = sld.notes() {
2373 notes_index += 1;
2374 let notes_rid = format!("rIdNotes{}", notes_index);
2377 let notes_partname = match entry.sld.notes_partname() {
2379 Some(p) => p.to_string(),
2380 None => format!("/ppt/notesSlides/notesSlide{}.xml", notes_index),
2381 };
2382 let notes_slide_target = match entry.sld.notes_slide_rel_target() {
2384 Some(t) => t.to_string(),
2385 None => format!("../slides/slide{}.xml", i + 1),
2386 };
2387 this_notes_rid = Some(notes_rid.clone());
2388 let notes_target_rel = {
2391 let trimmed = notes_partname.trim_start_matches("/ppt/");
2393 format!("../{}", trimmed)
2394 };
2395 extra_rels.add(Relationship::internal_str(
2396 notes_rid.clone(),
2397 RelType::NotesSlide,
2398 notes_target_rel,
2399 ))?;
2400 let pres_target_rel = {
2404 let trimmed = notes_partname.trim_start_matches("/ppt/");
2405 trimmed.to_string()
2406 };
2407 pres_rels.add(Relationship::internal_str(
2408 format!("rIdNotesPres{}", notes_index),
2409 RelType::NotesSlide,
2410 pres_target_rel,
2411 ))?;
2412 let mut notes_rels = Relationships::new();
2415 notes_rels.add(Relationship::internal_str(
2416 "rId1",
2417 RelType::Slide,
2418 notes_slide_target,
2419 ))?;
2420 let notes_rels_partname = rels_partname_for(¬es_partname);
2421 pkg.put_part(Part::new(
2422 PartName::from_unchecked(notes_rels_partname),
2423 ct::RELATIONSHIPS,
2424 notes_rels.to_xml().into_bytes(),
2425 ));
2426 let notes_xml_str = crate::oxml::notes_xml(notes_tb);
2428 pkg.put_part(Part::new(
2429 PartName::from_unchecked(notes_partname),
2430 ct::NOTES_SLIDE,
2431 notes_xml_str.into_bytes(),
2432 ));
2433 }
2434 if let Some(rid) = this_notes_rid {
2436 sld.set_notes_rid(rid);
2437 }
2438
2439 let mut this_comments_rid: Option<String> = None;
2443 if let Some(comment_lst) = sld.comments() {
2444 comments_index += 1;
2445 let comments_rid = format!("rIdComments{}", comments_index);
2446 let comments_partname = match sld.comments_partname() {
2448 Some(p) => p.to_string(),
2449 None => format!("/ppt/comments/comment{}.xml", comments_index),
2450 };
2451 this_comments_rid = Some(comments_rid.clone());
2452 let comments_target_rel = {
2455 let trimmed = comments_partname.trim_start_matches("/ppt/");
2456 format!("../{}", trimmed)
2457 };
2458 extra_rels.add(Relationship::internal_str(
2459 comments_rid.clone(),
2460 RelType::Comments,
2461 comments_target_rel,
2462 ))?;
2463 let comments_xml_str = comment_lst.to_xml();
2465 pkg.put_part(Part::new(
2466 PartName::from_unchecked(comments_partname),
2467 ct::COMMENTS,
2468 comments_xml_str.into_bytes(),
2469 ));
2470 }
2471 if let Some(rid) = this_comments_rid {
2472 sld.set_comments_rid(rid);
2473 }
2474
2475 for chart_entry in &entry.sld.chart_entries {
2486 chart_global_index += 1;
2487 let chart_partname = format!("/ppt/charts/chart{}.xml", chart_global_index);
2488 let chart_target_rel = format!("../charts/chart{}.xml", chart_global_index);
2491 extra_rels.add(Relationship::internal_str(
2492 chart_entry.rid.clone(),
2493 RelType::Chart,
2494 chart_target_rel,
2495 ))?;
2496
2497 let mut chart_model = chart_entry.chart.clone();
2508 if let Some(xlsx_blob) = &chart_entry.xlsx_blob {
2509 chart_xlsx_global_index += 1;
2510 let xlsx_partname = format!(
2511 "/ppt/embeddings/Microsoft_Excel_Worksheet{}.xlsx",
2512 chart_xlsx_global_index
2513 );
2514 let xlsx_target_rel = format!(
2515 "../embeddings/Microsoft_Excel_Worksheet{}.xlsx",
2516 chart_xlsx_global_index
2517 );
2518 let xlsx_rid = format!("rIdXlsx{}", chart_xlsx_global_index);
2520
2521 pkg.put_part(Part::new(
2523 PartName::from_unchecked(xlsx_partname.clone()),
2524 ct::SPREADSHEET_XLSX,
2525 xlsx_blob.clone(),
2526 ));
2527
2528 let mut chart_rels = crate::opc::rels::Relationships::new();
2530 chart_rels.add(Relationship::internal_str(
2531 xlsx_rid.clone(),
2532 RelType::Package,
2533 xlsx_target_rel,
2534 ))?;
2535 let chart_rels_partname =
2536 format!("/ppt/charts/_rels/chart{}.xml.rels", chart_global_index);
2537 pkg.put_part(Part::new(
2538 PartName::from_unchecked(chart_rels_partname),
2539 ct::RELATIONSHIPS,
2540 chart_rels.to_xml().into_bytes(),
2541 ));
2542
2543 chart_model.external_data_rid = Some(xlsx_rid);
2545 }
2546
2547 let chart_xml = chart_model.to_xml();
2549 pkg.put_part(Part::new(
2550 PartName::from_unchecked(chart_partname),
2551 ct::CHART,
2552 chart_xml.into_bytes(),
2553 ));
2554 }
2555
2556 for ole_entry in &entry.sld.ole_entries {
2567 ole_global_index += 1;
2568 let ole_partname = format!("/ppt/embeddings/oleObject{}.bin", ole_global_index);
2569 let ole_target_rel = format!("../embeddings/oleObject{}.bin", ole_global_index);
2572 extra_rels.add(Relationship::internal_str(
2573 ole_entry.rid.clone(),
2574 RelType::OleObject,
2575 ole_target_rel,
2576 ))?;
2577 pkg.put_part(Part::new(
2579 PartName::from_unchecked(ole_partname),
2580 ct::OLE_OBJECT,
2581 ole_entry.blob.clone(),
2582 ));
2583 }
2584
2585 for video_entry in &entry.sld.video_entries {
2596 video_global_index += 1;
2597 let video_partname = format!("/ppt/media/media{}.mp4", video_global_index);
2598 let video_target_rel = format!("../media/media{}.mp4", video_global_index);
2600 extra_rels.add(Relationship::internal_str(
2601 video_entry.rid.clone(),
2602 RelType::Video,
2603 video_target_rel,
2604 ))?;
2605 pkg.put_part(Part::new(
2607 PartName::from_unchecked(video_partname),
2608 ct::VIDEO_MP4,
2609 video_entry.blob.clone(),
2610 ));
2611 }
2612
2613 for audio_entry in &entry.sld.audio_entries {
2616 audio_global_index += 1;
2617 let audio_partname = format!("/ppt/media/media{}.mp3", audio_global_index);
2618 let audio_target_rel = format!("../media/media{}.mp3", audio_global_index);
2619 extra_rels.add(Relationship::internal_str(
2620 audio_entry.rid.clone(),
2621 RelType::Audio,
2622 audio_target_rel,
2623 ))?;
2624 pkg.put_part(Part::new(
2625 PartName::from_unchecked(audio_partname),
2626 ct::AUDIO_MP3,
2627 audio_entry.blob.clone(),
2628 ));
2629 }
2630
2631 for diagram_entry in &entry.sld.diagram_entries {
2649 diagram_global_index += 1;
2650 let idx = diagram_global_index;
2651
2652 let data_partname = format!("/ppt/diagrams/data{}.xml", idx);
2654 let layout_partname = format!("/ppt/diagrams/layout{}.xml", idx);
2655 let quick_style_partname = format!("/ppt/diagrams/quickStyles{}.xml", idx);
2656 let colors_partname = format!("/ppt/diagrams/colors{}.xml", idx);
2657
2658 let data_target_rel = format!("../diagrams/data{}.xml", idx);
2660 let layout_target_rel = format!("../diagrams/layout{}.xml", idx);
2661 let quick_style_target_rel = format!("../diagrams/quickStyles{}.xml", idx);
2662 let colors_target_rel = format!("../diagrams/colors{}.xml", idx);
2663
2664 extra_rels.add(Relationship::internal_str(
2666 diagram_entry.data_rid.clone(),
2667 RelType::DiagramData,
2668 data_target_rel,
2669 ))?;
2670 extra_rels.add(Relationship::internal_str(
2671 diagram_entry.layout_rid.clone(),
2672 RelType::DiagramLayout,
2673 layout_target_rel,
2674 ))?;
2675 extra_rels.add(Relationship::internal_str(
2676 diagram_entry.quick_style_rid.clone(),
2677 RelType::DiagramQuickStyle,
2678 quick_style_target_rel,
2679 ))?;
2680 extra_rels.add(Relationship::internal_str(
2681 diagram_entry.colors_rid.clone(),
2682 RelType::DiagramColors,
2683 colors_target_rel,
2684 ))?;
2685
2686 pkg.put_part(Part::new(
2688 PartName::from_unchecked(data_partname),
2689 ct::DIAGRAM_DATA,
2690 diagram_entry.data_xml.clone().into_bytes(),
2691 ));
2692 pkg.put_part(Part::new(
2693 PartName::from_unchecked(layout_partname),
2694 ct::DIAGRAM_LAYOUT,
2695 diagram_entry.layout_xml.clone().into_bytes(),
2696 ));
2697 pkg.put_part(Part::new(
2698 PartName::from_unchecked(quick_style_partname),
2699 ct::DIAGRAM_QUICK_STYLE,
2700 diagram_entry.quick_style_xml.clone().into_bytes(),
2701 ));
2702 pkg.put_part(Part::new(
2703 PartName::from_unchecked(colors_partname),
2704 ct::DIAGRAM_COLORS,
2705 diagram_entry.colors_xml.clone().into_bytes(),
2706 ));
2707 }
2708
2709 let extra_rels_xml = extra_rels.to_xml();
2710 let extra_rels_partname = rels_partname_for(partname.as_str());
2711 pkg.put_part(Part::new(
2712 PartName::from_unchecked(extra_rels_partname),
2713 ct::RELATIONSHIPS,
2714 extra_rels_xml.into_bytes(),
2715 ));
2716
2717 pkg.put_part(Part::new(partname, ct::SLIDE, sld.to_xml().into_bytes()));
2719
2720 pres_rels.add(Relationship::internal_str(
2722 rid.clone(),
2723 RelType::Slide,
2724 format!("slides/slide{}.xml", i + 1),
2725 ))?;
2726 pres_root.slide_ids.push(SlideIdEntry {
2728 id: entry.sld_id,
2729 rid: rid.clone(),
2730 });
2731 }
2732
2733 if !self.comment_authors.is_empty() {
2737 let authors_xml = self.comment_authors.to_xml();
2738 pkg.put_part(Part::new(
2739 new_part_name("/ppt/commentAuthors.xml"),
2740 ct::COMMENT_AUTHORS,
2741 authors_xml.into_bytes(),
2742 ));
2743 pres_rels.add(Relationship::internal_str(
2744 "rIdCommentAuthors",
2745 RelType::CommentAuthors,
2746 "commentAuthors.xml",
2747 ))?;
2748 }
2749
2750 let pres_xml = if let Some(ref mp) = self.modify_protection {
2753 inject_modify_verifier(&pres_root.to_xml(), &mp.to_xml_element())
2754 } else {
2755 pres_root.to_xml()
2756 };
2757 pkg.put_part(Part::new(
2758 new_part_name("/ppt/presentation.xml"),
2759 ct::PRESENTATION,
2760 pres_xml.into_bytes(),
2761 ));
2762 pkg.put_part(Part::new(
2764 new_part_name("/ppt/_rels/presentation.xml.rels"),
2765 ct::RELATIONSHIPS,
2766 pres_rels.to_xml().into_bytes(),
2767 ));
2768
2769 use std::collections::BTreeMap;
2772 let mut seen: BTreeMap<String, ()> = BTreeMap::new();
2773 for entry in self.slides.iter() {
2774 for m in &entry.sld.media_entries {
2775 if seen.insert(m.partname.as_str().to_string(), ()).is_none() {
2776 pkg.put_part(Part::new(
2777 m.partname.clone(),
2778 m.content_type.clone(),
2779 m.blob.clone(),
2780 ));
2781 }
2782 }
2783 }
2784
2785 let pres_props_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2788<p:presentationPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
2789 <p:showPr/>
2790</p:presentationPr>"#;
2791 pkg.put_part(Part::new(
2792 new_part_name("/ppt/presProps.xml"),
2793 "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml",
2794 pres_props_xml.as_bytes().to_vec(),
2795 ));
2796
2797 let view_props_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2800<p:viewProps xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
2801 <p:normalViewPr><p:restoredLeft sz="15620"/><p:restoredTop sz="94660"/></p:normalViewPr>
2802</p:viewProps>"#;
2803 pkg.put_part(Part::new(
2804 new_part_name("/ppt/viewProps.xml"),
2805 "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml",
2806 view_props_xml.as_bytes().to_vec(),
2807 ));
2808
2809 let table_styles_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2812<a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" defStyleStyle=""/>
2813"#;
2814 pkg.put_part(Part::new(
2815 new_part_name("/ppt/tableStyles.xml"),
2816 "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml",
2817 table_styles_xml.as_bytes().to_vec(),
2818 ));
2819
2820 Ok(pkg)
2821 }
2822
2823 fn ensure_default_master_and_layout(&mut self) -> crate::Result<()> {
2833 if self.slide_masters.is_empty() {
2834 self.slide_masters.items.push(SlideMasterRef {
2835 idx: 0,
2836 partname: "/ppt/slideMasters/slideMaster1.xml".to_string(),
2837 rid: "rIdMaster1".to_string(),
2838 oxml: Rc::new(std::cell::RefCell::new(OxmlSldMaster::default())),
2839 });
2840 }
2841 if self.slide_layouts.is_empty() {
2842 self.slide_layouts.items.push(SlideLayoutRef {
2843 idx: 0,
2844 partname: "/ppt/slideLayouts/slideLayout1.xml".to_string(),
2845 rid: "rIdLayout1".to_string(),
2846 oxml: Rc::new(std::cell::RefCell::new(OxmlSldLayout::default())),
2847 });
2848 }
2849 Ok(())
2850 }
2851}
2852
2853fn resolve_relative_partname(parent_partname: &str, rel_target: &str) -> String {
2866 if rel_target.starts_with('/') {
2868 return rel_target.to_string();
2869 }
2870 let parent_dir = match parent_partname.rfind('/') {
2872 Some(i) => &parent_partname[..i],
2873 None => "",
2874 };
2875 let mut stack: Vec<&str> = Vec::new();
2877 let combined = if parent_dir.is_empty() {
2878 format!("/{}", rel_target)
2879 } else {
2880 format!("{}/{}", parent_dir, rel_target)
2881 };
2882 for seg in combined.split('/') {
2883 match seg {
2884 "" | "." => continue,
2885 ".." => {
2886 stack.pop();
2887 }
2888 _ => stack.push(seg),
2889 }
2890 }
2891 let mut out = String::from("/");
2892 out.push_str(&stack.join("/"));
2893 if out == "/" && !stack.is_empty() {
2894 }
2896 out
2897}
2898
2899fn inject_modify_verifier(pres_xml: &str, verifier_xml: &str) -> String {
2907 if pres_xml.contains("p:modifyVerifier") {
2908 return pres_xml.to_string();
2909 }
2910 if let Some(pos) = pres_xml.find("<p:extLst") {
2912 let mut out = String::with_capacity(pres_xml.len() + verifier_xml.len());
2913 out.push_str(&pres_xml[..pos]);
2914 out.push_str(verifier_xml);
2915 out.push_str(&pres_xml[pos..]);
2916 return out;
2917 }
2918 if let Some(pos) = pres_xml.rfind("</p:presentation>") {
2920 let mut out = String::with_capacity(pres_xml.len() + verifier_xml.len());
2921 out.push_str(&pres_xml[..pos]);
2922 out.push_str(verifier_xml);
2923 out.push_str(&pres_xml[pos..]);
2924 return out;
2925 }
2926 pres_xml.to_string()
2928}
2929
2930impl Default for Presentation {
2931 #[allow(clippy::expect_used)]
2939 fn default() -> Self {
2940 Self::new().expect("Presentation::new() is infallible in current implementation")
2941 }
2942}
2943
2944#[cfg(test)]
2945mod tests {
2946 use super::*;
2947 use crate::units::Inches;
2948
2949 #[test]
2951 fn resolve_relative_partname_examples() {
2952 assert_eq!(
2953 resolve_relative_partname("/ppt/slides/slide1.xml", "../notesSlides/notesSlide1.xml"),
2954 "/ppt/notesSlides/notesSlide1.xml"
2955 );
2956 assert_eq!(
2957 resolve_relative_partname("/ppt/slides/slide1.xml", "media/image1.png"),
2958 "/ppt/slides/media/image1.png"
2959 );
2960 assert_eq!(
2961 resolve_relative_partname("/ppt/slides/slide1.xml", "../media/image1.png"),
2962 "/ppt/media/image1.png"
2963 );
2964 assert_eq!(
2966 resolve_relative_partname("/ppt/slides/slide1.xml", "/ppt/notesSlides/n1.xml"),
2967 "/ppt/notesSlides/n1.xml"
2968 );
2969 assert_eq!(
2971 resolve_relative_partname("/ppt/slides/slide1.xml", "slide2.xml"),
2972 "/ppt/slides/slide2.xml"
2973 );
2974 }
2975
2976 #[test]
2984 fn roundtrip_presentation_loads_back() {
2985 let mut p = Presentation::new().expect("new ok");
2987 let counter = p.id_counter();
2988 let slide = p.slides_mut().add_slide(counter).expect("add slide");
2989 slide
2990 .shapes_mut()
2991 .add_textbox_with_text(
2992 Inches(1.0),
2993 Inches(1.0),
2994 Inches(4.0),
2995 Inches(1.0),
2996 "Hello roundtrip",
2997 )
2998 .expect("textbox");
2999 slide.set_notes_text(Some("speaker note line 1\nline 2"));
3000 p.set_slide_size(Emu(7_000_000), Emu(5_000_000));
3001
3002 let bytes = p.to_bytes().expect("to_bytes ok");
3004 assert!(!bytes.is_empty());
3005
3006 let p2 = Presentation::load_bytes(&bytes).expect("load_bytes ok");
3008
3009 assert_eq!(p2.slides().len(), 1);
3011 let s = p2.slides().get(0).expect("slide 0");
3012 assert_eq!(s.sld.layout_rid(), "rId1");
3015 let inner_shapes = &s.sld.inner.shapes;
3017 assert_eq!(inner_shapes.len(), 1);
3018 if let crate::oxml::SlideShape::Sp(sp) = &inner_shapes[0] {
3019 assert_eq!(sp.text.paragraphs.len(), 1);
3020 assert_eq!(sp.text.paragraphs[0].runs[0].text, "Hello roundtrip");
3021 } else {
3022 panic!("expected Sp");
3023 }
3024 let notes = s.sld.notes_text().expect("notes present");
3026 assert!(notes.contains("speaker note line 1"));
3027 assert_eq!(p2.slide_width().0, 7_000_000);
3029 assert_eq!(p2.slide_height().0, 5_000_000);
3030 }
3031
3032 #[test]
3034 fn load_then_resave_preserves_content() {
3035 let mut p = Presentation::new().expect("new ok");
3036 let counter = p.id_counter();
3037 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3038 slide
3039 .shapes_mut()
3040 .add_textbox_with_text(Inches(0.5), Inches(0.5), Inches(6.0), Inches(0.8), "stable")
3041 .expect("textbox");
3042 let bytes = p.to_bytes().expect("to_bytes");
3043
3044 let p2 = Presentation::load_bytes(&bytes).expect("load 1");
3046 let bytes2 = p2.to_bytes().expect("to_bytes 2");
3047 let p3 = Presentation::load_bytes(&bytes2).expect("load 2");
3048
3049 assert_eq!(p3.slides().len(), 1);
3050 let s = p3.slides().get(0).expect("slide 0");
3051 let inner_shapes = &s.sld.inner.shapes;
3052 if let crate::oxml::SlideShape::Sp(sp) = &inner_shapes[0] {
3053 assert_eq!(sp.text.paragraphs[0].runs[0].text, "stable");
3054 } else {
3055 panic!("expected Sp");
3056 }
3057 }
3058
3059 #[test]
3066 fn notes_roundtrip_preserves_partname_and_rels_target() {
3067 let mut p = Presentation::new().expect("new ok");
3069 let counter = p.id_counter();
3070 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3071 slide
3072 .shapes_mut()
3073 .add_textbox_with_text(
3074 Inches(0.5),
3075 Inches(0.5),
3076 Inches(6.0),
3077 Inches(0.8),
3078 "roundtrip notes test",
3079 )
3080 .expect("textbox");
3081 slide.set_notes_text(Some("first notes\nline 2"));
3082 let bytes1 = p.to_bytes().expect("to_bytes 1");
3083
3084 let mut p2 = Presentation::load_bytes(&bytes1).expect("load 1");
3086 p2.slides_mut()
3087 .get_mut(0)
3088 .expect("slide 0")
3089 .sld
3090 .set_notes_text(Some("updated notes"));
3091 let bytes2 = p2.to_bytes().expect("to_bytes 2");
3092
3093 let p3 = Presentation::load_bytes(&bytes2).expect("load 2");
3095 let s = p3.slides().get(0).expect("slide 0");
3096 let notes_text = s.sld.notes_text().expect("notes present");
3097 assert!(notes_text.contains("updated notes"));
3098
3099 let cursor = std::io::Cursor::new(bytes2);
3101 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3102 let mut nrels_xml = String::new();
3103 zip.by_name("ppt/notesSlides/_rels/notesSlide1.xml.rels")
3104 .expect("notesSlide1.xml.rels exists")
3105 .read_to_string(&mut nrels_xml)
3106 .expect("read notes rels");
3107 assert!(
3109 nrels_xml.contains("../slides/slide1.xml"),
3110 "notesSlide1.xml.rels missing slide rels, got: {nrels_xml}"
3111 );
3112 }
3113
3114 #[test]
3118 fn new_slide_with_notes_allocates_fresh_partname() {
3119 let mut p = Presentation::new().expect("new ok");
3120 let counter = p.id_counter();
3121 let s = p.slides_mut().add_slide(counter).expect("add slide");
3122 s.set_notes_text(Some("brand new"));
3123 let bytes = p.to_bytes().expect("to_bytes");
3124
3125 let p2 = Presentation::load_bytes(&bytes).expect("load");
3126 let s2 = p2.slides().get(0).expect("slide 0");
3127 let notes = s2.sld.notes_text().expect("notes present");
3128 assert!(notes.contains("brand new"));
3129
3130 let cursor = std::io::Cursor::new(bytes);
3132 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3133 assert!(zip.by_name("ppt/notesSlides/notesSlide1.xml").is_ok());
3134 }
3135
3136 #[test]
3141 fn notes_roundtrip_multiple_slides() {
3142 let mut p = Presentation::new().expect("new ok");
3143 let counter = p.id_counter();
3144 for i in 0..3 {
3147 let s = p
3148 .slides_mut()
3149 .add_slide(counter.clone())
3150 .expect("add slide");
3151 if i != 1 {
3152 s.set_notes_text(Some(&format!("note for slide {}", i + 1)));
3153 }
3154 }
3155 let bytes1 = p.to_bytes().expect("to_bytes 1");
3156
3157 let mut p2 = Presentation::load_bytes(&bytes1).expect("load");
3159 {
3161 let sm = p2.slides_mut();
3162 sm.get_mut(0).unwrap().sld.set_notes_text(Some("edited 1"));
3163 sm.get_mut(2).unwrap().sld.set_notes_text(Some("edited 3"));
3165 }
3166 let bytes2 = p2.to_bytes().expect("to_bytes 2");
3167
3168 let p3 = Presentation::load_bytes(&bytes2).expect("load 2");
3170 assert_eq!(p3.slides().len(), 3);
3171 let s0 = p3.slides().get(0).expect("s0");
3172 let s1 = p3.slides().get(1).expect("s1");
3173 let s2 = p3.slides().get(2).expect("s2");
3174 assert_eq!(s0.sld.notes_text().as_deref(), Some("edited 1"));
3175 assert_eq!(s1.sld.notes_text(), None);
3176 assert_eq!(s2.sld.notes_text().as_deref(), Some("edited 3"));
3177
3178 let cursor = std::io::Cursor::new(bytes2);
3181 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3182 assert!(zip.by_name("ppt/notesSlides/notesSlide1.xml").is_ok());
3183 assert!(zip.by_name("ppt/notesSlides/notesSlide2.xml").is_ok());
3184 assert!(zip
3185 .by_name("ppt/notesSlides/_rels/notesSlide1.xml.rels")
3186 .is_ok());
3187 assert!(zip
3188 .by_name("ppt/notesSlides/_rels/notesSlide2.xml.rels")
3189 .is_ok());
3190 }
3191
3192 #[test]
3207 fn layout_rid_is_overwritten_to_rid1_on_save() {
3208 let mut p = Presentation::new().expect("new ok");
3210 let counter = p.id_counter();
3211 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3212 slide
3213 .shapes_mut()
3214 .add_textbox_with_text(
3215 Inches(0.5),
3216 Inches(0.5),
3217 Inches(4.0),
3218 Inches(0.8),
3219 "layout_rid reset test",
3220 )
3221 .expect("textbox");
3222 slide.set_layout_rid("rIdLayout999".to_string());
3224 let bytes1 = p.to_bytes().expect("to_bytes 1");
3225
3226 let p2 = Presentation::load_bytes(&bytes1).expect("load");
3228 let s = p2.slides().get(0).expect("slide 0");
3229 assert_eq!(
3231 s.sld.layout_rid(),
3232 "rId1",
3233 "to_opc_package 应把 layout_rid 重置为 rId1(与 slide rels 的固定 rId 对应)"
3234 );
3235
3236 let cursor = std::io::Cursor::new(bytes1);
3238 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3239 let mut srels_xml = String::new();
3240 zip.by_name("ppt/slides/_rels/slide1.xml.rels")
3241 .expect("slide1.xml.rels exists")
3242 .read_to_string(&mut srels_xml)
3243 .expect("read slide rels");
3244 assert!(
3245 srels_xml.contains("Id=\"rId1\""),
3246 "slide1.xml.rels 中 layout 关系应使用 rId1,实际:{srels_xml}"
3247 );
3248 }
3249
3250 #[test]
3254 fn custom_properties_set_get_remove() {
3255 let mut props = CustomProperties::new();
3256 assert!(props.is_empty());
3257 assert_eq!(props.len(), 0);
3258
3259 props.set("Project", CustomPropertyValue::Text("Demo".to_string()));
3260 props.set("Version", CustomPropertyValue::Int(42));
3261 props.set("Active", CustomPropertyValue::Bool(true));
3262 assert_eq!(props.len(), 3);
3263
3264 match props.get("Project") {
3266 Some(CustomPropertyValue::Text(s)) => assert_eq!(s, "Demo"),
3267 other => panic!("期望 Text,得到 {:?}", other),
3268 }
3269 match props.get("Version") {
3270 Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 42),
3271 other => panic!("期望 Int,得到 {:?}", other),
3272 }
3273 assert!(props.get("NonExistent").is_none());
3274
3275 props.set("Version", CustomPropertyValue::Int(100));
3277 match props.get("Version") {
3278 Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 100),
3279 other => panic!("期望 Int(100),得到 {:?}", other),
3280 }
3281 assert_eq!(props.len(), 3, "覆盖不应增加条目数");
3282
3283 let removed = props.remove("Active");
3285 assert!(matches!(removed, Some(CustomPropertyValue::Bool(true))));
3286 assert_eq!(props.len(), 2);
3287 assert!(props.get("Active").is_none());
3288 }
3289
3290 #[test]
3292 fn custom_properties_to_xml() {
3293 let mut props = CustomProperties::new();
3294 props.set(
3295 "Text",
3296 CustomPropertyValue::Text("Hello & World".to_string()),
3297 );
3298 props.set("Int", CustomPropertyValue::Int(42));
3299 props.set("Float", CustomPropertyValue::Float(3.15));
3301 props.set("Bool", CustomPropertyValue::Bool(true));
3302
3303 let xml = props.to_xml();
3304 assert!(xml.contains("<Properties"));
3305 assert!(xml.contains("custom-properties"));
3306 assert!(xml.contains("Hello & World"));
3308 assert!(xml.contains("pid=\"2\""));
3310 assert!(xml.contains("pid=\"3\""));
3311 assert!(xml.contains("<vt:lpwstr>Hello & World</vt:lpwstr>"));
3313 assert!(xml.contains("<vt:i4>42</vt:i4>"));
3314 assert!(xml.contains("<vt:r8>3.15</vt:r8>"));
3315 assert!(xml.contains("<vt:bool>true</vt:bool>"));
3316 assert!(xml.contains("fmtid=\"{D5CDD505-2E9C-101B-9397-08002B2CF9AE}\""));
3318 }
3319
3320 #[test]
3322 fn custom_properties_from_xml() {
3323 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3324<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
3325 xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
3326 <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="2" name="Text">
3327 <vt:lpwstr>Value</vt:lpwstr>
3328 </property>
3329 <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="3" name="Int">
3330 <vt:i4>42</vt:i4>
3331 </property>
3332 <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="4" name="Bool">
3333 <vt:bool>false</vt:bool>
3334 </property>
3335 <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="5" name="Float">
3336 <vt:r8>3.15</vt:r8>
3337 </property>
3338</Properties>"#;
3339 let props = CustomProperties::from_xml(xml);
3340 assert_eq!(props.len(), 4);
3341 match props.get("Text") {
3342 Some(CustomPropertyValue::Text(s)) => assert_eq!(s, "Value"),
3343 other => panic!("期望 Text,得到 {:?}", other),
3344 }
3345 match props.get("Int") {
3346 Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 42),
3347 other => panic!("期望 Int,得到 {:?}", other),
3348 }
3349 match props.get("Bool") {
3350 Some(CustomPropertyValue::Bool(b)) => assert!(!*b),
3351 other => panic!("期望 Bool(false),得到 {:?}", other),
3352 }
3353 match props.get("Float") {
3354 Some(CustomPropertyValue::Float(f)) => assert!((f - 3.15).abs() < 1e-6),
3355 other => panic!("期望 Float,得到 {:?}", other),
3356 }
3357 }
3358
3359 #[test]
3361 fn custom_properties_round_trip() {
3362 let mut props = CustomProperties::new();
3363 props.set("Key1", CustomPropertyValue::Text("Value1".to_string()));
3364 props.set("Key2", CustomPropertyValue::Int(123));
3365 props.set("Key3", CustomPropertyValue::Bool(true));
3366
3367 let xml = props.to_xml();
3368 let parsed = CustomProperties::from_xml(&xml);
3369 assert_eq!(parsed.len(), 3);
3370 assert_eq!(props.get("Key1"), parsed.get("Key1"));
3371 assert_eq!(props.get("Key2"), parsed.get("Key2"));
3372 assert_eq!(props.get("Key3"), parsed.get("Key3"));
3373 }
3374
3375 #[test]
3377 fn custom_properties_empty_not_serialized() {
3378 let p = Presentation::new().expect("new ok");
3379 let bytes = p.to_bytes().expect("to_bytes");
3380 let cursor = std::io::Cursor::new(bytes);
3381 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3382 assert!(zip.by_name("docProps/custom.xml").is_err());
3384 }
3385
3386 #[test]
3388 fn custom_properties_serialized_to_pptx() {
3389 let mut p = Presentation::new().expect("new ok");
3390 p.custom_properties_mut()
3391 .set("Project", CustomPropertyValue::Text("Test".to_string()));
3392 p.custom_properties_mut()
3393 .set("Version", CustomPropertyValue::Int(1));
3394
3395 let bytes = p.to_bytes().expect("to_bytes");
3396 let cursor = std::io::Cursor::new(bytes);
3397 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3398
3399 let mut custom_xml = String::new();
3401 zip.by_name("docProps/custom.xml")
3402 .expect("custom.xml 应存在")
3403 .read_to_string(&mut custom_xml)
3404 .expect("read custom.xml");
3405 assert!(custom_xml.contains("name=\"Project\""));
3406 assert!(custom_xml.contains("<vt:lpwstr>Test</vt:lpwstr>"));
3407 assert!(custom_xml.contains("name=\"Version\""));
3408 assert!(custom_xml.contains("<vt:i4>1</vt:i4>"));
3409
3410 let mut rels_xml = String::new();
3412 zip.by_name("_rels/.rels")
3413 .expect(".rels 应存在")
3414 .read_to_string(&mut rels_xml)
3415 .expect("read .rels");
3416 assert!(rels_xml.contains("custom-properties"), "rels: {}", rels_xml);
3417 assert!(
3418 rels_xml.contains("docProps/custom.xml"),
3419 "rels: {}",
3420 rels_xml
3421 );
3422 }
3423
3424 #[test]
3426 fn custom_properties_round_trip_through_pptx() {
3427 let mut p = Presentation::new().expect("new ok");
3428 p.custom_properties_mut()
3429 .set("Author", CustomPropertyValue::Text("TestUser".to_string()));
3430 p.custom_properties_mut()
3431 .set("Count", CustomPropertyValue::Int(99));
3432
3433 let bytes = p.to_bytes().expect("to_bytes");
3434 let p2 = Presentation::load_bytes(&bytes).expect("load");
3435
3436 assert_eq!(p2.custom_properties().len(), 2);
3437 match p2.custom_properties().get("Author") {
3438 Some(CustomPropertyValue::Text(s)) => assert_eq!(s, "TestUser"),
3439 other => panic!("期望 Text,得到 {:?}", other),
3440 }
3441 match p2.custom_properties().get("Count") {
3442 Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 99),
3443 other => panic!("期望 Int,得到 {:?}", other),
3444 }
3445 }
3446
3447 #[test]
3451 fn slide_add_comment_basic() {
3452 let mut p = Presentation::new().expect("new ok");
3453 let counter = p.id_counter();
3454 let author_id = p.comment_authors_mut().get_or_insert_id("张三", "ZS");
3455 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3456 let idx = slide.add_comment(
3457 author_id,
3458 crate::Emu::new(914400),
3459 crate::Emu::new(914400),
3460 "测试评论",
3461 );
3462 assert_eq!(idx, 1);
3463 let comments = slide.comments().expect("comments should exist");
3464 assert_eq!(comments.len(), 1);
3465 assert_eq!(comments.comments[0].text, "测试评论");
3466 assert_eq!(comments.comments[0].author_id, author_id);
3467 assert_eq!(comments.comments[0].idx, 1);
3468 assert_eq!(comments.comments[0].pos_x, 914400);
3469 assert_eq!(comments.comments[0].pos_y, 914400);
3470 }
3471
3472 #[test]
3474 fn slide_add_comment_multiple_idx_increments() {
3475 let mut p = Presentation::new().expect("new ok");
3476 let counter = p.id_counter();
3477 let author_id = p.comment_authors_mut().get_or_insert_id("作者", "ZZ");
3478 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3479 let idx1 = slide.add_comment(author_id, 0, 0, "第一条");
3480 let idx2 = slide.add_comment(author_id, 100, 100, "第二条");
3481 let idx3 = slide.add_comment(author_id, 200, 200, "第三条");
3482 assert_eq!(idx1, 1);
3483 assert_eq!(idx2, 2);
3484 assert_eq!(idx3, 3);
3485 assert_eq!(slide.comments().unwrap().len(), 3);
3486 }
3487
3488 #[test]
3490 fn slide_clear_comments() {
3491 let mut p = Presentation::new().expect("new ok");
3492 let counter = p.id_counter();
3493 let author_id = p.comment_authors_mut().get_or_insert_id("A", "A");
3494 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3495 slide.add_comment(author_id, 0, 0, "评论");
3496 assert!(slide.comments().is_some());
3497 slide.clear_comments();
3498 assert!(slide.comments().is_none());
3499 }
3500
3501 #[test]
3503 fn comments_serialized_to_pptx() {
3504 let mut p = Presentation::new().expect("new ok");
3505 let counter = p.id_counter();
3506 let author_id = p.comment_authors_mut().get_or_insert_id("张三", "ZS");
3507 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3508 slide.add_comment(
3509 author_id,
3510 crate::Emu::new(914400),
3511 crate::Emu::new(914400),
3512 "PPTX 评论",
3513 );
3514
3515 let bytes = p.to_bytes().expect("to_bytes");
3516 let cursor = std::io::Cursor::new(bytes);
3517 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3518
3519 let mut cxml = String::new();
3521 zip.by_name("ppt/comments/comment1.xml")
3522 .expect("comment1.xml 应存在")
3523 .read_to_string(&mut cxml)
3524 .expect("read comment1.xml");
3525 assert!(cxml.contains("<p:cmLst"), "comment xml: {}", cxml);
3526 assert!(cxml.contains("PPTX 评论"), "comment text: {}", cxml);
3527 assert!(cxml.contains("authorId=\""), "comment authorId: {}", cxml);
3528
3529 let mut axml = String::new();
3531 zip.by_name("ppt/commentAuthors.xml")
3532 .expect("commentAuthors.xml 应存在")
3533 .read_to_string(&mut axml)
3534 .expect("read commentAuthors.xml");
3535 assert!(axml.contains("<p:cmAuthorLst"), "authors xml: {}", axml);
3536 assert!(axml.contains("张三"), "author name: {}", axml);
3537
3538 let mut srels = String::new();
3540 zip.by_name("ppt/slides/_rels/slide1.xml.rels")
3541 .expect("slide1.xml.rels exists")
3542 .read_to_string(&mut srels)
3543 .expect("read slide rels");
3544 assert!(
3545 srels.contains("comments"),
3546 "slide rels should contain comments: {}",
3547 srels
3548 );
3549 assert!(
3550 srels.contains("../comments/comment1.xml"),
3551 "slide rels target: {}",
3552 srels
3553 );
3554
3555 let mut prels = String::new();
3557 zip.by_name("ppt/_rels/presentation.xml.rels")
3558 .expect("pres rels exists")
3559 .read_to_string(&mut prels)
3560 .expect("read pres rels");
3561 assert!(prels.contains("commentAuthors"), "pres rels: {}", prels);
3562 }
3563
3564 #[test]
3566 fn comments_round_trip_through_pptx() {
3567 let mut p = Presentation::new().expect("new ok");
3568 let counter = p.id_counter();
3569 let author_id = p.comment_authors_mut().get_or_insert_id("李四", "LS");
3570 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3571 slide.add_comment(
3572 author_id,
3573 crate::Emu::new(100),
3574 crate::Emu::new(200),
3575 "往返评论",
3576 );
3577
3578 let bytes = p.to_bytes().expect("to_bytes");
3579 let p2 = Presentation::load_bytes(&bytes).expect("load");
3580
3581 let s2 = p2.slides().get(0).expect("slide 0");
3583 let comments = s2.sld.comments().expect("comments should exist");
3584 assert_eq!(comments.len(), 1);
3585 let c = &comments.comments[0];
3586 assert_eq!(c.text, "往返评论");
3587 assert_eq!(c.pos_x, 100);
3588 assert_eq!(c.pos_y, 200);
3589 assert_eq!(c.idx, 1);
3590
3591 let authors = p2.comment_authors();
3593 assert_eq!(authors.len(), 1);
3594 assert_eq!(authors.authors[0].name, "李四");
3595 assert_eq!(authors.authors[0].initials, "LS");
3596 assert_eq!(authors.authors[0].id, c.author_id);
3597 }
3598
3599 #[test]
3601 fn no_comments_no_part() {
3602 let mut p = Presentation::new().expect("new ok");
3603 let counter = p.id_counter();
3604 p.slides_mut().add_slide(counter).expect("add slide");
3605
3606 let bytes = p.to_bytes().expect("to_bytes");
3607 let cursor = std::io::Cursor::new(bytes);
3608 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3609 assert!(zip.by_name("ppt/comments/comment1.xml").is_err());
3611 assert!(zip.by_name("ppt/commentAuthors.xml").is_err());
3613 }
3614
3615 #[test]
3617 fn comments_multiple_slides_partname_stable() {
3618 let mut p = Presentation::new().expect("new ok");
3619 let counter = p.id_counter();
3620 let aid = p.comment_authors_mut().get_or_insert_id("A", "A");
3621 let s1 = p.slides_mut().add_slide(counter.clone()).expect("slide 1");
3622 s1.add_comment(aid, 0, 0, "slide1 评论");
3623
3624 let s2 = p.slides_mut().add_slide(counter).expect("slide 2");
3625 s2.add_comment(aid, 0, 0, "slide2 评论");
3626
3627 let bytes = p.to_bytes().expect("to_bytes");
3628 let p2 = Presentation::load_bytes(&bytes).expect("load");
3629
3630 let s1b = p2.slides().get(0).expect("slide 0");
3632 let s2b = p2.slides().get(1).expect("slide 1");
3633 assert_eq!(s1b.sld.comments().unwrap().comments[0].text, "slide1 评论");
3634 assert_eq!(s2b.sld.comments().unwrap().comments[0].text, "slide2 评论");
3635
3636 let bytes2 = p2.to_bytes().expect("to_bytes 2");
3638 let cursor = std::io::Cursor::new(bytes2);
3639 let mut zip = zip::ZipArchive::new(cursor).expect("zip 2");
3640 assert!(zip.by_name("ppt/comments/comment1.xml").is_ok());
3641 assert!(zip.by_name("ppt/comments/comment2.xml").is_ok());
3642 }
3643
3644 #[test]
3648 fn slide_allocate_diagram_rids_increments() {
3649 let mut p = Presentation::new().expect("new ok");
3650 let counter = p.id_counter();
3651 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3652
3653 let (d1, l1, q1, c1) = slide.allocate_diagram_rids();
3654 assert_eq!(d1, "rIdDgmData1");
3655 assert_eq!(l1, "rIdDgmLayout1");
3656 assert_eq!(q1, "rIdDgmQs1");
3657 assert_eq!(c1, "rIdDgmColors1");
3658
3659 let (d2, l2, q2, c2) = slide.allocate_diagram_rids();
3660 assert_eq!(d2, "rIdDgmData2");
3661 assert_eq!(l2, "rIdDgmLayout2");
3662 assert_eq!(q2, "rIdDgmQs2");
3663 assert_eq!(c2, "rIdDgmColors2");
3664 }
3665
3666 #[test]
3668 fn slide_next_diagram_index_increments() {
3669 let mut p = Presentation::new().expect("new ok");
3670 let counter = p.id_counter();
3671 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3672
3673 assert_eq!(slide.next_diagram_index(), 1);
3674 assert_eq!(slide.next_diagram_index(), 2);
3675 assert_eq!(slide.next_diagram_index(), 3);
3676 }
3677
3678 #[test]
3680 fn slide_register_diagram_stores_entry() {
3681 let mut p = Presentation::new().expect("new ok");
3682 let counter = p.id_counter();
3683 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3684
3685 let entry = DiagramEntry {
3686 data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
3687 layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
3688 quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
3689 colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
3690 data_xml: "<dgm:dataModel/>".to_string(),
3691 layout_xml: "<dgm:layoutDef/>".to_string(),
3692 quick_style_xml: "<dgm:styleData/>".to_string(),
3693 colors_xml: "<dgm:colorsDef/>".to_string(),
3694 data_rid: "rIdDgmData1".to_string(),
3695 layout_rid: "rIdDgmLayout1".to_string(),
3696 quick_style_rid: "rIdDgmQs1".to_string(),
3697 colors_rid: "rIdDgmColors1".to_string(),
3698 };
3699 slide.register_diagram(entry);
3700
3701 assert_eq!(slide.diagram_entries.len(), 1);
3703 assert_eq!(
3704 slide.diagram_entries[0].data_partname.as_str(),
3705 "/ppt/diagrams/data1.xml"
3706 );
3707 assert_eq!(slide.diagram_entries[0].data_xml, "<dgm:dataModel/>");
3708 }
3709
3710 #[test]
3713 fn diagram_parts_written_to_zip_after_register() {
3714 let mut p = Presentation::new().expect("new ok");
3715 let counter = p.id_counter();
3716 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3717
3718 let entry = DiagramEntry {
3719 data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
3720 layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
3721 quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
3722 colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
3723 data_xml: "<?xml version=\"1.0\"?><dgm:dataModel xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
3724 layout_xml: "<?xml version=\"1.0\"?><dgm:layoutDef xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
3725 quick_style_xml: "<?xml version=\"1.0\"?><dgm:styleData xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
3726 colors_xml: "<?xml version=\"1.0\"?><dgm:colorsDef xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
3727 data_rid: "rIdDgmData1".to_string(),
3728 layout_rid: "rIdDgmLayout1".to_string(),
3729 quick_style_rid: "rIdDgmQs1".to_string(),
3730 colors_rid: "rIdDgmColors1".to_string(),
3731 };
3732 slide.register_diagram(entry);
3733
3734 let bytes = p.to_bytes().expect("to_bytes");
3735 let cursor = std::io::Cursor::new(bytes);
3736 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3737
3738 assert!(
3740 zip.by_name("ppt/diagrams/data1.xml").is_ok(),
3741 "data1.xml missing"
3742 );
3743 assert!(
3744 zip.by_name("ppt/diagrams/layout1.xml").is_ok(),
3745 "layout1.xml missing"
3746 );
3747 assert!(
3748 zip.by_name("ppt/diagrams/quickStyles1.xml").is_ok(),
3749 "quickStyles1.xml missing"
3750 );
3751 assert!(
3752 zip.by_name("ppt/diagrams/colors1.xml").is_ok(),
3753 "colors1.xml missing"
3754 );
3755
3756 let mut srels_xml = String::new();
3758 zip.by_name("ppt/slides/_rels/slide1.xml.rels")
3759 .expect("slide1.xml.rels exists")
3760 .read_to_string(&mut srels_xml)
3761 .expect("read slide rels");
3762 assert!(
3763 srels_xml.contains("/diagramData"),
3764 "slide1.xml.rels 缺 diagramData 关系,实际:{srels_xml}"
3765 );
3766 assert!(
3767 srels_xml.contains("/diagramLayout"),
3768 "slide1.xml.rels 缺 diagramLayout 关系,实际:{srels_xml}"
3769 );
3770 assert!(
3771 srels_xml.contains("/diagramQuickStyle"),
3772 "slide1.xml.rels 缺 diagramQuickStyle 关系,实际:{srels_xml}"
3773 );
3774 assert!(
3775 srels_xml.contains("/diagramColors"),
3776 "slide1.xml.rels 缺 diagramColors 关系,实际:{srels_xml}"
3777 );
3778 assert!(srels_xml.contains("rIdDgmData1"), "缺 rIdDgmData1");
3780 assert!(srels_xml.contains("rIdDgmLayout1"), "缺 rIdDgmLayout1");
3781 assert!(srels_xml.contains("rIdDgmQs1"), "缺 rIdDgmQs1");
3782 assert!(srels_xml.contains("rIdDgmColors1"), "缺 rIdDgmColors1");
3783 }
3784
3785 #[test]
3787 fn diagram_parts_global_index_across_slides() {
3788 let mut p = Presentation::new().expect("new ok");
3789 let counter = p.id_counter();
3790
3791 let s1 = p.slides_mut().add_slide(counter.clone()).expect("s1");
3793 let e1 = DiagramEntry {
3794 data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
3795 layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
3796 quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
3797 colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
3798 data_xml: "<dgm:dataModel/>".to_string(),
3799 layout_xml: "<dgm:layoutDef/>".to_string(),
3800 quick_style_xml: "<dgm:styleData/>".to_string(),
3801 colors_xml: "<dgm:colorsDef/>".to_string(),
3802 data_rid: "rIdDgmData1".to_string(),
3803 layout_rid: "rIdDgmLayout1".to_string(),
3804 quick_style_rid: "rIdDgmQs1".to_string(),
3805 colors_rid: "rIdDgmColors1".to_string(),
3806 };
3807 s1.register_diagram(e1);
3808
3809 let s2 = p.slides_mut().add_slide(counter).expect("s2");
3811 let e2 = DiagramEntry {
3812 data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
3813 layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
3814 quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
3815 colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
3816 data_xml: "<dgm:dataModel/>".to_string(),
3817 layout_xml: "<dgm:layoutDef/>".to_string(),
3818 quick_style_xml: "<dgm:styleData/>".to_string(),
3819 colors_xml: "<dgm:colorsDef/>".to_string(),
3820 data_rid: "rIdDgmData1".to_string(),
3821 layout_rid: "rIdDgmLayout1".to_string(),
3822 quick_style_rid: "rIdDgmQs1".to_string(),
3823 colors_rid: "rIdDgmColors1".to_string(),
3824 };
3825 s2.register_diagram(e2);
3826
3827 let bytes = p.to_bytes().expect("to_bytes");
3828 let cursor = std::io::Cursor::new(bytes);
3829 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3830
3831 assert!(zip.by_name("ppt/diagrams/data1.xml").is_ok());
3833 assert!(zip.by_name("ppt/diagrams/layout1.xml").is_ok());
3834 assert!(
3837 zip.by_name("ppt/diagrams/data2.xml").is_ok(),
3838 "data2.xml missing"
3839 );
3840 assert!(
3841 zip.by_name("ppt/diagrams/layout2.xml").is_ok(),
3842 "layout2.xml missing"
3843 );
3844 assert!(zip.by_name("ppt/diagrams/quickStyles2.xml").is_ok());
3845 assert!(zip.by_name("ppt/diagrams/colors2.xml").is_ok());
3846 }
3847}