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) themes: Vec<crate::oxml::theme::Theme>,
108 pub(crate) sld_master_ids: Vec<(u32, String)>,
113 pub(crate) notes_master_ids: Vec<(u32, String)>,
118 pub(crate) width: Emu,
120 pub(crate) height: Emu,
122 pub(crate) id_counter: Rc<Cell<u32>>,
124 pub(crate) core_properties: CoreProperties,
126 pub(crate) custom_properties: CustomProperties,
130 pub(crate) comment_authors: crate::oxml::comments::CommentAuthorList,
135 pub(crate) modify_protection: Option<crate::crypto::ModifyProtection>,
140 pub(crate) sections: crate::oxml::section::SectionList,
146}
147
148#[derive(Debug, Clone, Default)]
166pub struct CoreProperties {
167 pub title: Option<String>,
169 pub creator: Option<String>,
171 pub subject: Option<String>,
173 pub last_modified_by: Option<String>,
175 pub application: Option<String>,
177 pub created: Option<String>,
179 pub modified: Option<String>,
181 pub keywords: Option<String>,
183 pub category: Option<String>,
185 pub description: Option<String>,
187 pub revision: Option<String>,
189}
190
191#[derive(Clone, Debug, PartialEq)]
204pub enum CustomPropertyValue {
205 Text(String),
207 Int(i32),
209 Float(f64),
211 Bool(bool),
213 DateTime(String),
215}
216
217impl CustomPropertyValue {
218 fn vt_element(&self) -> &'static str {
220 match self {
221 CustomPropertyValue::Text(_) => "vt:lpwstr",
222 CustomPropertyValue::Int(_) => "vt:i4",
223 CustomPropertyValue::Float(_) => "vt:r8",
224 CustomPropertyValue::Bool(_) => "vt:bool",
225 CustomPropertyValue::DateTime(_) => "vt:filetime",
226 }
227 }
228
229 fn value_str(&self) -> String {
231 match self {
232 CustomPropertyValue::Text(s) => s.clone(),
233 CustomPropertyValue::Int(i) => i.to_string(),
234 CustomPropertyValue::Float(f) => f.to_string(),
235 CustomPropertyValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
236 CustomPropertyValue::DateTime(s) => s.clone(),
237 }
238 }
239}
240
241#[derive(Debug, Clone, Default)]
263pub struct CustomProperties {
264 entries: Vec<(String, CustomPropertyValue)>,
266}
267
268impl CustomProperties {
269 pub fn new() -> Self {
271 Self::default()
272 }
273
274 pub fn is_empty(&self) -> bool {
276 self.entries.is_empty()
277 }
278
279 pub fn len(&self) -> usize {
281 self.entries.len()
282 }
283
284 pub fn set(&mut self, name: impl Into<String>, value: CustomPropertyValue) {
290 let name = name.into();
291 for entry in &mut self.entries {
292 if entry.0 == name {
293 entry.1 = value;
294 return;
295 }
296 }
297 self.entries.push((name, value));
298 }
299
300 pub fn get(&self, name: &str) -> Option<&CustomPropertyValue> {
302 self.entries.iter().find(|(k, _)| k == name).map(|(_, v)| v)
303 }
304
305 pub fn remove(&mut self, name: &str) -> Option<CustomPropertyValue> {
309 let idx = self.entries.iter().position(|(k, _)| k == name)?;
310 Some(self.entries.remove(idx).1)
311 }
312
313 pub fn iter(&self) -> impl Iterator<Item = (&str, &CustomPropertyValue)> {
315 self.entries.iter().map(|(k, v)| (k.as_str(), v))
316 }
317
318 pub fn to_xml(&self) -> String {
333 if self.entries.is_empty() {
334 return String::new();
335 }
336 let mut out = String::with_capacity(256 * self.entries.len());
337 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
338 out.push_str("<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties\"");
339 out.push_str(
340 " xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\n",
341 );
342 for (i, (name, value)) in self.entries.iter().enumerate() {
343 let pid = (i + 2) as u32; let vt_elem = value.vt_element();
345 let val = xml_escape(&value.value_str());
346 out.push_str(&format!(
347 " <property fmtid=\"{{D5CDD505-2E9C-101B-9397-08002B2CF9AE}}\" pid=\"{}\" name=\"{}\">\n",
348 pid,
349 xml_escape(name)
350 ));
351 out.push_str(&format!(" <{}>{}</{}>\n", vt_elem, val, vt_elem));
352 out.push_str(" </property>\n");
353 }
354 out.push_str("</Properties>");
355 out
356 }
357
358 pub fn from_xml(xml: &str) -> Self {
366 use quick_xml::events::Event;
367 use quick_xml::Reader;
368
369 let mut props = CustomProperties::new();
370 let mut rd = Reader::from_str(xml);
371 rd.config_mut().trim_text(true);
372 let mut buf = Vec::new();
373 let mut cur_name: Option<String> = None;
375 let mut cur_vt: Option<String> = None;
377 let mut cur_text = String::new();
379
380 loop {
381 match rd.read_event_into(&mut buf) {
382 Ok(Event::Start(e)) => {
383 let name = e.name();
384 let qname = String::from_utf8_lossy(name.as_ref()).to_string();
385 let local = local_name(name.as_ref());
386 if local == b"property" {
387 for a in e.attributes().flatten() {
389 if a.key.as_ref() == b"name" {
390 cur_name = Some(
391 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
392 .unwrap_or_default()
393 .to_string(),
394 );
395 }
396 }
397 } else if cur_name.is_some() && qname.starts_with("vt:") {
398 cur_vt = Some(qname);
400 cur_text.clear();
401 }
402 }
403 Ok(Event::Empty(e)) => {
404 let name = e.name();
405 let local = local_name(name.as_ref());
406 if local == b"property" {
407 cur_name = None;
409 }
410 }
411 Ok(Event::Text(t)) => {
412 if cur_vt.is_some() {
413 cur_text.push_str(&t.decode().unwrap_or_default());
414 }
415 }
416 Ok(Event::End(e)) => {
417 let name = e.name();
418 let qname = String::from_utf8_lossy(name.as_ref()).to_string();
419 let local = local_name(name.as_ref());
420 if qname.starts_with("vt:") && cur_vt.is_some() {
421 if let (Some(prop_name), Some(vt)) = (cur_name.take(), cur_vt.take()) {
424 let value = parse_vt_value(&vt, &cur_text);
425 if let Some(v) = value {
426 props.set(prop_name, v);
427 }
428 }
429 cur_vt = None;
430 cur_text.clear();
431 } else if local == b"property" {
432 cur_name = None;
433 }
434 }
435 Ok(Event::Eof) => break,
436 _ => {}
437 }
438 buf.clear();
439 }
440 props
441 }
442}
443
444fn xml_escape(s: &str) -> String {
446 let mut out = String::with_capacity(s.len());
447 for c in s.chars() {
448 match c {
449 '&' => out.push_str("&"),
450 '<' => out.push_str("<"),
451 '>' => out.push_str(">"),
452 '"' => out.push_str("""),
453 '\'' => out.push_str("'"),
454 _ => out.push(c),
455 }
456 }
457 out
458}
459
460fn local_name(name: &[u8]) -> &[u8] {
462 match name.iter().position(|&b| b == b':') {
463 Some(i) => &name[i + 1..],
464 None => name,
465 }
466}
467
468fn parse_core_xml(xml: &str) -> CoreProperties {
483 use quick_xml::events::Event;
484 use quick_xml::Reader;
485
486 let mut cp = CoreProperties::default();
487 let mut rd = Reader::from_str(xml);
488 rd.config_mut().trim_text(true);
489 let mut buf = Vec::new();
490 let mut cur_field: Option<&'static str> = None;
492 let mut cur_text = String::new();
493
494 loop {
495 match rd.read_event_into(&mut buf) {
496 Ok(Event::Start(e)) => {
497 let name = e.name();
498 let local = local_name(name.as_ref());
499 cur_field = match local {
501 b"title" => Some("title"),
502 b"creator" => Some("creator"),
503 b"subject" => Some("subject"),
504 b"lastModifiedBy" => Some("last_modified_by"),
505 b"keywords" => Some("keywords"),
506 b"category" => Some("category"),
507 b"description" => Some("description"),
508 b"revision" => Some("revision"),
509 b"created" => Some("created"),
510 b"modified" => Some("modified"),
511 _ => None,
512 };
513 if cur_field.is_some() {
514 cur_text.clear();
515 }
516 }
517 Ok(Event::Empty(_)) => {
518 cur_field = None;
520 }
521 Ok(Event::Text(t)) => {
522 if cur_field.is_some() {
523 cur_text.push_str(&t.decode().unwrap_or_default());
525 }
526 }
527 Ok(Event::End(_)) => {
528 if let Some(field) = cur_field.take() {
529 let value = cur_text.trim().to_string();
530 if !value.is_empty() {
531 match field {
532 "title" => cp.title = Some(value),
533 "creator" => cp.creator = Some(value),
534 "subject" => cp.subject = Some(value),
535 "last_modified_by" => cp.last_modified_by = Some(value),
536 "keywords" => cp.keywords = Some(value),
537 "category" => cp.category = Some(value),
538 "description" => cp.description = Some(value),
539 "revision" => cp.revision = Some(value),
540 "created" => cp.created = Some(value),
541 "modified" => cp.modified = Some(value),
542 _ => {}
543 }
544 }
545 cur_text.clear();
546 }
547 }
548 Ok(Event::Eof) => break,
549 _ => {}
550 }
551 buf.clear();
552 }
553 cp
554}
555
556fn parse_app_xml_application(xml: &str) -> Option<String> {
568 use quick_xml::events::Event;
569 use quick_xml::Reader;
570
571 let mut rd = Reader::from_str(xml);
572 rd.config_mut().trim_text(true);
573 let mut buf = Vec::new();
574 let mut in_application = false;
575 let mut text = String::new();
576
577 loop {
578 match rd.read_event_into(&mut buf) {
579 Ok(Event::Start(e)) => {
580 if local_name(e.name().as_ref()) == b"Application" {
581 in_application = true;
582 text.clear();
583 }
584 }
585 Ok(Event::End(_)) => {
586 if in_application {
587 let v = text.trim().to_string();
588 if !v.is_empty() {
589 return Some(v);
590 }
591 in_application = false;
592 }
593 }
594 Ok(Event::Text(t)) => {
595 if in_application {
596 text.push_str(&t.decode().unwrap_or_default());
597 }
598 }
599 Ok(Event::Eof) => break,
600 _ => {}
601 }
602 buf.clear();
603 }
604 None
605}
606
607fn parse_vt_value(vt_elem: &str, text: &str) -> Option<CustomPropertyValue> {
609 let trimmed = text.trim();
610 match vt_elem {
611 "vt:lpwstr" | "vt:bstr" => Some(CustomPropertyValue::Text(trimmed.to_string())),
612 "vt:i4" | "vt:int" | "vt:i2" | "vt:ui1" | "vt:ui2" | "vt:ui4" => {
613 trimmed.parse::<i32>().ok().map(CustomPropertyValue::Int)
614 }
615 "vt:i8" | "vt:ui8" => {
616 trimmed.parse::<i64>().ok().and_then(|v| {
618 if v >= i32::MIN as i64 && v <= i32::MAX as i64 {
619 Some(CustomPropertyValue::Int(v as i32))
620 } else {
621 None
622 }
623 })
624 }
625 "vt:r4" | "vt:r8" | "vt:decimal" => {
626 trimmed.parse::<f64>().ok().map(CustomPropertyValue::Float)
627 }
628 "vt:bool" => {
629 let v = trimmed.eq_ignore_ascii_case("true") || trimmed == "1";
630 if v || trimmed.eq_ignore_ascii_case("false") || trimmed == "0" {
631 Some(CustomPropertyValue::Bool(v))
632 } else {
633 None
634 }
635 }
636 "vt:filetime" | "vt:date" => Some(CustomPropertyValue::DateTime(trimmed.to_string())),
637 _ => None,
638 }
639}
640
641#[derive(Debug, Clone)]
647pub struct MediaEntry {
648 pub partname: PartName,
650 pub content_type: String,
652 pub blob: Vec<u8>,
654 pub rid: String,
656}
657
658#[derive(Debug, Clone)]
670pub struct ChartEntry {
671 pub partname: PartName,
673 pub chart: crate::oxml::chart::Chart,
675 pub rid: String,
677 pub xlsx_blob: Option<Vec<u8>>,
688}
689
690#[derive(Debug, Clone)]
709pub struct OleEntry {
710 pub partname: PartName,
712 pub blob: Vec<u8>,
714 pub rid: String,
716}
717
718#[derive(Debug, Clone)]
740pub struct VideoEntry {
741 pub partname: PartName,
743 pub blob: Vec<u8>,
745 pub rid: String,
751}
752
753#[derive(Debug, Clone)]
762pub struct AudioEntry {
763 pub partname: PartName,
765 pub blob: Vec<u8>,
767 pub rid: String,
769}
770
771#[derive(Debug, Clone)]
798pub struct DiagramEntry {
799 pub data_partname: PartName,
801 pub layout_partname: PartName,
803 pub quick_style_partname: PartName,
805 pub colors_partname: PartName,
807 pub data_xml: String,
809 pub layout_xml: String,
811 pub quick_style_xml: String,
813 pub colors_xml: String,
815 pub data_rid: String,
819 pub layout_rid: String,
823 pub quick_style_rid: String,
827 pub colors_rid: String,
831}
832
833impl DiagramEntry {
834 pub fn data_model(&self) -> crate::Result<crate::oxml::diagram::DataModel> {
855 crate::oxml::diagram::DataModel::parse_from_xml(&self.data_xml)
856 }
857
858 pub fn layout_def(&self) -> crate::Result<crate::oxml::diagram::LayoutDef> {
862 crate::oxml::diagram::LayoutDef::parse_from_xml(&self.layout_xml)
863 }
864
865 pub fn quick_style_def(&self) -> crate::Result<crate::oxml::diagram::QuickStyleDef> {
869 crate::oxml::diagram::QuickStyleDef::parse_from_xml(&self.quick_style_xml)
870 }
871
872 pub fn colors_def(&self) -> crate::Result<crate::oxml::diagram::ColorsDef> {
876 crate::oxml::diagram::ColorsDef::parse_from_xml(&self.colors_xml)
877 }
878
879 pub fn set_data_model(&mut self, data_model: &crate::oxml::diagram::DataModel) {
898 self.data_xml = data_model.to_xml();
899 }
900
901 pub fn set_point_text(
914 &mut self,
915 model_id: u32,
916 new_text: impl Into<String>,
917 ) -> crate::Result<bool> {
918 let mut dm = self.data_model()?;
919 let ok = dm.set_point_text(model_id, new_text);
920 if ok {
921 self.data_xml = dm.to_xml();
922 }
923 Ok(ok)
924 }
925}
926
927impl Presentation {
928 pub fn new() -> crate::Result<Self> {
939 let id_counter = Rc::new(Cell::new(2));
940 let mut pres = Presentation {
941 slides: Slides::new(),
942 slide_layouts: SlideLayouts::new(),
943 slide_masters: SlideMasters::new(),
944 notes_masters: NotesMasters::new(),
945 theme: crate::oxml::theme::Theme::default(),
946 themes: Vec::new(),
947 sld_master_ids: Vec::new(),
948 notes_master_ids: Vec::new(),
949 width: Emu(DEFAULT_WIDTH_EMU),
950 height: Emu(DEFAULT_HEIGHT_EMU),
951 id_counter,
952 core_properties: CoreProperties {
953 application: Some("pptx-rs".to_string()),
954 ..Default::default()
955 },
956 custom_properties: CustomProperties::default(),
957 comment_authors: crate::oxml::comments::CommentAuthorList::default(),
958 modify_protection: None,
959 sections: crate::oxml::section::SectionList::default(),
960 };
961 pres.ensure_default_master_and_layout()?;
962 Ok(pres)
963 }
964
965 pub fn open(path: impl AsRef<Path>) -> crate::Result<Self> {
975 Self::load(path.as_ref())
976 }
977
978 pub fn load(path: &Path) -> crate::Result<Self> {
980 let pkg = OpcPackage::load(path)?;
981 Self::from_opc(pkg)
982 }
983
984 pub fn load_bytes(bytes: &[u8]) -> crate::Result<Self> {
996 let mut pkg = OpcPackage::new();
997 let cursor = std::io::Cursor::new(bytes);
998 let mut zip = zip::ZipArchive::new(cursor)?;
999 let mut ct_xml = String::new();
1001 zip.by_name("[Content_Types].xml")?
1002 .read_to_string(&mut ct_xml)?;
1003 pkg.content_types = crate::opc::package::parse_content_types_public(&ct_xml)?;
1004 for i in 0..zip.len() {
1006 let mut e = zip.by_index(i)?;
1007 let name = e.name().to_string();
1008 if name == "[Content_Types].xml" || e.is_dir() {
1009 continue;
1010 }
1011 let mut blob = Vec::with_capacity(e.size() as usize);
1012 e.read_to_end(&mut blob)?;
1013 let ct_str: String = if name.ends_with(".rels") {
1014 crate::opc::package::ct::RELATIONSHIPS.to_string()
1015 } else {
1016 let partname = format!("/{}", name);
1021 crate::opc::package::derive_content_type(&pkg.content_types, &partname)
1022 };
1023 let partname = format!("/{}", name);
1024 let p = Part::new(PartName::from_unchecked(partname), ct_str, blob);
1025 pkg.parts.insert(p.partname.as_str().to_string(), p);
1026 }
1027 Self::from_opc(pkg)
1028 }
1029
1030 fn from_opc(pkg: OpcPackage) -> crate::Result<Self> {
1054 type DiagramTask = (
1058 String,
1059 String,
1060 String,
1061 String,
1062 String,
1063 String,
1064 String,
1065 String,
1066 );
1067 let id_counter = Rc::new(Cell::new(2));
1069 let mut pres = Presentation {
1070 slides: Slides::new(),
1071 slide_layouts: SlideLayouts::new(),
1072 slide_masters: SlideMasters::new(),
1073 notes_masters: NotesMasters::new(),
1074 theme: crate::oxml::theme::Theme::default(),
1075 themes: Vec::new(),
1076 sld_master_ids: Vec::new(),
1077 notes_master_ids: Vec::new(),
1078 width: Emu(DEFAULT_WIDTH_EMU),
1079 height: Emu(DEFAULT_HEIGHT_EMU),
1080 id_counter: id_counter.clone(),
1081 core_properties: CoreProperties::default(),
1082 custom_properties: CustomProperties::default(),
1083 comment_authors: crate::oxml::comments::CommentAuthorList::default(),
1084 modify_protection: None,
1085 sections: crate::oxml::section::SectionList::default(),
1086 };
1087 pres.ensure_default_master_and_layout()?;
1088
1089 let pres_part = pkg
1091 .get_part("/ppt/presentation.xml")
1092 .ok_or_else(|| crate::Error::opc("presentation.xml not found in package"))?;
1093 let pres_xml = String::from_utf8_lossy(&pres_part.blob).into_owned();
1094 let (sld_id_list, sld_w, sld_h, sld_master_id_list, sections, notes_master_id_list) =
1095 crate::oxml::parse_sld::parse_pres_root(&pres_xml)?;
1096 pres.sld_master_ids = sld_master_id_list;
1098 pres.sections = sections;
1100 pres.notes_master_ids = notes_master_id_list;
1102 if let (Some(w), Some(h)) = (sld_w, sld_h) {
1103 pres.width = w;
1104 pres.height = h;
1105 }
1106
1107 let pres_rels_part = pkg
1109 .get_part("/ppt/_rels/presentation.xml.rels")
1110 .ok_or_else(|| crate::Error::opc("presentation.xml.rels not found"))?;
1111 let pres_rels_xml = String::from_utf8_lossy(&pres_rels_part.blob).into_owned();
1112 let pres_rels = Relationships::from_xml(&pres_rels_xml)?;
1113
1114 let mut parsed_masters: Vec<(String, String, String, String, OxmlSldMaster)> = Vec::new();
1122 let mut parsed_layouts: Vec<(String, String, String, OxmlSldLayout)> = Vec::new();
1123 for r in pres_rels.iter() {
1124 if !matches!(r.reltype, RelType::SlideMaster) {
1125 continue;
1126 }
1127 let master_partname =
1128 resolve_relative_partname("/ppt/presentation.xml", r.target.as_str());
1129 let master_xml = match pkg.get_part(master_partname.as_str()) {
1130 Some(p) => String::from_utf8_lossy(&p.blob).into_owned(),
1131 None => continue,
1132 };
1133 let master = match crate::oxml::parse_sld::parse_sld_master(&master_xml) {
1134 Ok(m) => m,
1135 Err(_e) => continue,
1136 };
1137 let mut current_theme_partname = String::new();
1139 let mut current_theme_rid = String::new();
1140 let master_rels_path = rels_partname_for(master_partname.as_str());
1142 if let Some(mrp) = pkg.get_part(master_rels_path.as_str()) {
1143 let mrp_xml = String::from_utf8_lossy(&mrp.blob).into_owned();
1144 if let Ok(master_rels) = Relationships::from_xml(&mrp_xml) {
1145 for mr in master_rels.iter() {
1146 if matches!(mr.reltype, RelType::SlideLayout) {
1147 let layout_partname = resolve_relative_partname(
1148 master_partname.as_str(),
1149 mr.target.as_str(),
1150 );
1151 let layout_xml = match pkg.get_part(layout_partname.as_str()) {
1152 Some(p) => String::from_utf8_lossy(&p.blob).into_owned(),
1153 None => continue,
1154 };
1155 if let Ok(layout) =
1156 crate::oxml::parse_sld::parse_sld_layout(&layout_xml)
1157 {
1158 parsed_layouts.push((
1160 mr.id.clone(),
1161 layout_partname,
1162 master_partname.clone(),
1163 layout,
1164 ));
1165 }
1166 }
1167 if matches!(mr.reltype, RelType::Theme) {
1169 current_theme_partname = resolve_relative_partname(
1170 master_partname.as_str(),
1171 mr.target.as_str(),
1172 );
1173 current_theme_rid = mr.id.clone();
1174 if let Some(tp) = pkg.get_part(current_theme_partname.as_str()) {
1175 let theme_xml = String::from_utf8_lossy(&tp.blob).into_owned();
1176 if let Ok(mut theme) =
1178 crate::oxml::parse_sld::parse_theme(&theme_xml)
1179 {
1180 theme.partname = current_theme_partname.clone();
1181 pres.themes.push(theme.clone());
1182 if pres.theme.name.is_empty()
1185 && pres.theme.color_scheme.dk1.is_none()
1186 {
1187 pres.theme = theme;
1188 }
1189 }
1190 }
1191 }
1192 }
1193 }
1194 }
1195 parsed_masters.push((
1196 r.id.clone(),
1197 master_partname,
1198 current_theme_partname,
1199 current_theme_rid,
1200 master,
1201 ));
1202 }
1203 if !parsed_masters.is_empty() {
1205 pres.slide_masters.items.clear();
1206 pres.slide_layouts.items.clear();
1207 for (rid, partname, theme_partname, theme_rid, oxml) in parsed_masters {
1208 let idx = pres.slide_masters.items.len();
1209 pres.slide_masters.items.push(SlideMasterRef {
1210 idx,
1211 partname,
1212 rid,
1213 oxml: Rc::new(std::cell::RefCell::new(oxml)),
1214 theme_partname,
1215 theme_rid,
1216 });
1217 }
1218 for (rid, partname, master_partname, oxml) in parsed_layouts {
1219 let idx = pres.slide_layouts.items.len();
1220 pres.slide_layouts.items.push(SlideLayoutRef {
1221 idx,
1222 partname,
1223 rid,
1224 oxml: Rc::new(std::cell::RefCell::new(oxml)),
1225 master_partname,
1226 });
1227 }
1228 }
1229
1230 for r in pres_rels.iter() {
1234 if !matches!(r.reltype, RelType::NotesMaster) {
1235 continue;
1236 }
1237 let nm_partname = resolve_relative_partname("/ppt/presentation.xml", r.target.as_str());
1238 let nm_xml = match pkg.get_part(nm_partname.as_str()) {
1239 Some(p) => String::from_utf8_lossy(&p.blob).into_owned(),
1240 None => continue,
1241 };
1242 let nm = match crate::oxml::parse_sld::parse_notes_master(&nm_xml) {
1243 Ok(m) => m,
1244 Err(_e) => continue,
1245 };
1246 let idx = pres.notes_masters.items.len();
1247 pres.notes_masters.items.push(NotesMasterRef {
1248 idx,
1249 partname: nm_partname,
1250 rid: r.id.clone(),
1251 oxml: Rc::new(std::cell::RefCell::new(nm)),
1252 });
1253 }
1254
1255 let mut max_id_seen: u32 = 2;
1258 for (sld_id, rid) in &sld_id_list {
1259 let rel = match pres_rels.get(rid) {
1261 Some(r) => r,
1262 None => {
1263 continue;
1265 }
1266 };
1267 let slide_partname =
1270 resolve_relative_partname("/ppt/presentation.xml", rel.target.as_str());
1271 let slide_part = match pkg.get_part(slide_partname.as_str()) {
1273 Some(p) => p,
1274 None => continue,
1275 };
1276 let slide_xml = String::from_utf8_lossy(&slide_part.blob).into_owned();
1277 let mut sld = match crate::oxml::parse_sld::parse_sld(&slide_xml) {
1278 Ok(s) => s,
1279 Err(_e) => {
1280 continue;
1282 }
1283 };
1284 sld.id = *sld_id;
1286
1287 let rels_path = rels_partname_for(slide_partname.as_str());
1289 let mut layout_rid = String::from("rId1");
1290 let mut captured_layout_partname: Option<String> = None;
1292 let mut notes_rid: Option<String> = None;
1293 let mut notes_target_raw: Option<String> = None;
1296 let mut comments_rid: Option<String> = None;
1298 let mut comments_target_raw: Option<String> = None;
1299 let mut image_rels: Vec<(String, String)> = Vec::new(); let mut diagram_rel_map: std::collections::HashMap<String, String> =
1305 std::collections::HashMap::new();
1306 let mut chart_rel_map: std::collections::HashMap<String, String> =
1309 std::collections::HashMap::new();
1310 if let Some(rels_part) = pkg.get_part(rels_path.as_str()) {
1311 let rels_xml = String::from_utf8_lossy(&rels_part.blob).into_owned();
1312 if let Ok(slide_rels) = Relationships::from_xml(&rels_xml) {
1313 for r in slide_rels.iter() {
1314 match r.reltype {
1315 RelType::SlideLayout => {
1316 layout_rid = r.id.clone();
1317 let abs_layout = resolve_relative_partname(
1319 slide_partname.as_str(),
1320 r.target.as_str(),
1321 );
1322 captured_layout_partname = Some(abs_layout);
1323 }
1324 RelType::NotesSlide => {
1325 notes_rid = Some(r.id.clone());
1326 notes_target_raw = Some(r.target.as_str().to_string());
1328 }
1329 RelType::Comments => {
1330 comments_rid = Some(r.id.clone());
1331 comments_target_raw = Some(r.target.as_str().to_string());
1332 }
1333 RelType::Image => {
1334 let rel_target = r.target.as_str();
1335 let abs =
1336 resolve_relative_partname(slide_partname.as_str(), rel_target);
1337 image_rels.push((r.id.clone(), abs));
1338 }
1339 RelType::DiagramData
1340 | RelType::DiagramLayout
1341 | RelType::DiagramQuickStyle
1342 | RelType::DiagramColors => {
1343 let rel_target = r.target.as_str();
1345 let abs =
1346 resolve_relative_partname(slide_partname.as_str(), rel_target);
1347 diagram_rel_map.insert(r.id.clone(), abs);
1348 }
1349 RelType::Chart => {
1350 let rel_target = r.target.as_str();
1354 let abs =
1355 resolve_relative_partname(slide_partname.as_str(), rel_target);
1356 chart_rel_map.insert(r.id.clone(), abs);
1357 }
1358 _ => {}
1359 }
1360 }
1361 }
1362 }
1363 sld.set_layout_rid(layout_rid.clone());
1365
1366 let mut parsed_notes_partname: Option<String> = None;
1372 let mut parsed_notes_rels_target: Option<String> = None;
1373 if let Some(raw_target) = ¬es_target_raw {
1374 let notes_partname =
1377 resolve_relative_partname(slide_partname.as_str(), raw_target.as_str());
1378 parsed_notes_partname = Some(notes_partname.clone());
1379 let notes_rels_path = rels_partname_for(¬es_partname);
1383 if let Some(nrp) = pkg.get_part(notes_rels_path.as_str()) {
1384 let nrp_xml = String::from_utf8_lossy(&nrp.blob).into_owned();
1385 if let Ok(nr) = Relationships::from_xml(&nrp_xml) {
1386 for r in nr.iter() {
1387 if matches!(r.reltype, RelType::Slide) {
1388 parsed_notes_rels_target = Some(r.target.as_str().to_string());
1389 break;
1390 }
1391 }
1392 }
1393 }
1394 if let Some(np) = pkg.get_part(notes_partname.as_str()) {
1395 let notes_xml = String::from_utf8_lossy(&np.blob).into_owned();
1396 if let Ok(tb) = crate::oxml::parse_sld::parse_notes(¬es_xml) {
1397 sld.notes = Some(tb);
1398 }
1399 }
1400 }
1401
1402 let mut parsed_comments_partname: Option<String> = None;
1405 let mut parsed_comments_lst: Option<crate::oxml::comments::CommentList> = None;
1406 if let Some(raw_target) = &comments_target_raw {
1407 let comments_partname =
1410 resolve_relative_partname(slide_partname.as_str(), raw_target.as_str());
1411 parsed_comments_partname = Some(comments_partname.clone());
1412 if let Some(cp) = pkg.get_part(comments_partname.as_str()) {
1414 let comments_xml = String::from_utf8_lossy(&cp.blob).into_owned();
1415 if let Ok(lst) = crate::oxml::parse_sld::parse_comments(&comments_xml) {
1416 parsed_comments_lst = Some(lst);
1417 }
1418 }
1419 }
1420
1421 for shape in &sld.shapes {
1425 let sp_id = match shape {
1426 crate::oxml::SlideShape::Sp(sp) => sp.id,
1427 crate::oxml::SlideShape::Pic(pic) => pic.id,
1428 crate::oxml::SlideShape::CxnSp(c) => c.id,
1429 crate::oxml::SlideShape::Group(g) => g.id,
1430 crate::oxml::SlideShape::GraphicFrame(gf) => gf.id,
1431 };
1432 if sp_id > max_id_seen {
1433 max_id_seen = sp_id;
1434 }
1435 }
1436
1437 let mut slide = Slide::from_sld(sld, id_counter.clone(), layout_rid);
1438 if let Some(lp) = captured_layout_partname {
1441 slide.set_layout_partname(lp);
1442 }
1443 if let Some(nrid) = notes_rid {
1444 slide.set_notes_rid(nrid);
1445 }
1446 if let Some(np) = parsed_notes_partname {
1450 slide.set_notes_partname(np);
1451 }
1452 if let Some(nrt) = parsed_notes_rels_target {
1453 slide.set_notes_slide_rel_target(nrt);
1454 }
1455 if let Some(crid) = comments_rid {
1459 slide.set_comments_rid(crid);
1460 }
1461 if let Some(cp) = parsed_comments_partname {
1462 slide.set_comments_partname(cp);
1463 }
1464 if let Some(clst) = parsed_comments_lst {
1465 slide.set_comments(Some(clst));
1466 }
1467
1468 for (rid, target_partname) in &image_rels {
1472 if let Some(media_part) = pkg.get_part(target_partname.as_str()) {
1473 let partname = crate::opc::part::new_part_name(target_partname.as_str());
1474 slide.register_media(MediaEntry {
1475 partname,
1476 content_type: media_part.content_type.clone(),
1477 blob: media_part.blob.clone(),
1478 rid: rid.clone(),
1479 });
1480 }
1481 }
1482
1483 let mut diagram_tasks: Vec<DiagramTask> = Vec::new();
1498 for shape in &slide.inner.shapes {
1499 if let crate::oxml::SlideShape::GraphicFrame(gf) = shape {
1500 if let crate::oxml::shape::Graphic::SmartArt(smart_ref) = &gf.graphic {
1501 let (Some(dm_rid), Some(lo_rid), Some(qs_rid), Some(cs_rid)) = (
1502 &smart_ref.dm_rid,
1503 &smart_ref.lo_rid,
1504 &smart_ref.qs_rid,
1505 &smart_ref.cs_rid,
1506 ) else {
1507 continue;
1509 };
1510 let (
1511 Some(data_partname),
1512 Some(layout_partname),
1513 Some(quick_style_partname),
1514 Some(colors_partname),
1515 ) = (
1516 diagram_rel_map.get(dm_rid),
1517 diagram_rel_map.get(lo_rid),
1518 diagram_rel_map.get(qs_rid),
1519 diagram_rel_map.get(cs_rid),
1520 )
1521 else {
1522 continue;
1524 };
1525 diagram_tasks.push((
1526 dm_rid.clone(),
1527 lo_rid.clone(),
1528 qs_rid.clone(),
1529 cs_rid.clone(),
1530 data_partname.clone(),
1531 layout_partname.clone(),
1532 quick_style_partname.clone(),
1533 colors_partname.clone(),
1534 ));
1535 }
1536 }
1537 }
1538 for (
1539 dm_rid,
1540 lo_rid,
1541 qs_rid,
1542 cs_rid,
1543 data_partname,
1544 layout_partname,
1545 quick_style_partname,
1546 colors_partname,
1547 ) in diagram_tasks
1548 {
1549 let data_xml = pkg
1551 .get_part(data_partname.as_str())
1552 .map(|p| String::from_utf8_lossy(&p.blob).into_owned())
1553 .unwrap_or_default();
1554 let layout_xml = pkg
1555 .get_part(layout_partname.as_str())
1556 .map(|p| String::from_utf8_lossy(&p.blob).into_owned())
1557 .unwrap_or_default();
1558 let quick_style_xml = pkg
1559 .get_part(quick_style_partname.as_str())
1560 .map(|p| String::from_utf8_lossy(&p.blob).into_owned())
1561 .unwrap_or_default();
1562 let colors_xml = pkg
1563 .get_part(colors_partname.as_str())
1564 .map(|p| String::from_utf8_lossy(&p.blob).into_owned())
1565 .unwrap_or_default();
1566 let entry = DiagramEntry {
1567 data_partname: crate::opc::part::new_part_name(data_partname.as_str()),
1568 layout_partname: crate::opc::part::new_part_name(layout_partname.as_str()),
1569 quick_style_partname: crate::opc::part::new_part_name(
1570 quick_style_partname.as_str(),
1571 ),
1572 colors_partname: crate::opc::part::new_part_name(colors_partname.as_str()),
1573 data_xml,
1574 layout_xml,
1575 quick_style_xml,
1576 colors_xml,
1577 data_rid: dm_rid,
1578 layout_rid: lo_rid,
1579 quick_style_rid: qs_rid,
1580 colors_rid: cs_rid,
1581 };
1582 slide.register_diagram(entry);
1583 }
1584
1585 let mut chart_tasks: Vec<(String, String)> = Vec::new(); for shape in &slide.inner.shapes {
1599 if let crate::oxml::SlideShape::GraphicFrame(gf) = shape {
1600 if let crate::oxml::shape::Graphic::Chart(chart) = &gf.graphic {
1601 let rid = chart.rid.as_str();
1602 if rid.is_empty() {
1603 continue;
1604 }
1605 if let Some(chart_partname) = chart_rel_map.get(rid) {
1606 chart_tasks.push((rid.to_string(), chart_partname.clone()));
1607 }
1608 }
1609 }
1610 }
1611 for (rid, chart_partname) in chart_tasks {
1612 let Some(chart_part) = pkg.get_part(chart_partname.as_str()) else {
1613 continue;
1614 };
1615 let chart_xml = String::from_utf8_lossy(&chart_part.blob).into_owned();
1616 let Ok(mut parsed) = crate::oxml::chart::Chart::parse_from_xml(&chart_xml) else {
1617 continue;
1618 };
1619 parsed.rid = rid.clone();
1620 for s in slide.inner.shapes.iter_mut() {
1622 if let crate::oxml::SlideShape::GraphicFrame(gf2) = s {
1623 if let crate::oxml::shape::Graphic::Chart(c2) = &mut gf2.graphic {
1624 if c2.rid == rid {
1625 *c2 = parsed.clone();
1626 break;
1627 }
1628 }
1629 }
1630 }
1631 slide.register_chart(ChartEntry {
1634 partname: crate::opc::part::new_part_name(chart_partname.as_str()),
1635 chart: parsed,
1636 rid,
1637 xlsx_blob: None,
1638 });
1639 }
1640
1641 let entry = SlideEntry::new(slide, *sld_id, rid.clone(), slide_partname);
1642 pres.slides.push_entry(entry);
1643 }
1644 pres.id_counter.set(max_id_seen);
1646
1647 if let Some(custom_part) = pkg.get_part("/docProps/custom.xml") {
1649 let custom_xml = String::from_utf8_lossy(&custom_part.blob).into_owned();
1650 pres.custom_properties = CustomProperties::from_xml(&custom_xml);
1651 }
1652
1653 if let Some(authors_part) = pkg.get_part("/ppt/commentAuthors.xml") {
1655 let authors_xml = String::from_utf8_lossy(&authors_part.blob).into_owned();
1656 if let Ok(lst) = crate::oxml::parse_sld::parse_comment_authors(&authors_xml) {
1657 pres.comment_authors = lst;
1658 }
1659 }
1660
1661 if let Some(core_part) = pkg.get_part("/docProps/core.xml") {
1666 let core_xml = String::from_utf8_lossy(&core_part.blob).into_owned();
1667 pres.core_properties = parse_core_xml(&core_xml);
1668 }
1669 if let Some(app_part) = pkg.get_part("/docProps/app.xml") {
1670 let app_xml = String::from_utf8_lossy(&app_part.blob).into_owned();
1671 if let Some(app) = parse_app_xml_application(&app_xml) {
1672 pres.core_properties.application = Some(app);
1673 }
1674 }
1675
1676 Ok(pres)
1677 }
1678
1679 pub fn slides(&self) -> &Slides {
1681 &self.slides
1682 }
1683 pub fn slides_mut(&mut self) -> &mut Slides {
1685 &mut self.slides
1686 }
1687
1688 pub fn id_counter(&self) -> std::rc::Rc<std::cell::Cell<u32>> {
1692 self.id_counter.clone()
1693 }
1694
1695 pub fn slide_layouts(&self) -> &SlideLayouts {
1697 &self.slide_layouts
1698 }
1699 pub fn slide_layouts_mut(&mut self) -> &mut SlideLayouts {
1701 &mut self.slide_layouts
1702 }
1703
1704 pub fn layout_for_slide(&self, slide_idx: usize) -> Option<SlideLayoutRef> {
1728 let entry = self.slides.get(slide_idx)?;
1729 let layout_rid = entry.sld.layout_rid();
1730 self.slide_layouts
1731 .items
1732 .iter()
1733 .find(|l| l.rid == layout_rid)
1734 .cloned()
1735 }
1736
1737 pub fn slides_using_layout(&self, layout_rid: &str) -> Vec<usize> {
1747 self.slides
1748 .iter()
1749 .enumerate()
1750 .filter(|(_, entry)| entry.sld.layout_rid() == layout_rid)
1751 .map(|(i, _)| i)
1752 .collect()
1753 }
1754
1755 pub fn slide_masters(&self) -> &SlideMasters {
1757 &self.slide_masters
1758 }
1759 pub fn slide_masters_mut(&mut self) -> &mut SlideMasters {
1761 &mut self.slide_masters
1762 }
1763
1764 pub fn slide_width(&self) -> Emu {
1766 self.width
1767 }
1768 pub fn slide_height(&self) -> Emu {
1770 self.height
1771 }
1772
1773 pub fn set_slide_size(&mut self, width: Emu, height: Emu) {
1778 self.width = width;
1779 self.height = height;
1780 }
1781
1782 pub fn core_properties(&self) -> &CoreProperties {
1786 &self.core_properties
1787 }
1788
1789 pub fn core_properties_mut(&mut self) -> &mut CoreProperties {
1794 &mut self.core_properties
1795 }
1796
1797 pub fn custom_properties(&self) -> &CustomProperties {
1801 &self.custom_properties
1802 }
1803
1804 pub fn custom_properties_mut(&mut self) -> &mut CustomProperties {
1819 &mut self.custom_properties
1820 }
1821
1822 pub fn comment_authors(&self) -> &crate::oxml::comments::CommentAuthorList {
1826 &self.comment_authors
1827 }
1828
1829 pub fn comment_authors_mut(&mut self) -> &mut crate::oxml::comments::CommentAuthorList {
1842 &mut self.comment_authors
1843 }
1844
1845 pub fn sections(&self) -> &crate::oxml::section::SectionList {
1854 &self.sections
1855 }
1856
1857 pub fn sections_mut(&mut self) -> &mut crate::oxml::section::SectionList {
1877 &mut self.sections
1878 }
1879
1880 pub fn notes_masters(&self) -> &NotesMasters {
1900 &self.notes_masters
1901 }
1902
1903 pub fn notes_masters_mut(&mut self) -> &mut NotesMasters {
1907 &mut self.notes_masters
1908 }
1909
1910 pub fn notes_master(&self) -> Option<&NotesMasterRef> {
1914 self.notes_masters.first()
1915 }
1916
1917 pub fn set_metadata(
1935 &mut self,
1936 title: Option<&str>,
1937 creator: Option<&str>,
1938 subject: Option<&str>,
1939 keywords: Option<&str>,
1940 ) {
1941 if let Some(t) = title {
1942 self.core_properties.title = Some(t.to_string());
1943 }
1944 if let Some(c) = creator {
1945 self.core_properties.creator = Some(c.to_string());
1946 }
1947 if let Some(s) = subject {
1948 self.core_properties.subject = Some(s.to_string());
1949 }
1950 if let Some(k) = keywords {
1951 self.core_properties.keywords = Some(k.to_string());
1952 }
1953 }
1954
1955 pub fn num_slides(&self) -> usize {
1959 self.slides.len()
1960 }
1961
1962 #[allow(clippy::field_reassign_with_default)]
1984 pub fn add_watermark(
1985 &mut self,
1986 text: &str,
1987 font_size_pt: Option<f64>,
1988 color: Option<crate::units::RGBColor>,
1989 rotation_deg: Option<i32>,
1990 alpha: Option<i32>,
1991 font_name: Option<&str>,
1992 ) -> crate::Result<()> {
1993 let fs = font_size_pt.unwrap_or(36.0);
1994 let clr = color.unwrap_or(crate::units::RGBColor(0xC0, 0xC0, 0xC0));
1995 let rot = rotation_deg.unwrap_or(-30);
1996 let alpha_val = alpha.unwrap_or(30_000);
1997 let font = font_name.unwrap_or("Calibri");
1998 let rot_emu = rot * 60000; for i in 0..self.slides.len() {
2001 let slide = self
2003 .slides
2004 .get_mut(i)
2005 .ok_or(crate::Error::IndexOutOfRange(i))?;
2006 let sld = &mut slide.sld;
2007 let id = sld.next_shape_id();
2008
2009 let mut sp = crate::oxml::shape::Sp::default();
2010 sp.id = id;
2011 sp.name = format!("Watermark {}", i + 1);
2012 sp.c_nv_sp_pr_tx_box = true;
2013
2014 let cx = self.width.0;
2016 let cy = self.height.0;
2017 sp.properties.xfrm.off_x = Some(Emu(cx / 4));
2018 sp.properties.xfrm.off_y = Some(Emu(cy / 4));
2019 sp.properties.xfrm.ext_cx = Some(Emu(cx / 2));
2020 sp.properties.xfrm.ext_cy = Some(Emu(cy / 2));
2021 sp.properties.xfrm.rot = Some(rot_emu);
2022 sp.properties.geometry = Some(crate::oxml::sppr::Geometry::preset(
2023 crate::oxml::simpletypes::PresetGeometry::Rectangle,
2024 ));
2025 sp.properties.fill = crate::oxml::sppr::Fill::None;
2027 sp.properties.line = None;
2028
2029 let mut tb = crate::oxml::txbody::TextBody::new();
2031 let mut para = crate::oxml::txbody::Paragraph::default();
2032 let mut run = crate::oxml::txbody::Run::default();
2033 run.text = text.to_string();
2034 run.properties.size = Some(crate::units::Pt(fs));
2035 run.properties.color = crate::oxml::color::Color::RGB(clr);
2036 run.properties.bold = true;
2037 run.properties.alpha = Some(alpha_val);
2038 run.properties.latin_font = Some(font.to_string());
2040 run.properties.eastasia_font = Some("宋体".to_string());
2041 para.runs.push(run);
2042 para.properties.alignment = Some(crate::oxml::simpletypes::Alignment::Center);
2044 tb.paragraphs.push(para);
2045 sp.text = tb;
2046
2047 sld.inner.shapes.push(crate::oxml::SlideShape::Sp(sp));
2048 }
2049 Ok(())
2050 }
2051
2052 #[allow(clippy::field_reassign_with_default)]
2077 #[allow(clippy::too_many_arguments)]
2078 pub fn add_image_watermark(
2079 &mut self,
2080 image_bytes: &[u8],
2081 ext: &str,
2082 alpha: Option<i32>,
2083 left: Option<Emu>,
2084 top: Option<Emu>,
2085 width: Option<Emu>,
2086 height: Option<Emu>,
2087 ) -> crate::Result<()> {
2088 if image_bytes.is_empty() {
2089 return Err(crate::Error::encryption("image bytes must not be empty"));
2090 }
2091 let alpha_val = alpha.unwrap_or(30_000);
2092 let cx = self.width.0;
2093 let cy = self.height.0;
2094 let wm_left = left.unwrap_or(Emu(cx / 4));
2095 let wm_top = top.unwrap_or(Emu(cy / 4));
2096 let wm_width = width.unwrap_or(Emu(cx / 2));
2097 let wm_height = height.unwrap_or(Emu(cy / 2));
2098
2099 let ext_norm = if ext.starts_with('.') {
2101 ext.to_string()
2102 } else {
2103 format!(".{}", ext)
2104 };
2105 let global_media_idx = self
2107 .slides
2108 .get(0)
2109 .map(|s| s.sld.next_media_index())
2110 .unwrap_or(1);
2111 let shared_partname = crate::opc::part::new_part_name(
2112 format!("/ppt/media/image{}{}", global_media_idx, ext_norm).as_str(),
2113 );
2114 let ct = crate::shape::picture::content_type_for(&ext_norm);
2115
2116 for i in 0..self.slides.len() {
2117 let slide = self
2119 .slides
2120 .get_mut(i)
2121 .ok_or(crate::Error::IndexOutOfRange(i))?;
2122 let id = slide.sld.next_shape_id();
2123 let rid = slide.sld.allocate_image_rid();
2124
2125 let mut pic = crate::oxml::shape::Pic::default();
2127 pic.id = id;
2128 pic.name = format!("WatermarkImage {}", i + 1);
2129 pic.rid = rid.clone();
2130 pic.alpha = Some(alpha_val);
2131 pic.fill_mode = crate::oxml::sppr::BlipFillMode::Stretch;
2132 pic.properties.xfrm.off_x = Some(wm_left);
2133 pic.properties.xfrm.off_y = Some(wm_top);
2134 pic.properties.xfrm.ext_cx = Some(wm_width);
2135 pic.properties.xfrm.ext_cy = Some(wm_height);
2136 pic.properties.fill = crate::oxml::sppr::Fill::None;
2138 pic.properties.line = None;
2139
2140 slide.sld.register_media(MediaEntry {
2142 partname: shared_partname.clone(),
2143 content_type: ct.to_string(),
2144 blob: image_bytes.to_vec(),
2145 rid,
2146 });
2147
2148 slide
2149 .sld
2150 .inner
2151 .shapes
2152 .push(crate::oxml::SlideShape::Pic(pic));
2153 }
2154 Ok(())
2155 }
2156
2157 pub fn set_write_protection(&mut self, password: &str) -> crate::Result<()> {
2171 if password.is_empty() {
2172 return Err(crate::Error::encryption("password must not be empty"));
2173 }
2174 let salt = crate::crypto::generate_random_bytes(crate::crypto::SALT_LEN);
2175 self.modify_protection = Some(crate::crypto::ModifyProtection::from_password(
2176 password,
2177 &salt,
2178 crate::crypto::MODIFY_SPIN_COUNT,
2179 ));
2180 Ok(())
2181 }
2182
2183 pub fn verify_write_protection(&self, password: &str) -> crate::Result<bool> {
2193 match &self.modify_protection {
2194 Some(mp) => Ok(mp.verify_password(password)),
2195 None => Ok(false),
2196 }
2197 }
2198
2199 pub fn remove_write_protection(&mut self) {
2201 self.modify_protection = None;
2202 }
2203
2204 pub fn is_write_protected(&self) -> bool {
2206 self.modify_protection.is_some()
2207 }
2208
2209 pub fn save_encrypted(&self, path: impl AsRef<Path>, password: &str) -> crate::Result<()> {
2222 let pkg = self.to_opc_package()?;
2223 let zip_bytes = pkg.to_bytes()?;
2224 let encrypted = crate::crypto::encrypt_package(&zip_bytes, password)?;
2225 std::fs::write(path.as_ref(), &encrypted)?;
2226 Ok(())
2227 }
2228
2229 pub fn to_encrypted_bytes(&self, password: &str) -> crate::Result<Vec<u8>> {
2233 let pkg = self.to_opc_package()?;
2234 let zip_bytes = pkg.to_bytes()?;
2235 crate::crypto::encrypt_package(&zip_bytes, password)
2236 }
2237
2238 pub fn open_encrypted(path: impl AsRef<Path>, password: &str) -> crate::Result<Self> {
2248 let bytes = std::fs::read(path.as_ref())?;
2249 Self::load_encrypted_bytes(&bytes, password)
2250 }
2251
2252 pub fn load_encrypted_bytes(bytes: &[u8], password: &str) -> crate::Result<Self> {
2254 let decrypted = crate::crypto::decrypt_package(bytes, password)?;
2255 Self::load_bytes(&decrypted)
2256 }
2257
2258 pub fn is_encrypted_file(path: impl AsRef<Path>) -> crate::Result<bool> {
2262 let bytes = std::fs::read(path.as_ref())?;
2263 Ok(crate::crypto::is_encrypted_package(&bytes))
2264 }
2265
2266 pub fn is_encrypted_bytes(bytes: &[u8]) -> bool {
2268 crate::crypto::is_encrypted_package(bytes)
2269 }
2270
2271 pub fn save(&self, path: impl AsRef<Path>) -> crate::Result<()> {
2276 let pkg = self.to_opc_package()?;
2277 Ok(pkg.save(path)?)
2278 }
2279
2280 pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
2284 let pkg = self.to_opc_package()?;
2285 Ok(pkg.to_bytes()?)
2286 }
2287
2288 fn to_opc_package(&self) -> crate::Result<OpcPackage> {
2303 let mut pkg = OpcPackage::new();
2304
2305 let mut root_rels = Relationships::new();
2308 root_rels.add(Relationship::internal(
2309 "rId1",
2310 RelType::OfficeDocument,
2311 new_part_name("/ppt/presentation.xml"),
2312 ))?;
2313 root_rels.add(Relationship::internal(
2314 "rId2",
2315 crate::opc::rels::RelType::Other(
2316 "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties".to_string(),
2317 ),
2318 new_part_name("/docProps/core.xml"),
2319 ))?;
2320 root_rels.add(Relationship::internal(
2321 "rId3",
2322 crate::opc::rels::RelType::Other(
2323 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties".to_string(),
2324 ),
2325 new_part_name("/docProps/app.xml"),
2326 ))?;
2327 if !self.custom_properties.is_empty() {
2329 root_rels.add(Relationship::internal(
2330 "rId4",
2331 crate::opc::rels::RelType::Other(
2332 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties".to_string(),
2333 ),
2334 new_part_name("/docProps/custom.xml"),
2335 ))?;
2336 }
2337 let root_rels_xml = root_rels.to_xml();
2338 let root_rels_part = Part::new(
2339 new_part_name("/_rels/.rels"),
2340 ct::RELATIONSHIPS,
2341 root_rels_xml.into_bytes(),
2342 );
2343 pkg.put_part(root_rels_part);
2344
2345 let cp = &self.core_properties;
2349 let title = xml_escape(cp.title.as_deref().unwrap_or("pptx-rs"));
2350 let creator = xml_escape(cp.creator.as_deref().unwrap_or("pptx-rs"));
2351 let last_modified_by = xml_escape(cp.last_modified_by.as_deref().unwrap_or("pptx-rs"));
2352 let mut core_xml = format!(
2353 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2354<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
2355 xmlns:dc="http://purl.org/dc/elements/1.1/"
2356 xmlns:dcterms="http://purl.org/dc/terms/"
2357 xmlns:dcmitype="http://purl.org/dc/dcmitype/"
2358 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2359 <dc:title>{}</dc:title>
2360 <dc:creator>{}</dc:creator>
2361 <cp:lastModifiedBy>{}</cp:lastModifiedBy>"#,
2362 title, creator, last_modified_by
2363 );
2364 if let Some(s) = &cp.subject {
2366 core_xml.push_str(&format!("\n <dc:subject>{}</dc:subject>", xml_escape(s)));
2367 }
2368 if let Some(k) = &cp.keywords {
2369 core_xml.push_str(&format!("\n <cp:keywords>{}</cp:keywords>", xml_escape(k)));
2370 }
2371 if let Some(c) = &cp.category {
2372 core_xml.push_str(&format!("\n <cp:category>{}</cp:category>", xml_escape(c)));
2373 }
2374 if let Some(d) = &cp.description {
2375 core_xml.push_str(&format!(
2376 "\n <dc:description>{}</dc:description>",
2377 xml_escape(d)
2378 ));
2379 }
2380 if let Some(r) = &cp.revision {
2381 core_xml.push_str(&format!("\n <cp:revision>{}</cp:revision>", xml_escape(r)));
2382 }
2383 if let Some(c) = &cp.created {
2384 core_xml.push_str(&format!(
2385 "\n <dcterms:created xsi:type=\"dcterms:W3CDTF\">{}</dcterms:created>",
2386 xml_escape(c)
2387 ));
2388 }
2389 if let Some(m) = &cp.modified {
2390 core_xml.push_str(&format!(
2391 "\n <dcterms:modified xsi:type=\"dcterms:W3CDTF\">{}</dcterms:modified>",
2392 xml_escape(m)
2393 ));
2394 }
2395 core_xml.push_str("\n</cp:coreProperties>");
2396 pkg.put_part(Part::new(
2397 new_part_name("/docProps/core.xml"),
2398 ct::CORE_PROPS,
2399 core_xml.into_bytes(),
2400 ));
2401
2402 let app = xml_escape(cp.application.as_deref().unwrap_or("pptx-rs"));
2405 let app_xml = format!(
2406 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2407<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
2408 xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
2409 <Application>{}</Application>
2410 <AppVersion>1.0</AppVersion>
2411</Properties>"#,
2412 app
2413 );
2414 pkg.put_part(Part::new(
2415 new_part_name("/docProps/app.xml"),
2416 ct::APP_PROPS,
2417 app_xml.into_bytes(),
2418 ));
2419
2420 if !self.custom_properties.is_empty() {
2423 let custom_xml = self.custom_properties.to_xml();
2424 if !custom_xml.is_empty() {
2425 pkg.put_part(Part::new(
2426 new_part_name("/docProps/custom.xml"),
2427 ct::CUSTOM_PROPS,
2428 custom_xml.into_bytes(),
2429 ));
2430 }
2431 }
2432
2433 if self.themes.is_empty() {
2437 let theme_xml = if self.theme.name.is_empty()
2439 && self.theme.color_scheme.dk1.is_none()
2440 && self.theme.font_scheme.major_latin.is_empty()
2441 {
2442 default_theme_xml()
2443 } else {
2444 self.theme.to_xml()
2445 };
2446 pkg.put_part(Part::new(
2447 new_part_name("/ppt/theme/theme1.xml"),
2448 ct::THEME,
2449 theme_xml.into_bytes(),
2450 ));
2451 } else {
2452 for theme in &self.themes {
2454 let theme_partname = if theme.partname.is_empty() {
2455 new_part_name("/ppt/theme/theme1.xml")
2456 } else {
2457 new_part_name(theme.partname.as_str())
2458 };
2459 pkg.put_part(Part::new(
2460 theme_partname,
2461 ct::THEME,
2462 theme.to_xml().into_bytes(),
2463 ));
2464 }
2465 }
2466
2467 if self.slide_masters.items.is_empty() {
2471 let master_xml = OxmlSldMaster::default().to_xml();
2473 pkg.put_part(Part::new(
2474 new_part_name("/ppt/slideMasters/slideMaster1.xml"),
2475 ct::SLIDE_MASTER,
2476 master_xml.into_bytes(),
2477 ));
2478 } else {
2479 for (i, master_ref) in self.slide_masters.items.iter().enumerate() {
2480 let master_partname_str = if master_ref.partname.is_empty() {
2482 format!("/ppt/slideMasters/slideMaster{}.xml", i + 1)
2483 } else {
2484 master_ref.partname.clone()
2485 };
2486 let master_partname = new_part_name(master_partname_str.as_str());
2487 let master_xml = master_ref.oxml.borrow().to_xml();
2488
2489 let mut master_rels = Relationships::new();
2491 let master_partname_for_rel = master_partname_str.clone();
2492
2493 for layout_ref in &self.slide_layouts.items {
2495 let layout_belongs_to_this_master = layout_ref.master_partname
2499 == master_ref.partname
2500 || (layout_ref.master_partname.is_empty() && i == 0);
2501 if layout_belongs_to_this_master {
2502 let layout_target =
2504 relative_part_path(&master_partname_for_rel, &layout_ref.partname);
2505 master_rels.add(Relationship::internal_str(
2506 layout_ref.rid.as_str(),
2507 RelType::SlideLayout,
2508 layout_target.as_str(),
2509 ))?;
2510 }
2511 }
2512 let master_layout_rids_empty = master_ref.oxml.borrow().layout_rids.is_empty();
2515 let has_matching_layout = self.slide_layouts.items.iter().any(|l| {
2516 l.master_partname == master_ref.partname
2517 || (l.master_partname.is_empty() && i == 0)
2518 });
2519 if master_layout_rids_empty && !has_matching_layout && i == 0 {
2520 master_rels.add(Relationship::internal_str(
2521 "rId1",
2522 RelType::SlideLayout,
2523 "../slideLayouts/slideLayout1.xml",
2524 ))?;
2525 }
2526
2527 let theme_rid = if master_ref.theme_rid.is_empty() {
2529 "rId2".to_string()
2530 } else {
2531 master_ref.theme_rid.clone()
2532 };
2533 let theme_target = if master_ref.theme_partname.is_empty() {
2535 "../theme/theme1.xml".to_string()
2536 } else {
2537 relative_part_path(&master_partname_for_rel, &master_ref.theme_partname)
2538 };
2539 master_rels.add(Relationship::internal_str(
2540 theme_rid.as_str(),
2541 RelType::Theme,
2542 theme_target.as_str(),
2543 ))?;
2544
2545 let master_rels_xml = master_rels.to_xml();
2546 let master_rels_partname = rels_partname_for(master_partname.as_str());
2547 pkg.put_part(Part::new(
2548 PartName::from_unchecked(master_rels_partname),
2549 ct::RELATIONSHIPS,
2550 master_rels_xml.into_bytes(),
2551 ));
2552 pkg.put_part(Part::new(
2553 master_partname,
2554 ct::SLIDE_MASTER,
2555 master_xml.into_bytes(),
2556 ));
2557 }
2558 }
2559
2560 if self.slide_layouts.items.is_empty() {
2564 let layout_xml = OxmlSldLayout::default().to_xml();
2565 pkg.put_part(Part::new(
2566 new_part_name("/ppt/slideLayouts/slideLayout1.xml"),
2567 ct::SLIDE_LAYOUT,
2568 layout_xml.into_bytes(),
2569 ));
2570 } else {
2571 for (i, layout_ref) in self.slide_layouts.items.iter().enumerate() {
2572 let layout_partname_str = if layout_ref.partname.is_empty() {
2574 format!("/ppt/slideLayouts/slideLayout{}.xml", i + 1)
2575 } else {
2576 layout_ref.partname.clone()
2577 };
2578 let layout_partname = new_part_name(layout_partname_str.as_str());
2579 let layout_xml = layout_ref.oxml.borrow().to_xml();
2580
2581 let mut layout_rels = Relationships::new();
2583 let master_target = if layout_ref.master_partname.is_empty() {
2585 "../slideMasters/slideMaster1.xml".to_string()
2586 } else {
2587 relative_part_path(&layout_partname_str, &layout_ref.master_partname)
2588 };
2589 layout_rels.add(Relationship::internal_str(
2590 "rId1",
2591 RelType::SlideMaster,
2592 master_target.as_str(),
2593 ))?;
2594
2595 let layout_rels_partname = rels_partname_for(layout_partname.as_str());
2596 pkg.put_part(Part::new(
2597 PartName::from_unchecked(layout_rels_partname),
2598 ct::RELATIONSHIPS,
2599 layout_rels.to_xml().into_bytes(),
2600 ));
2601 pkg.put_part(Part::new(
2602 layout_partname,
2603 ct::SLIDE_LAYOUT,
2604 layout_xml.into_bytes(),
2605 ));
2606 }
2607 }
2608
2609 if !self.notes_masters.items.is_empty() {
2615 for (i, nm_ref) in self.notes_masters.items.iter().enumerate() {
2616 let nm_partname_str = if nm_ref.partname.is_empty() {
2618 format!("/ppt/notesMasters/notesMaster{}.xml", i + 1)
2619 } else {
2620 nm_ref.partname.clone()
2621 };
2622 let nm_partname = new_part_name(nm_partname_str.as_str());
2623 let nm_xml = nm_ref.oxml.borrow().to_xml();
2624 pkg.put_part(Part::new(
2625 nm_partname,
2626 ct::NOTES_MASTER,
2627 nm_xml.into_bytes(),
2628 ));
2629 }
2630 }
2631
2632 let mut pres_rels = Relationships::new();
2641 if !self.sld_master_ids.is_empty() {
2642 for (i, (_id, rid)) in self.sld_master_ids.iter().enumerate() {
2644 let target = self
2645 .slide_masters
2646 .items
2647 .get(i)
2648 .map(|m| {
2649 m.partname
2650 .strip_prefix("/ppt/")
2651 .unwrap_or(&m.partname)
2652 .to_string()
2653 })
2654 .unwrap_or_else(|| format!("slideMasters/slideMaster{}.xml", i + 1));
2655 pres_rels.add(Relationship::internal_str(
2656 rid,
2657 RelType::SlideMaster,
2658 &target,
2659 ))?;
2660 }
2661 } else {
2662 pres_rels.add(Relationship::internal_str(
2664 "rIdP1",
2665 RelType::SlideMaster,
2666 "slideMasters/slideMaster1.xml",
2667 ))?;
2668 }
2669 for (i, (_id, rid)) in self.notes_master_ids.iter().enumerate() {
2672 let target = self
2673 .notes_masters
2674 .items
2675 .get(i)
2676 .map(|nm| {
2677 nm.partname
2678 .strip_prefix("/ppt/")
2679 .unwrap_or(&nm.partname)
2680 .to_string()
2681 })
2682 .unwrap_or_else(|| format!("notesMasters/notesMaster{}.xml", i + 1));
2683 pres_rels.add(Relationship::internal_str(
2684 rid,
2685 RelType::NotesMaster,
2686 &target,
2687 ))?;
2688 }
2689 pres_rels.add(Relationship::internal_str(
2690 "rIdP4",
2691 crate::opc::rels::RelType::Other(
2692 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"
2693 .to_string(),
2694 ),
2695 "presProps.xml",
2696 ))?;
2697 pres_rels.add(Relationship::internal_str(
2698 "rIdP5",
2699 crate::opc::rels::RelType::Other(
2700 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"
2701 .to_string(),
2702 ),
2703 "viewProps.xml",
2704 ))?;
2705 pres_rels.add(Relationship::internal_str(
2706 "rIdP6",
2707 crate::opc::rels::RelType::Other(
2708 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"
2709 .to_string(),
2710 ),
2711 "tableStyles.xml",
2712 ))?;
2713
2714 let sld_master_ids: Vec<crate::oxml::presentation::SldMasterIdEntry> =
2717 if !self.sld_master_ids.is_empty() {
2718 self.sld_master_ids
2719 .iter()
2720 .map(|(id, rid)| crate::oxml::presentation::SldMasterIdEntry {
2721 id: *id,
2722 rid: rid.clone(),
2723 })
2724 .collect()
2725 } else {
2726 Vec::new()
2728 };
2729 let mut pres_root = PresentationRoot {
2730 slide_width: Some(self.width),
2731 slide_height: Some(self.height),
2732 slide_ids: Vec::new(),
2733 sld_master_ids,
2734 notes_master_ids: self
2737 .notes_master_ids
2738 .iter()
2739 .map(|(id, rid)| crate::oxml::presentation::NotesMasterIdEntry {
2740 id: *id,
2741 rid: rid.clone(),
2742 })
2743 .collect(),
2744 sections: self.sections.clone(),
2747 ..Default::default()
2748 };
2749
2750 let mut notes_index: u32 = 0;
2752 let mut comments_index: u32 = 0;
2753 let mut chart_global_index: u32 = 0;
2755 let mut chart_xlsx_global_index: u32 = 0;
2757 let mut ole_global_index: u32 = 0;
2759 let mut video_global_index: u32 = 0;
2763 let mut audio_global_index: u32 = 0;
2764 let mut diagram_global_index: u32 = 0;
2766 for (i, entry) in self.slides.iter().enumerate() {
2767 let partname = PartName::from_unchecked(entry.partname.clone());
2768 let rid = entry.rid.clone();
2769 let mut sld = entry.sld.clone();
2770 let slide_layout_rid = {
2772 let lr = entry.sld.layout_rid();
2773 if lr.is_empty() {
2774 "rId1".to_string()
2775 } else {
2776 lr
2777 }
2778 };
2779 let slide_layout_target = match entry.sld.layout_partname() {
2781 Some(lp) => relative_part_path(&entry.partname, lp),
2782 None => "../slideLayouts/slideLayout1.xml".to_string(),
2783 };
2784 sld.set_layout_rid(slide_layout_rid.clone());
2786
2787 let mut extra_rels = Relationships::new();
2789 extra_rels.add(Relationship::internal_str(
2790 slide_layout_rid.as_str(),
2791 RelType::SlideLayout,
2792 slide_layout_target.as_str(),
2793 ))?;
2794 for media in &entry.sld.media_entries {
2796 let media_target = format!(
2797 "../media/{}",
2798 media.partname.as_str().trim_start_matches("/ppt/media/")
2799 );
2800 extra_rels.add(Relationship::internal_str(
2801 media.rid.clone(),
2802 RelType::Image,
2803 media_target,
2804 ))?;
2805 }
2806 let mut this_notes_rid: Option<String> = None;
2814 if let Some(notes_tb) = sld.notes() {
2815 notes_index += 1;
2816 let notes_rid = format!("rIdNotes{}", notes_index);
2819 let notes_partname = match entry.sld.notes_partname() {
2821 Some(p) => p.to_string(),
2822 None => format!("/ppt/notesSlides/notesSlide{}.xml", notes_index),
2823 };
2824 let notes_slide_target = match entry.sld.notes_slide_rel_target() {
2826 Some(t) => t.to_string(),
2827 None => format!("../slides/slide{}.xml", i + 1),
2828 };
2829 this_notes_rid = Some(notes_rid.clone());
2830 let notes_target_rel = {
2833 let trimmed = notes_partname.trim_start_matches("/ppt/");
2835 format!("../{}", trimmed)
2836 };
2837 extra_rels.add(Relationship::internal_str(
2838 notes_rid.clone(),
2839 RelType::NotesSlide,
2840 notes_target_rel,
2841 ))?;
2842 let pres_target_rel = {
2846 let trimmed = notes_partname.trim_start_matches("/ppt/");
2847 trimmed.to_string()
2848 };
2849 pres_rels.add(Relationship::internal_str(
2850 format!("rIdNotesPres{}", notes_index),
2851 RelType::NotesSlide,
2852 pres_target_rel,
2853 ))?;
2854 let mut notes_rels = Relationships::new();
2857 notes_rels.add(Relationship::internal_str(
2858 "rId1",
2859 RelType::Slide,
2860 notes_slide_target,
2861 ))?;
2862 let notes_rels_partname = rels_partname_for(¬es_partname);
2863 pkg.put_part(Part::new(
2864 PartName::from_unchecked(notes_rels_partname),
2865 ct::RELATIONSHIPS,
2866 notes_rels.to_xml().into_bytes(),
2867 ));
2868 let notes_xml_str = crate::oxml::notes_xml(notes_tb);
2870 pkg.put_part(Part::new(
2871 PartName::from_unchecked(notes_partname),
2872 ct::NOTES_SLIDE,
2873 notes_xml_str.into_bytes(),
2874 ));
2875 }
2876 if let Some(rid) = this_notes_rid {
2878 sld.set_notes_rid(rid);
2879 }
2880
2881 let mut this_comments_rid: Option<String> = None;
2885 if let Some(comment_lst) = sld.comments() {
2886 comments_index += 1;
2887 let comments_rid = format!("rIdComments{}", comments_index);
2888 let comments_partname = match sld.comments_partname() {
2890 Some(p) => p.to_string(),
2891 None => format!("/ppt/comments/comment{}.xml", comments_index),
2892 };
2893 this_comments_rid = Some(comments_rid.clone());
2894 let comments_target_rel = {
2897 let trimmed = comments_partname.trim_start_matches("/ppt/");
2898 format!("../{}", trimmed)
2899 };
2900 extra_rels.add(Relationship::internal_str(
2901 comments_rid.clone(),
2902 RelType::Comments,
2903 comments_target_rel,
2904 ))?;
2905 let comments_xml_str = comment_lst.to_xml();
2907 pkg.put_part(Part::new(
2908 PartName::from_unchecked(comments_partname),
2909 ct::COMMENTS,
2910 comments_xml_str.into_bytes(),
2911 ));
2912 }
2913 if let Some(rid) = this_comments_rid {
2914 sld.set_comments_rid(rid);
2915 }
2916
2917 for chart_entry in &entry.sld.chart_entries {
2928 chart_global_index += 1;
2929 let chart_partname = format!("/ppt/charts/chart{}.xml", chart_global_index);
2930 let chart_target_rel = format!("../charts/chart{}.xml", chart_global_index);
2933 extra_rels.add(Relationship::internal_str(
2934 chart_entry.rid.clone(),
2935 RelType::Chart,
2936 chart_target_rel,
2937 ))?;
2938
2939 let mut chart_model = chart_entry.chart.clone();
2950 if let Some(xlsx_blob) = &chart_entry.xlsx_blob {
2951 chart_xlsx_global_index += 1;
2952 let xlsx_partname = format!(
2953 "/ppt/embeddings/Microsoft_Excel_Worksheet{}.xlsx",
2954 chart_xlsx_global_index
2955 );
2956 let xlsx_target_rel = format!(
2957 "../embeddings/Microsoft_Excel_Worksheet{}.xlsx",
2958 chart_xlsx_global_index
2959 );
2960 let xlsx_rid = format!("rIdXlsx{}", chart_xlsx_global_index);
2962
2963 pkg.put_part(Part::new(
2965 PartName::from_unchecked(xlsx_partname.clone()),
2966 ct::SPREADSHEET_XLSX,
2967 xlsx_blob.clone(),
2968 ));
2969
2970 let mut chart_rels = crate::opc::rels::Relationships::new();
2972 chart_rels.add(Relationship::internal_str(
2973 xlsx_rid.clone(),
2974 RelType::Package,
2975 xlsx_target_rel,
2976 ))?;
2977 let chart_rels_partname =
2978 format!("/ppt/charts/_rels/chart{}.xml.rels", chart_global_index);
2979 pkg.put_part(Part::new(
2980 PartName::from_unchecked(chart_rels_partname),
2981 ct::RELATIONSHIPS,
2982 chart_rels.to_xml().into_bytes(),
2983 ));
2984
2985 chart_model.external_data_rid = Some(xlsx_rid);
2987 }
2988
2989 let chart_xml = chart_model.to_xml();
2991 pkg.put_part(Part::new(
2992 PartName::from_unchecked(chart_partname),
2993 ct::CHART,
2994 chart_xml.into_bytes(),
2995 ));
2996 }
2997
2998 for ole_entry in &entry.sld.ole_entries {
3009 ole_global_index += 1;
3010 let ole_partname = format!("/ppt/embeddings/oleObject{}.bin", ole_global_index);
3011 let ole_target_rel = format!("../embeddings/oleObject{}.bin", ole_global_index);
3014 extra_rels.add(Relationship::internal_str(
3015 ole_entry.rid.clone(),
3016 RelType::OleObject,
3017 ole_target_rel,
3018 ))?;
3019 pkg.put_part(Part::new(
3021 PartName::from_unchecked(ole_partname),
3022 ct::OLE_OBJECT,
3023 ole_entry.blob.clone(),
3024 ));
3025 }
3026
3027 for video_entry in &entry.sld.video_entries {
3038 video_global_index += 1;
3039 let video_partname = format!("/ppt/media/media{}.mp4", video_global_index);
3040 let video_target_rel = format!("../media/media{}.mp4", video_global_index);
3042 extra_rels.add(Relationship::internal_str(
3043 video_entry.rid.clone(),
3044 RelType::Video,
3045 video_target_rel,
3046 ))?;
3047 pkg.put_part(Part::new(
3049 PartName::from_unchecked(video_partname),
3050 ct::VIDEO_MP4,
3051 video_entry.blob.clone(),
3052 ));
3053 }
3054
3055 for audio_entry in &entry.sld.audio_entries {
3058 audio_global_index += 1;
3059 let audio_partname = format!("/ppt/media/media{}.mp3", audio_global_index);
3060 let audio_target_rel = format!("../media/media{}.mp3", audio_global_index);
3061 extra_rels.add(Relationship::internal_str(
3062 audio_entry.rid.clone(),
3063 RelType::Audio,
3064 audio_target_rel,
3065 ))?;
3066 pkg.put_part(Part::new(
3067 PartName::from_unchecked(audio_partname),
3068 ct::AUDIO_MP3,
3069 audio_entry.blob.clone(),
3070 ));
3071 }
3072
3073 for diagram_entry in &entry.sld.diagram_entries {
3091 diagram_global_index += 1;
3092 let idx = diagram_global_index;
3093
3094 let data_partname = format!("/ppt/diagrams/data{}.xml", idx);
3096 let layout_partname = format!("/ppt/diagrams/layout{}.xml", idx);
3097 let quick_style_partname = format!("/ppt/diagrams/quickStyles{}.xml", idx);
3098 let colors_partname = format!("/ppt/diagrams/colors{}.xml", idx);
3099
3100 let data_target_rel = format!("../diagrams/data{}.xml", idx);
3102 let layout_target_rel = format!("../diagrams/layout{}.xml", idx);
3103 let quick_style_target_rel = format!("../diagrams/quickStyles{}.xml", idx);
3104 let colors_target_rel = format!("../diagrams/colors{}.xml", idx);
3105
3106 extra_rels.add(Relationship::internal_str(
3108 diagram_entry.data_rid.clone(),
3109 RelType::DiagramData,
3110 data_target_rel,
3111 ))?;
3112 extra_rels.add(Relationship::internal_str(
3113 diagram_entry.layout_rid.clone(),
3114 RelType::DiagramLayout,
3115 layout_target_rel,
3116 ))?;
3117 extra_rels.add(Relationship::internal_str(
3118 diagram_entry.quick_style_rid.clone(),
3119 RelType::DiagramQuickStyle,
3120 quick_style_target_rel,
3121 ))?;
3122 extra_rels.add(Relationship::internal_str(
3123 diagram_entry.colors_rid.clone(),
3124 RelType::DiagramColors,
3125 colors_target_rel,
3126 ))?;
3127
3128 pkg.put_part(Part::new(
3130 PartName::from_unchecked(data_partname),
3131 ct::DIAGRAM_DATA,
3132 diagram_entry.data_xml.clone().into_bytes(),
3133 ));
3134 pkg.put_part(Part::new(
3135 PartName::from_unchecked(layout_partname),
3136 ct::DIAGRAM_LAYOUT,
3137 diagram_entry.layout_xml.clone().into_bytes(),
3138 ));
3139 pkg.put_part(Part::new(
3140 PartName::from_unchecked(quick_style_partname),
3141 ct::DIAGRAM_QUICK_STYLE,
3142 diagram_entry.quick_style_xml.clone().into_bytes(),
3143 ));
3144 pkg.put_part(Part::new(
3145 PartName::from_unchecked(colors_partname),
3146 ct::DIAGRAM_COLORS,
3147 diagram_entry.colors_xml.clone().into_bytes(),
3148 ));
3149 }
3150
3151 let extra_rels_xml = extra_rels.to_xml();
3152 let extra_rels_partname = rels_partname_for(partname.as_str());
3153 pkg.put_part(Part::new(
3154 PartName::from_unchecked(extra_rels_partname),
3155 ct::RELATIONSHIPS,
3156 extra_rels_xml.into_bytes(),
3157 ));
3158
3159 pkg.put_part(Part::new(partname, ct::SLIDE, sld.to_xml().into_bytes()));
3161
3162 pres_rels.add(Relationship::internal_str(
3164 rid.clone(),
3165 RelType::Slide,
3166 format!("slides/slide{}.xml", i + 1),
3167 ))?;
3168 pres_root.slide_ids.push(SlideIdEntry {
3170 id: entry.sld_id,
3171 rid: rid.clone(),
3172 });
3173 }
3174
3175 if !self.comment_authors.is_empty() {
3179 let authors_xml = self.comment_authors.to_xml();
3180 pkg.put_part(Part::new(
3181 new_part_name("/ppt/commentAuthors.xml"),
3182 ct::COMMENT_AUTHORS,
3183 authors_xml.into_bytes(),
3184 ));
3185 pres_rels.add(Relationship::internal_str(
3186 "rIdCommentAuthors",
3187 RelType::CommentAuthors,
3188 "commentAuthors.xml",
3189 ))?;
3190 }
3191
3192 let pres_xml = if let Some(ref mp) = self.modify_protection {
3195 inject_modify_verifier(&pres_root.to_xml(), &mp.to_xml_element())
3196 } else {
3197 pres_root.to_xml()
3198 };
3199 pkg.put_part(Part::new(
3200 new_part_name("/ppt/presentation.xml"),
3201 ct::PRESENTATION,
3202 pres_xml.into_bytes(),
3203 ));
3204 pkg.put_part(Part::new(
3206 new_part_name("/ppt/_rels/presentation.xml.rels"),
3207 ct::RELATIONSHIPS,
3208 pres_rels.to_xml().into_bytes(),
3209 ));
3210
3211 use std::collections::BTreeMap;
3214 let mut seen: BTreeMap<String, ()> = BTreeMap::new();
3215 for entry in self.slides.iter() {
3216 for m in &entry.sld.media_entries {
3217 if seen.insert(m.partname.as_str().to_string(), ()).is_none() {
3218 pkg.put_part(Part::new(
3219 m.partname.clone(),
3220 m.content_type.clone(),
3221 m.blob.clone(),
3222 ));
3223 }
3224 }
3225 }
3226
3227 let pres_props_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3230<p:presentationPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
3231 <p:showPr/>
3232</p:presentationPr>"#;
3233 pkg.put_part(Part::new(
3234 new_part_name("/ppt/presProps.xml"),
3235 "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml",
3236 pres_props_xml.as_bytes().to_vec(),
3237 ));
3238
3239 let view_props_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3242<p:viewProps xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
3243 <p:normalViewPr><p:restoredLeft sz="15620"/><p:restoredTop sz="94660"/></p:normalViewPr>
3244</p:viewProps>"#;
3245 pkg.put_part(Part::new(
3246 new_part_name("/ppt/viewProps.xml"),
3247 "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml",
3248 view_props_xml.as_bytes().to_vec(),
3249 ));
3250
3251 let table_styles_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3254<a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" defStyleStyle=""/>
3255"#;
3256 pkg.put_part(Part::new(
3257 new_part_name("/ppt/tableStyles.xml"),
3258 "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml",
3259 table_styles_xml.as_bytes().to_vec(),
3260 ));
3261
3262 Ok(pkg)
3263 }
3264
3265 fn ensure_default_master_and_layout(&mut self) -> crate::Result<()> {
3275 if self.slide_masters.is_empty() {
3276 self.slide_masters.items.push(SlideMasterRef {
3277 idx: 0,
3278 partname: "/ppt/slideMasters/slideMaster1.xml".to_string(),
3279 rid: "rIdMaster1".to_string(),
3280 oxml: Rc::new(std::cell::RefCell::new(OxmlSldMaster::default())),
3281 theme_partname: String::new(),
3282 theme_rid: String::new(),
3283 });
3284 }
3285 if self.slide_layouts.is_empty() {
3286 self.slide_layouts.items.push(SlideLayoutRef {
3287 idx: 0,
3288 partname: "/ppt/slideLayouts/slideLayout1.xml".to_string(),
3289 rid: "rIdLayout1".to_string(),
3290 oxml: Rc::new(std::cell::RefCell::new(OxmlSldLayout::default())),
3291 master_partname: String::new(),
3292 });
3293 }
3294 Ok(())
3295 }
3296}
3297
3298fn resolve_relative_partname(parent_partname: &str, rel_target: &str) -> String {
3311 if rel_target.starts_with('/') {
3313 return rel_target.to_string();
3314 }
3315 let parent_dir = match parent_partname.rfind('/') {
3317 Some(i) => &parent_partname[..i],
3318 None => "",
3319 };
3320 let mut stack: Vec<&str> = Vec::new();
3322 let combined = if parent_dir.is_empty() {
3323 format!("/{}", rel_target)
3324 } else {
3325 format!("{}/{}", parent_dir, rel_target)
3326 };
3327 for seg in combined.split('/') {
3328 match seg {
3329 "" | "." => continue,
3330 ".." => {
3331 stack.pop();
3332 }
3333 _ => stack.push(seg),
3334 }
3335 }
3336 let mut out = String::from("/");
3337 out.push_str(&stack.join("/"));
3338 if out == "/" && !stack.is_empty() {
3339 }
3341 out
3342}
3343
3344fn relative_part_path(src_partname: &str, dst_partname: &str) -> String {
3354 let src_owned;
3356 let src = if src_partname.starts_with('/') {
3357 src_partname
3358 } else {
3359 src_owned = format!("/{}", src_partname);
3360 &src_owned
3361 };
3362 let dst_owned;
3363 let dst = if dst_partname.starts_with('/') {
3364 dst_partname
3365 } else {
3366 dst_owned = format!("/{}", dst_partname);
3367 &dst_owned
3368 };
3369 let src_dir = match src.rfind('/') {
3371 Some(i) => &src[..i],
3372 None => "",
3373 };
3374 let dst_dir = match dst.rfind('/') {
3375 Some(i) => &dst[..i],
3376 None => "",
3377 };
3378 let dst_basename = match dst.rfind('/') {
3379 Some(i) => &dst[i + 1..],
3380 None => dst,
3381 };
3382 let src_segs: Vec<&str> = src_dir.split('/').filter(|s| !s.is_empty()).collect();
3384 let dst_segs: Vec<&str> = dst_dir.split('/').filter(|s| !s.is_empty()).collect();
3385 let common = src_segs
3386 .iter()
3387 .zip(dst_segs.iter())
3388 .take_while(|(a, b)| a == b)
3389 .count();
3390 let up_count = src_segs.len() - common;
3391 let mut result = String::new();
3393 for _ in 0..up_count {
3394 result.push_str("../");
3395 }
3396 for seg in &dst_segs[common..] {
3397 result.push_str(seg);
3398 result.push('/');
3399 }
3400 result.push_str(dst_basename);
3401 result
3402}
3403
3404fn inject_modify_verifier(pres_xml: &str, verifier_xml: &str) -> String {
3412 if pres_xml.contains("p:modifyVerifier") {
3413 return pres_xml.to_string();
3414 }
3415 if let Some(pos) = pres_xml.find("<p:extLst") {
3417 let mut out = String::with_capacity(pres_xml.len() + verifier_xml.len());
3418 out.push_str(&pres_xml[..pos]);
3419 out.push_str(verifier_xml);
3420 out.push_str(&pres_xml[pos..]);
3421 return out;
3422 }
3423 if let Some(pos) = pres_xml.rfind("</p:presentation>") {
3425 let mut out = String::with_capacity(pres_xml.len() + verifier_xml.len());
3426 out.push_str(&pres_xml[..pos]);
3427 out.push_str(verifier_xml);
3428 out.push_str(&pres_xml[pos..]);
3429 return out;
3430 }
3431 pres_xml.to_string()
3433}
3434
3435impl Default for Presentation {
3436 #[allow(clippy::expect_used)]
3444 fn default() -> Self {
3445 Self::new().expect("Presentation::new() is infallible in current implementation")
3446 }
3447}
3448
3449#[cfg(test)]
3450mod tests {
3451 use super::*;
3452 use crate::units::Inches;
3453
3454 #[test]
3456 fn resolve_relative_partname_examples() {
3457 assert_eq!(
3458 resolve_relative_partname("/ppt/slides/slide1.xml", "../notesSlides/notesSlide1.xml"),
3459 "/ppt/notesSlides/notesSlide1.xml"
3460 );
3461 assert_eq!(
3462 resolve_relative_partname("/ppt/slides/slide1.xml", "media/image1.png"),
3463 "/ppt/slides/media/image1.png"
3464 );
3465 assert_eq!(
3466 resolve_relative_partname("/ppt/slides/slide1.xml", "../media/image1.png"),
3467 "/ppt/media/image1.png"
3468 );
3469 assert_eq!(
3471 resolve_relative_partname("/ppt/slides/slide1.xml", "/ppt/notesSlides/n1.xml"),
3472 "/ppt/notesSlides/n1.xml"
3473 );
3474 assert_eq!(
3476 resolve_relative_partname("/ppt/slides/slide1.xml", "slide2.xml"),
3477 "/ppt/slides/slide2.xml"
3478 );
3479 }
3480
3481 #[test]
3483 fn relative_part_path_examples() {
3484 assert_eq!(
3486 relative_part_path(
3487 "/ppt/slideMasters/slideMaster1.xml",
3488 "/ppt/slideLayouts/slideLayout1.xml"
3489 ),
3490 "../slideLayouts/slideLayout1.xml"
3491 );
3492 assert_eq!(
3494 relative_part_path(
3495 "/ppt/slideMasters/slideMaster1.xml",
3496 "/ppt/theme/theme1.xml"
3497 ),
3498 "../theme/theme1.xml"
3499 );
3500 assert_eq!(
3502 relative_part_path(
3503 "/ppt/slides/slide1.xml",
3504 "/ppt/slideLayouts/slideLayout1.xml"
3505 ),
3506 "../slideLayouts/slideLayout1.xml"
3507 );
3508 assert_eq!(
3510 relative_part_path(
3511 "/ppt/slideLayouts/slideLayout1.xml",
3512 "/ppt/slideMasters/slideMaster1.xml"
3513 ),
3514 "../slideMasters/slideMaster1.xml"
3515 );
3516 assert_eq!(
3518 relative_part_path("/ppt/slides/slide1.xml", "/ppt/slides/slide2.xml"),
3519 "slide2.xml"
3520 );
3521 assert_eq!(
3523 relative_part_path("ppt/slides/slide1.xml", "ppt/slideLayouts/slideLayout1.xml"),
3524 "../slideLayouts/slideLayout1.xml"
3525 );
3526 }
3527
3528 #[test]
3536 fn roundtrip_presentation_loads_back() {
3537 let mut p = Presentation::new().expect("new ok");
3539 let counter = p.id_counter();
3540 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3541 slide
3542 .shapes_mut()
3543 .add_textbox_with_text(
3544 Inches(1.0),
3545 Inches(1.0),
3546 Inches(4.0),
3547 Inches(1.0),
3548 "Hello roundtrip",
3549 )
3550 .expect("textbox");
3551 slide.set_notes_text(Some("speaker note line 1\nline 2"));
3552 p.set_slide_size(Emu(7_000_000), Emu(5_000_000));
3553
3554 let bytes = p.to_bytes().expect("to_bytes ok");
3556 assert!(!bytes.is_empty());
3557
3558 let p2 = Presentation::load_bytes(&bytes).expect("load_bytes ok");
3560
3561 assert_eq!(p2.slides().len(), 1);
3563 let s = p2.slides().get(0).expect("slide 0");
3564 assert_eq!(s.sld.layout_rid(), "rIdLayout1");
3566 let inner_shapes = &s.sld.inner.shapes;
3568 assert_eq!(inner_shapes.len(), 1);
3569 if let crate::oxml::SlideShape::Sp(sp) = &inner_shapes[0] {
3570 assert_eq!(sp.text.paragraphs.len(), 1);
3571 assert_eq!(sp.text.paragraphs[0].runs[0].text, "Hello roundtrip");
3572 } else {
3573 panic!("expected Sp");
3574 }
3575 let notes = s.sld.notes_text().expect("notes present");
3577 assert!(notes.contains("speaker note line 1"));
3578 assert_eq!(p2.slide_width().0, 7_000_000);
3580 assert_eq!(p2.slide_height().0, 5_000_000);
3581 }
3582
3583 #[test]
3585 fn load_then_resave_preserves_content() {
3586 let mut p = Presentation::new().expect("new ok");
3587 let counter = p.id_counter();
3588 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3589 slide
3590 .shapes_mut()
3591 .add_textbox_with_text(Inches(0.5), Inches(0.5), Inches(6.0), Inches(0.8), "stable")
3592 .expect("textbox");
3593 let bytes = p.to_bytes().expect("to_bytes");
3594
3595 let p2 = Presentation::load_bytes(&bytes).expect("load 1");
3597 let bytes2 = p2.to_bytes().expect("to_bytes 2");
3598 let p3 = Presentation::load_bytes(&bytes2).expect("load 2");
3599
3600 assert_eq!(p3.slides().len(), 1);
3601 let s = p3.slides().get(0).expect("slide 0");
3602 let inner_shapes = &s.sld.inner.shapes;
3603 if let crate::oxml::SlideShape::Sp(sp) = &inner_shapes[0] {
3604 assert_eq!(sp.text.paragraphs[0].runs[0].text, "stable");
3605 } else {
3606 panic!("expected Sp");
3607 }
3608 }
3609
3610 #[test]
3617 fn notes_roundtrip_preserves_partname_and_rels_target() {
3618 let mut p = Presentation::new().expect("new ok");
3620 let counter = p.id_counter();
3621 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3622 slide
3623 .shapes_mut()
3624 .add_textbox_with_text(
3625 Inches(0.5),
3626 Inches(0.5),
3627 Inches(6.0),
3628 Inches(0.8),
3629 "roundtrip notes test",
3630 )
3631 .expect("textbox");
3632 slide.set_notes_text(Some("first notes\nline 2"));
3633 let bytes1 = p.to_bytes().expect("to_bytes 1");
3634
3635 let mut p2 = Presentation::load_bytes(&bytes1).expect("load 1");
3637 p2.slides_mut()
3638 .get_mut(0)
3639 .expect("slide 0")
3640 .sld
3641 .set_notes_text(Some("updated notes"));
3642 let bytes2 = p2.to_bytes().expect("to_bytes 2");
3643
3644 let p3 = Presentation::load_bytes(&bytes2).expect("load 2");
3646 let s = p3.slides().get(0).expect("slide 0");
3647 let notes_text = s.sld.notes_text().expect("notes present");
3648 assert!(notes_text.contains("updated notes"));
3649
3650 let cursor = std::io::Cursor::new(bytes2);
3652 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3653 let mut nrels_xml = String::new();
3654 zip.by_name("ppt/notesSlides/_rels/notesSlide1.xml.rels")
3655 .expect("notesSlide1.xml.rels exists")
3656 .read_to_string(&mut nrels_xml)
3657 .expect("read notes rels");
3658 assert!(
3660 nrels_xml.contains("../slides/slide1.xml"),
3661 "notesSlide1.xml.rels missing slide rels, got: {nrels_xml}"
3662 );
3663 }
3664
3665 #[test]
3669 fn new_slide_with_notes_allocates_fresh_partname() {
3670 let mut p = Presentation::new().expect("new ok");
3671 let counter = p.id_counter();
3672 let s = p.slides_mut().add_slide(counter).expect("add slide");
3673 s.set_notes_text(Some("brand new"));
3674 let bytes = p.to_bytes().expect("to_bytes");
3675
3676 let p2 = Presentation::load_bytes(&bytes).expect("load");
3677 let s2 = p2.slides().get(0).expect("slide 0");
3678 let notes = s2.sld.notes_text().expect("notes present");
3679 assert!(notes.contains("brand new"));
3680
3681 let cursor = std::io::Cursor::new(bytes);
3683 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3684 assert!(zip.by_name("ppt/notesSlides/notesSlide1.xml").is_ok());
3685 }
3686
3687 #[test]
3692 fn notes_roundtrip_multiple_slides() {
3693 let mut p = Presentation::new().expect("new ok");
3694 let counter = p.id_counter();
3695 for i in 0..3 {
3698 let s = p
3699 .slides_mut()
3700 .add_slide(counter.clone())
3701 .expect("add slide");
3702 if i != 1 {
3703 s.set_notes_text(Some(&format!("note for slide {}", i + 1)));
3704 }
3705 }
3706 let bytes1 = p.to_bytes().expect("to_bytes 1");
3707
3708 let mut p2 = Presentation::load_bytes(&bytes1).expect("load");
3710 {
3712 let sm = p2.slides_mut();
3713 sm.get_mut(0).unwrap().sld.set_notes_text(Some("edited 1"));
3714 sm.get_mut(2).unwrap().sld.set_notes_text(Some("edited 3"));
3716 }
3717 let bytes2 = p2.to_bytes().expect("to_bytes 2");
3718
3719 let p3 = Presentation::load_bytes(&bytes2).expect("load 2");
3721 assert_eq!(p3.slides().len(), 3);
3722 let s0 = p3.slides().get(0).expect("s0");
3723 let s1 = p3.slides().get(1).expect("s1");
3724 let s2 = p3.slides().get(2).expect("s2");
3725 assert_eq!(s0.sld.notes_text().as_deref(), Some("edited 1"));
3726 assert_eq!(s1.sld.notes_text(), None);
3727 assert_eq!(s2.sld.notes_text().as_deref(), Some("edited 3"));
3728
3729 let cursor = std::io::Cursor::new(bytes2);
3732 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3733 assert!(zip.by_name("ppt/notesSlides/notesSlide1.xml").is_ok());
3734 assert!(zip.by_name("ppt/notesSlides/notesSlide2.xml").is_ok());
3735 assert!(zip
3736 .by_name("ppt/notesSlides/_rels/notesSlide1.xml.rels")
3737 .is_ok());
3738 assert!(zip
3739 .by_name("ppt/notesSlides/_rels/notesSlide2.xml.rels")
3740 .is_ok());
3741 }
3742
3743 #[test]
3755 fn layout_rid_preserved_on_save() {
3756 let mut p = Presentation::new().expect("new ok");
3758 let counter = p.id_counter();
3759 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3760 slide
3761 .shapes_mut()
3762 .add_textbox_with_text(
3763 Inches(0.5),
3764 Inches(0.5),
3765 Inches(4.0),
3766 Inches(0.8),
3767 "layout_rid preserve test",
3768 )
3769 .expect("textbox");
3770 slide.set_layout_rid("rIdLayout999".to_string());
3772 let bytes1 = p.to_bytes().expect("to_bytes 1");
3773
3774 let p2 = Presentation::load_bytes(&bytes1).expect("load");
3776 let s = p2.slides().get(0).expect("slide 0");
3777 assert_eq!(
3778 s.sld.layout_rid(),
3779 "rIdLayout999",
3780 "to_opc_package 应保留 layout_rid 原值(round-trip 保真)"
3781 );
3782
3783 let cursor = std::io::Cursor::new(bytes1);
3785 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3786 let mut srels_xml = String::new();
3787 zip.by_name("ppt/slides/_rels/slide1.xml.rels")
3788 .expect("slide1.xml.rels exists")
3789 .read_to_string(&mut srels_xml)
3790 .expect("read slide rels");
3791 assert!(
3792 srels_xml.contains("Id=\"rIdLayout999\""),
3793 "slide1.xml.rels 中 layout 关系应使用 rIdLayout999,实际:{srels_xml}"
3794 );
3795 }
3796
3797 fn read_zip_entry(bytes: &[u8], path: &str) -> String {
3801 let cursor = std::io::Cursor::new(bytes);
3802 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3803 let mut s = String::new();
3804 match zip.by_name(path) {
3806 Ok(mut f) => {
3807 f.read_to_string(&mut s).expect("read zip entry");
3808 }
3809 Err(_) => panic!("zip entry not found: {}", path),
3810 };
3811 s
3812 }
3813
3814 fn zip_contains(bytes: &[u8], path: &str) -> bool {
3816 let cursor = std::io::Cursor::new(bytes);
3817 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
3818 let result = zip.by_name(path);
3820 result.is_ok()
3821 }
3822
3823 fn build_multi_master_presentation() -> Presentation {
3836 let mut p = Presentation::new().expect("new ok");
3837 p.slide_masters.items.clear();
3839 p.slide_layouts.items.clear();
3840 p.themes.clear();
3841 p.sld_master_ids.clear();
3842
3843 let theme1 = crate::oxml::theme::Theme {
3845 partname: "/ppt/theme/theme1.xml".to_string(),
3846 name: "Office Theme 1".to_string(),
3847 ..Default::default()
3848 };
3849 p.themes.push(theme1);
3850 let theme2 = crate::oxml::theme::Theme {
3851 partname: "/ppt/theme/theme2.xml".to_string(),
3852 name: "Office Theme 2".to_string(),
3853 ..Default::default()
3854 };
3855 p.themes.push(theme2);
3856
3857 let master1_oxml = crate::oxml::slidemaster::SldMaster {
3859 layout_rids: vec!["rIdL1".to_string(), "rIdL2".to_string()],
3860 ..Default::default()
3861 };
3862 p.slide_masters
3863 .items
3864 .push(crate::slide_masters::SlideMasterRef {
3865 idx: 0,
3866 partname: "/ppt/slideMasters/slideMaster1.xml".to_string(),
3867 rid: "rIdM1".to_string(),
3868 oxml: std::rc::Rc::new(std::cell::RefCell::new(master1_oxml)),
3869 theme_partname: "/ppt/theme/theme1.xml".to_string(),
3870 theme_rid: "rIdT1".to_string(),
3871 });
3872 let master2_oxml = crate::oxml::slidemaster::SldMaster {
3874 layout_rids: vec!["rIdL3".to_string(), "rIdL4".to_string()],
3875 ..Default::default()
3876 };
3877 p.slide_masters
3878 .items
3879 .push(crate::slide_masters::SlideMasterRef {
3880 idx: 1,
3881 partname: "/ppt/slideMasters/slideMaster2.xml".to_string(),
3882 rid: "rIdM2".to_string(),
3883 oxml: std::rc::Rc::new(std::cell::RefCell::new(master2_oxml)),
3884 theme_partname: "/ppt/theme/theme2.xml".to_string(),
3885 theme_rid: "rIdT2".to_string(),
3886 });
3887
3888 let mk_layout = |idx: usize, partname: &str, rid: &str, master_partname: &str| {
3890 crate::slide_layouts::SlideLayoutRef {
3891 idx,
3892 partname: partname.to_string(),
3893 rid: rid.to_string(),
3894 oxml: std::rc::Rc::new(std::cell::RefCell::new(
3895 crate::oxml::slidelayout::SldLayout::default(),
3896 )),
3897 master_partname: master_partname.to_string(),
3898 }
3899 };
3900 p.slide_layouts.items.push(mk_layout(
3901 0,
3902 "/ppt/slideLayouts/slideLayout1.xml",
3903 "rIdL1",
3904 "/ppt/slideMasters/slideMaster1.xml",
3905 ));
3906 p.slide_layouts.items.push(mk_layout(
3907 1,
3908 "/ppt/slideLayouts/slideLayout2.xml",
3909 "rIdL2",
3910 "/ppt/slideMasters/slideMaster1.xml",
3911 ));
3912 p.slide_layouts.items.push(mk_layout(
3913 2,
3914 "/ppt/slideLayouts/slideLayout3.xml",
3915 "rIdL3",
3916 "/ppt/slideMasters/slideMaster2.xml",
3917 ));
3918 p.slide_layouts.items.push(mk_layout(
3919 3,
3920 "/ppt/slideLayouts/slideLayout4.xml",
3921 "rIdL4",
3922 "/ppt/slideMasters/slideMaster2.xml",
3923 ));
3924
3925 p.sld_master_ids = vec![
3927 (2147483648u32, "rIdM1".to_string()),
3928 (2147483649u32, "rIdM2".to_string()),
3929 ];
3930
3931 let slide_specs = [
3933 ("rIdL1", "/ppt/slideLayouts/slideLayout1.xml"),
3934 ("rIdL3", "/ppt/slideLayouts/slideLayout3.xml"),
3935 ("rIdL2", "/ppt/slideLayouts/slideLayout2.xml"),
3936 ];
3937 for (rid, partname) in slide_specs.iter() {
3938 let counter = p.id_counter();
3939 let slide = p.slides_mut().add_slide(counter).expect("add slide");
3940 slide.set_layout_rid(rid.to_string());
3941 slide.set_layout_partname(partname.to_string());
3942 }
3943
3944 p
3945 }
3946
3947 #[test]
3949 fn single_master_round_trip_preserves_rid() {
3950 let mut p = Presentation::new().expect("new ok");
3951 let counter = p.id_counter();
3952 let _ = p.slides_mut().add_slide(counter).expect("add slide");
3953 let bytes = p.to_bytes().expect("to_bytes");
3954
3955 let p2 = Presentation::load_bytes(&bytes).expect("load");
3956 assert!(!p2.sld_master_ids.is_empty(), "应有至少 1 个 master id");
3957 let (_, rid) = &p2.sld_master_ids[0];
3958 let pres_rels = read_zip_entry(&bytes, "ppt/_rels/presentation.xml.rels");
3959 assert!(
3960 pres_rels.contains(&format!("Id=\"{}\"", rid)),
3961 "presentation.xml.rels 应包含 rid={},实际:{}",
3962 rid,
3963 pres_rels
3964 );
3965 }
3966
3967 #[test]
3969 fn single_master_round_trip_preserves_partname() {
3970 let mut p = Presentation::new().expect("new ok");
3971 let counter = p.id_counter();
3972 let _ = p.slides_mut().add_slide(counter).expect("add slide");
3973 let bytes = p.to_bytes().expect("to_bytes");
3974
3975 let p2 = Presentation::load_bytes(&bytes).expect("load");
3976 assert_eq!(
3977 p2.slide_masters.items[0].partname, "/ppt/slideMasters/slideMaster1.xml",
3978 "master partname 应保持不变"
3979 );
3980 assert_eq!(
3981 p2.slide_layouts.items[0].partname, "/ppt/slideLayouts/slideLayout1.xml",
3982 "layout partname 应保持不变"
3983 );
3984 }
3985
3986 #[test]
3988 fn multi_master_writes_all_masters() {
3989 let p = build_multi_master_presentation();
3990 let bytes = p.to_bytes().expect("to_bytes");
3991 assert!(
3992 zip_contains(&bytes, "ppt/slideMasters/slideMaster1.xml"),
3993 "应写出 slideMaster1.xml"
3994 );
3995 assert!(
3996 zip_contains(&bytes, "ppt/slideMasters/slideMaster2.xml"),
3997 "应写出 slideMaster2.xml"
3998 );
3999 }
4000
4001 #[test]
4003 fn multi_master_pres_rels_matches_sld_master_ids() {
4004 let p = build_multi_master_presentation();
4005 let bytes = p.to_bytes().expect("to_bytes");
4006 let pres_rels = read_zip_entry(&bytes, "ppt/_rels/presentation.xml.rels");
4007 assert!(
4008 pres_rels.contains("Id=\"rIdM1\""),
4009 "presentation.xml.rels 应包含 rIdM1,实际:{}",
4010 pres_rels
4011 );
4012 assert!(
4013 pres_rels.contains("Id=\"rIdM2\""),
4014 "presentation.xml.rels 应包含 rIdM2,实际:{}",
4015 pres_rels
4016 );
4017 }
4018
4019 #[test]
4021 fn multi_master_master_rels_uses_layout_rids() {
4022 let p = build_multi_master_presentation();
4023 let bytes = p.to_bytes().expect("to_bytes");
4024 let m1_rels = read_zip_entry(&bytes, "ppt/slideMasters/_rels/slideMaster1.xml.rels");
4026 assert!(
4027 m1_rels.contains("Id=\"rIdL1\""),
4028 "master1.rels 应含 rIdL1:{}",
4029 m1_rels
4030 );
4031 assert!(
4032 m1_rels.contains("Id=\"rIdL2\""),
4033 "master1.rels 应含 rIdL2:{}",
4034 m1_rels
4035 );
4036 let m2_rels = read_zip_entry(&bytes, "ppt/slideMasters/_rels/slideMaster2.xml.rels");
4038 assert!(
4039 m2_rels.contains("Id=\"rIdL3\""),
4040 "master2.rels 应含 rIdL3:{}",
4041 m2_rels
4042 );
4043 assert!(
4044 m2_rels.contains("Id=\"rIdL4\""),
4045 "master2.rels 应含 rIdL4:{}",
4046 m2_rels
4047 );
4048 }
4049
4050 #[test]
4052 fn multi_master_layout_rels_points_to_correct_master() {
4053 let p = build_multi_master_presentation();
4054 let bytes = p.to_bytes().expect("to_bytes");
4055 let l1_rels = read_zip_entry(&bytes, "ppt/slideLayouts/_rels/slideLayout1.xml.rels");
4057 assert!(
4058 l1_rels.contains("slideMaster1.xml"),
4059 "layout1.rels 应指向 slideMaster1.xml:{}",
4060 l1_rels
4061 );
4062 let l3_rels = read_zip_entry(&bytes, "ppt/slideLayouts/_rels/slideLayout3.xml.rels");
4064 assert!(
4065 l3_rels.contains("slideMaster2.xml"),
4066 "layout3.rels 应指向 slideMaster2.xml:{}",
4067 l3_rels
4068 );
4069 }
4070
4071 #[test]
4073 fn slide_layout_rel_uses_real_partname() {
4074 let p = build_multi_master_presentation();
4075 let bytes = p.to_bytes().expect("to_bytes");
4076 let s2_rels = read_zip_entry(&bytes, "ppt/slides/_rels/slide2.xml.rels");
4078 assert!(
4079 s2_rels.contains("slideLayout3.xml"),
4080 "slide2.rels 应指向 slideLayout3.xml:{}",
4081 s2_rels
4082 );
4083 }
4084
4085 #[test]
4087 fn sldmaster_to_xml_uses_layout_rids() {
4088 let m = crate::oxml::slidemaster::SldMaster {
4089 layout_rids: vec!["rIdA".to_string(), "rIdB".to_string()],
4090 ..Default::default()
4091 };
4092 let xml = m.to_xml();
4093 assert!(
4094 xml.contains("r:id=\"rIdA\""),
4095 "sldLayoutIdLst 应含 r:id=\"rIdA\":{}",
4096 xml
4097 );
4098 assert!(
4099 xml.contains("r:id=\"rIdB\""),
4100 "sldLayoutIdLst 应含 r:id=\"rIdB\":{}",
4101 xml
4102 );
4103 }
4104
4105 #[test]
4107 fn field_level_round_trip_master_partname() {
4108 let p = build_multi_master_presentation();
4109 let before_m1 = p.slide_masters.items[0].partname.clone();
4110 let before_m2 = p.slide_masters.items[1].partname.clone();
4111 let bytes = p.to_bytes().expect("to_bytes");
4112 let p2 = Presentation::load_bytes(&bytes).expect("load");
4113 assert_eq!(
4114 p2.slide_masters.items[0].partname, before_m1,
4115 "master1 partname round-trip 后应保持"
4116 );
4117 assert_eq!(
4118 p2.slide_masters.items[1].partname, before_m2,
4119 "master2 partname round-trip 后应保持"
4120 );
4121 }
4122
4123 #[test]
4125 fn field_level_round_trip_layout_count() {
4126 let p = build_multi_master_presentation();
4127 let before = p.slide_layouts.items.len();
4128 let bytes = p.to_bytes().expect("to_bytes");
4129 let p2 = Presentation::load_bytes(&bytes).expect("load");
4130 assert_eq!(
4131 p2.slide_layouts.items.len(),
4132 before,
4133 "layout 数量 round-trip 后应保持为 {},实际 {}",
4134 before,
4135 p2.slide_layouts.items.len()
4136 );
4137 }
4138
4139 #[test]
4141 fn field_level_round_trip_theme_count() {
4142 let p = build_multi_master_presentation();
4143 let before = p.themes.len();
4144 let bytes = p.to_bytes().expect("to_bytes");
4145 let p2 = Presentation::load_bytes(&bytes).expect("load");
4146 assert_eq!(
4147 p2.themes.len(),
4148 before,
4149 "theme 数量 round-trip 后应保持为 {},实际 {}",
4150 before,
4151 p2.themes.len()
4152 );
4153 }
4154
4155 #[test]
4159 fn custom_properties_set_get_remove() {
4160 let mut props = CustomProperties::new();
4161 assert!(props.is_empty());
4162 assert_eq!(props.len(), 0);
4163
4164 props.set("Project", CustomPropertyValue::Text("Demo".to_string()));
4165 props.set("Version", CustomPropertyValue::Int(42));
4166 props.set("Active", CustomPropertyValue::Bool(true));
4167 assert_eq!(props.len(), 3);
4168
4169 match props.get("Project") {
4171 Some(CustomPropertyValue::Text(s)) => assert_eq!(s, "Demo"),
4172 other => panic!("期望 Text,得到 {:?}", other),
4173 }
4174 match props.get("Version") {
4175 Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 42),
4176 other => panic!("期望 Int,得到 {:?}", other),
4177 }
4178 assert!(props.get("NonExistent").is_none());
4179
4180 props.set("Version", CustomPropertyValue::Int(100));
4182 match props.get("Version") {
4183 Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 100),
4184 other => panic!("期望 Int(100),得到 {:?}", other),
4185 }
4186 assert_eq!(props.len(), 3, "覆盖不应增加条目数");
4187
4188 let removed = props.remove("Active");
4190 assert!(matches!(removed, Some(CustomPropertyValue::Bool(true))));
4191 assert_eq!(props.len(), 2);
4192 assert!(props.get("Active").is_none());
4193 }
4194
4195 #[test]
4197 fn custom_properties_to_xml() {
4198 let mut props = CustomProperties::new();
4199 props.set(
4200 "Text",
4201 CustomPropertyValue::Text("Hello & World".to_string()),
4202 );
4203 props.set("Int", CustomPropertyValue::Int(42));
4204 props.set("Float", CustomPropertyValue::Float(3.15));
4206 props.set("Bool", CustomPropertyValue::Bool(true));
4207
4208 let xml = props.to_xml();
4209 assert!(xml.contains("<Properties"));
4210 assert!(xml.contains("custom-properties"));
4211 assert!(xml.contains("Hello & World"));
4213 assert!(xml.contains("pid=\"2\""));
4215 assert!(xml.contains("pid=\"3\""));
4216 assert!(xml.contains("<vt:lpwstr>Hello & World</vt:lpwstr>"));
4218 assert!(xml.contains("<vt:i4>42</vt:i4>"));
4219 assert!(xml.contains("<vt:r8>3.15</vt:r8>"));
4220 assert!(xml.contains("<vt:bool>true</vt:bool>"));
4221 assert!(xml.contains("fmtid=\"{D5CDD505-2E9C-101B-9397-08002B2CF9AE}\""));
4223 }
4224
4225 #[test]
4227 fn custom_properties_from_xml() {
4228 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4229<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
4230 xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
4231 <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="2" name="Text">
4232 <vt:lpwstr>Value</vt:lpwstr>
4233 </property>
4234 <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="3" name="Int">
4235 <vt:i4>42</vt:i4>
4236 </property>
4237 <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="4" name="Bool">
4238 <vt:bool>false</vt:bool>
4239 </property>
4240 <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="5" name="Float">
4241 <vt:r8>3.15</vt:r8>
4242 </property>
4243</Properties>"#;
4244 let props = CustomProperties::from_xml(xml);
4245 assert_eq!(props.len(), 4);
4246 match props.get("Text") {
4247 Some(CustomPropertyValue::Text(s)) => assert_eq!(s, "Value"),
4248 other => panic!("期望 Text,得到 {:?}", other),
4249 }
4250 match props.get("Int") {
4251 Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 42),
4252 other => panic!("期望 Int,得到 {:?}", other),
4253 }
4254 match props.get("Bool") {
4255 Some(CustomPropertyValue::Bool(b)) => assert!(!*b),
4256 other => panic!("期望 Bool(false),得到 {:?}", other),
4257 }
4258 match props.get("Float") {
4259 Some(CustomPropertyValue::Float(f)) => assert!((f - 3.15).abs() < 1e-6),
4260 other => panic!("期望 Float,得到 {:?}", other),
4261 }
4262 }
4263
4264 #[test]
4266 fn custom_properties_round_trip() {
4267 let mut props = CustomProperties::new();
4268 props.set("Key1", CustomPropertyValue::Text("Value1".to_string()));
4269 props.set("Key2", CustomPropertyValue::Int(123));
4270 props.set("Key3", CustomPropertyValue::Bool(true));
4271
4272 let xml = props.to_xml();
4273 let parsed = CustomProperties::from_xml(&xml);
4274 assert_eq!(parsed.len(), 3);
4275 assert_eq!(props.get("Key1"), parsed.get("Key1"));
4276 assert_eq!(props.get("Key2"), parsed.get("Key2"));
4277 assert_eq!(props.get("Key3"), parsed.get("Key3"));
4278 }
4279
4280 #[test]
4282 fn custom_properties_empty_not_serialized() {
4283 let p = Presentation::new().expect("new ok");
4284 let bytes = p.to_bytes().expect("to_bytes");
4285 let cursor = std::io::Cursor::new(bytes);
4286 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
4287 assert!(zip.by_name("docProps/custom.xml").is_err());
4289 }
4290
4291 #[test]
4293 fn custom_properties_serialized_to_pptx() {
4294 let mut p = Presentation::new().expect("new ok");
4295 p.custom_properties_mut()
4296 .set("Project", CustomPropertyValue::Text("Test".to_string()));
4297 p.custom_properties_mut()
4298 .set("Version", CustomPropertyValue::Int(1));
4299
4300 let bytes = p.to_bytes().expect("to_bytes");
4301 let cursor = std::io::Cursor::new(bytes);
4302 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
4303
4304 let mut custom_xml = String::new();
4306 zip.by_name("docProps/custom.xml")
4307 .expect("custom.xml 应存在")
4308 .read_to_string(&mut custom_xml)
4309 .expect("read custom.xml");
4310 assert!(custom_xml.contains("name=\"Project\""));
4311 assert!(custom_xml.contains("<vt:lpwstr>Test</vt:lpwstr>"));
4312 assert!(custom_xml.contains("name=\"Version\""));
4313 assert!(custom_xml.contains("<vt:i4>1</vt:i4>"));
4314
4315 let mut rels_xml = String::new();
4317 zip.by_name("_rels/.rels")
4318 .expect(".rels 应存在")
4319 .read_to_string(&mut rels_xml)
4320 .expect("read .rels");
4321 assert!(rels_xml.contains("custom-properties"), "rels: {}", rels_xml);
4322 assert!(
4323 rels_xml.contains("docProps/custom.xml"),
4324 "rels: {}",
4325 rels_xml
4326 );
4327 }
4328
4329 #[test]
4331 fn custom_properties_round_trip_through_pptx() {
4332 let mut p = Presentation::new().expect("new ok");
4333 p.custom_properties_mut()
4334 .set("Author", CustomPropertyValue::Text("TestUser".to_string()));
4335 p.custom_properties_mut()
4336 .set("Count", CustomPropertyValue::Int(99));
4337
4338 let bytes = p.to_bytes().expect("to_bytes");
4339 let p2 = Presentation::load_bytes(&bytes).expect("load");
4340
4341 assert_eq!(p2.custom_properties().len(), 2);
4342 match p2.custom_properties().get("Author") {
4343 Some(CustomPropertyValue::Text(s)) => assert_eq!(s, "TestUser"),
4344 other => panic!("期望 Text,得到 {:?}", other),
4345 }
4346 match p2.custom_properties().get("Count") {
4347 Some(CustomPropertyValue::Int(i)) => assert_eq!(*i, 99),
4348 other => panic!("期望 Int,得到 {:?}", other),
4349 }
4350 }
4351
4352 #[test]
4356 fn slide_add_comment_basic() {
4357 let mut p = Presentation::new().expect("new ok");
4358 let counter = p.id_counter();
4359 let author_id = p.comment_authors_mut().get_or_insert_id("张三", "ZS");
4360 let slide = p.slides_mut().add_slide(counter).expect("add slide");
4361 let idx = slide.add_comment(
4362 author_id,
4363 crate::Emu::new(914400),
4364 crate::Emu::new(914400),
4365 "测试评论",
4366 );
4367 assert_eq!(idx, 1);
4368 let comments = slide.comments().expect("comments should exist");
4369 assert_eq!(comments.len(), 1);
4370 assert_eq!(comments.comments[0].text, "测试评论");
4371 assert_eq!(comments.comments[0].author_id, author_id);
4372 assert_eq!(comments.comments[0].idx, 1);
4373 assert_eq!(comments.comments[0].pos_x, 914400);
4374 assert_eq!(comments.comments[0].pos_y, 914400);
4375 }
4376
4377 #[test]
4379 fn slide_add_comment_multiple_idx_increments() {
4380 let mut p = Presentation::new().expect("new ok");
4381 let counter = p.id_counter();
4382 let author_id = p.comment_authors_mut().get_or_insert_id("作者", "ZZ");
4383 let slide = p.slides_mut().add_slide(counter).expect("add slide");
4384 let idx1 = slide.add_comment(author_id, 0, 0, "第一条");
4385 let idx2 = slide.add_comment(author_id, 100, 100, "第二条");
4386 let idx3 = slide.add_comment(author_id, 200, 200, "第三条");
4387 assert_eq!(idx1, 1);
4388 assert_eq!(idx2, 2);
4389 assert_eq!(idx3, 3);
4390 assert_eq!(slide.comments().unwrap().len(), 3);
4391 }
4392
4393 #[test]
4395 fn slide_clear_comments() {
4396 let mut p = Presentation::new().expect("new ok");
4397 let counter = p.id_counter();
4398 let author_id = p.comment_authors_mut().get_or_insert_id("A", "A");
4399 let slide = p.slides_mut().add_slide(counter).expect("add slide");
4400 slide.add_comment(author_id, 0, 0, "评论");
4401 assert!(slide.comments().is_some());
4402 slide.clear_comments();
4403 assert!(slide.comments().is_none());
4404 }
4405
4406 #[test]
4408 fn comments_serialized_to_pptx() {
4409 let mut p = Presentation::new().expect("new ok");
4410 let counter = p.id_counter();
4411 let author_id = p.comment_authors_mut().get_or_insert_id("张三", "ZS");
4412 let slide = p.slides_mut().add_slide(counter).expect("add slide");
4413 slide.add_comment(
4414 author_id,
4415 crate::Emu::new(914400),
4416 crate::Emu::new(914400),
4417 "PPTX 评论",
4418 );
4419
4420 let bytes = p.to_bytes().expect("to_bytes");
4421 let cursor = std::io::Cursor::new(bytes);
4422 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
4423
4424 let mut cxml = String::new();
4426 zip.by_name("ppt/comments/comment1.xml")
4427 .expect("comment1.xml 应存在")
4428 .read_to_string(&mut cxml)
4429 .expect("read comment1.xml");
4430 assert!(cxml.contains("<p:cmLst"), "comment xml: {}", cxml);
4431 assert!(cxml.contains("PPTX 评论"), "comment text: {}", cxml);
4432 assert!(cxml.contains("authorId=\""), "comment authorId: {}", cxml);
4433
4434 let mut axml = String::new();
4436 zip.by_name("ppt/commentAuthors.xml")
4437 .expect("commentAuthors.xml 应存在")
4438 .read_to_string(&mut axml)
4439 .expect("read commentAuthors.xml");
4440 assert!(axml.contains("<p:cmAuthorLst"), "authors xml: {}", axml);
4441 assert!(axml.contains("张三"), "author name: {}", axml);
4442
4443 let mut srels = String::new();
4445 zip.by_name("ppt/slides/_rels/slide1.xml.rels")
4446 .expect("slide1.xml.rels exists")
4447 .read_to_string(&mut srels)
4448 .expect("read slide rels");
4449 assert!(
4450 srels.contains("comments"),
4451 "slide rels should contain comments: {}",
4452 srels
4453 );
4454 assert!(
4455 srels.contains("../comments/comment1.xml"),
4456 "slide rels target: {}",
4457 srels
4458 );
4459
4460 let mut prels = String::new();
4462 zip.by_name("ppt/_rels/presentation.xml.rels")
4463 .expect("pres rels exists")
4464 .read_to_string(&mut prels)
4465 .expect("read pres rels");
4466 assert!(prels.contains("commentAuthors"), "pres rels: {}", prels);
4467 }
4468
4469 #[test]
4471 fn comments_round_trip_through_pptx() {
4472 let mut p = Presentation::new().expect("new ok");
4473 let counter = p.id_counter();
4474 let author_id = p.comment_authors_mut().get_or_insert_id("李四", "LS");
4475 let slide = p.slides_mut().add_slide(counter).expect("add slide");
4476 slide.add_comment(
4477 author_id,
4478 crate::Emu::new(100),
4479 crate::Emu::new(200),
4480 "往返评论",
4481 );
4482
4483 let bytes = p.to_bytes().expect("to_bytes");
4484 let p2 = Presentation::load_bytes(&bytes).expect("load");
4485
4486 let s2 = p2.slides().get(0).expect("slide 0");
4488 let comments = s2.sld.comments().expect("comments should exist");
4489 assert_eq!(comments.len(), 1);
4490 let c = &comments.comments[0];
4491 assert_eq!(c.text, "往返评论");
4492 assert_eq!(c.pos_x, 100);
4493 assert_eq!(c.pos_y, 200);
4494 assert_eq!(c.idx, 1);
4495
4496 let authors = p2.comment_authors();
4498 assert_eq!(authors.len(), 1);
4499 assert_eq!(authors.authors[0].name, "李四");
4500 assert_eq!(authors.authors[0].initials, "LS");
4501 assert_eq!(authors.authors[0].id, c.author_id);
4502 }
4503
4504 #[test]
4506 fn no_comments_no_part() {
4507 let mut p = Presentation::new().expect("new ok");
4508 let counter = p.id_counter();
4509 p.slides_mut().add_slide(counter).expect("add slide");
4510
4511 let bytes = p.to_bytes().expect("to_bytes");
4512 let cursor = std::io::Cursor::new(bytes);
4513 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
4514 assert!(zip.by_name("ppt/comments/comment1.xml").is_err());
4516 assert!(zip.by_name("ppt/commentAuthors.xml").is_err());
4518 }
4519
4520 #[test]
4522 fn comments_multiple_slides_partname_stable() {
4523 let mut p = Presentation::new().expect("new ok");
4524 let counter = p.id_counter();
4525 let aid = p.comment_authors_mut().get_or_insert_id("A", "A");
4526 let s1 = p.slides_mut().add_slide(counter.clone()).expect("slide 1");
4527 s1.add_comment(aid, 0, 0, "slide1 评论");
4528
4529 let s2 = p.slides_mut().add_slide(counter).expect("slide 2");
4530 s2.add_comment(aid, 0, 0, "slide2 评论");
4531
4532 let bytes = p.to_bytes().expect("to_bytes");
4533 let p2 = Presentation::load_bytes(&bytes).expect("load");
4534
4535 let s1b = p2.slides().get(0).expect("slide 0");
4537 let s2b = p2.slides().get(1).expect("slide 1");
4538 assert_eq!(s1b.sld.comments().unwrap().comments[0].text, "slide1 评论");
4539 assert_eq!(s2b.sld.comments().unwrap().comments[0].text, "slide2 评论");
4540
4541 let bytes2 = p2.to_bytes().expect("to_bytes 2");
4543 let cursor = std::io::Cursor::new(bytes2);
4544 let mut zip = zip::ZipArchive::new(cursor).expect("zip 2");
4545 assert!(zip.by_name("ppt/comments/comment1.xml").is_ok());
4546 assert!(zip.by_name("ppt/comments/comment2.xml").is_ok());
4547 }
4548
4549 #[test]
4553 fn slide_allocate_diagram_rids_increments() {
4554 let mut p = Presentation::new().expect("new ok");
4555 let counter = p.id_counter();
4556 let slide = p.slides_mut().add_slide(counter).expect("add slide");
4557
4558 let (d1, l1, q1, c1) = slide.allocate_diagram_rids();
4559 assert_eq!(d1, "rIdDgmData1");
4560 assert_eq!(l1, "rIdDgmLayout1");
4561 assert_eq!(q1, "rIdDgmQs1");
4562 assert_eq!(c1, "rIdDgmColors1");
4563
4564 let (d2, l2, q2, c2) = slide.allocate_diagram_rids();
4565 assert_eq!(d2, "rIdDgmData2");
4566 assert_eq!(l2, "rIdDgmLayout2");
4567 assert_eq!(q2, "rIdDgmQs2");
4568 assert_eq!(c2, "rIdDgmColors2");
4569 }
4570
4571 #[test]
4573 fn slide_next_diagram_index_increments() {
4574 let mut p = Presentation::new().expect("new ok");
4575 let counter = p.id_counter();
4576 let slide = p.slides_mut().add_slide(counter).expect("add slide");
4577
4578 assert_eq!(slide.next_diagram_index(), 1);
4579 assert_eq!(slide.next_diagram_index(), 2);
4580 assert_eq!(slide.next_diagram_index(), 3);
4581 }
4582
4583 #[test]
4585 fn slide_register_diagram_stores_entry() {
4586 let mut p = Presentation::new().expect("new ok");
4587 let counter = p.id_counter();
4588 let slide = p.slides_mut().add_slide(counter).expect("add slide");
4589
4590 let entry = DiagramEntry {
4591 data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
4592 layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
4593 quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
4594 colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
4595 data_xml: "<dgm:dataModel/>".to_string(),
4596 layout_xml: "<dgm:layoutDef/>".to_string(),
4597 quick_style_xml: "<dgm:styleData/>".to_string(),
4598 colors_xml: "<dgm:colorsDef/>".to_string(),
4599 data_rid: "rIdDgmData1".to_string(),
4600 layout_rid: "rIdDgmLayout1".to_string(),
4601 quick_style_rid: "rIdDgmQs1".to_string(),
4602 colors_rid: "rIdDgmColors1".to_string(),
4603 };
4604 slide.register_diagram(entry);
4605
4606 assert_eq!(slide.diagram_entries.len(), 1);
4608 assert_eq!(
4609 slide.diagram_entries[0].data_partname.as_str(),
4610 "/ppt/diagrams/data1.xml"
4611 );
4612 assert_eq!(slide.diagram_entries[0].data_xml, "<dgm:dataModel/>");
4613 }
4614
4615 #[test]
4618 fn diagram_parts_written_to_zip_after_register() {
4619 let mut p = Presentation::new().expect("new ok");
4620 let counter = p.id_counter();
4621 let slide = p.slides_mut().add_slide(counter).expect("add slide");
4622
4623 let entry = DiagramEntry {
4624 data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
4625 layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
4626 quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
4627 colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
4628 data_xml: "<?xml version=\"1.0\"?><dgm:dataModel xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
4629 layout_xml: "<?xml version=\"1.0\"?><dgm:layoutDef xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
4630 quick_style_xml: "<?xml version=\"1.0\"?><dgm:styleData xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
4631 colors_xml: "<?xml version=\"1.0\"?><dgm:colorsDef xmlns:dgm=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\"/>".to_string(),
4632 data_rid: "rIdDgmData1".to_string(),
4633 layout_rid: "rIdDgmLayout1".to_string(),
4634 quick_style_rid: "rIdDgmQs1".to_string(),
4635 colors_rid: "rIdDgmColors1".to_string(),
4636 };
4637 slide.register_diagram(entry);
4638
4639 let bytes = p.to_bytes().expect("to_bytes");
4640 let cursor = std::io::Cursor::new(bytes);
4641 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
4642
4643 assert!(
4645 zip.by_name("ppt/diagrams/data1.xml").is_ok(),
4646 "data1.xml missing"
4647 );
4648 assert!(
4649 zip.by_name("ppt/diagrams/layout1.xml").is_ok(),
4650 "layout1.xml missing"
4651 );
4652 assert!(
4653 zip.by_name("ppt/diagrams/quickStyles1.xml").is_ok(),
4654 "quickStyles1.xml missing"
4655 );
4656 assert!(
4657 zip.by_name("ppt/diagrams/colors1.xml").is_ok(),
4658 "colors1.xml missing"
4659 );
4660
4661 let mut srels_xml = String::new();
4663 zip.by_name("ppt/slides/_rels/slide1.xml.rels")
4664 .expect("slide1.xml.rels exists")
4665 .read_to_string(&mut srels_xml)
4666 .expect("read slide rels");
4667 assert!(
4668 srels_xml.contains("/diagramData"),
4669 "slide1.xml.rels 缺 diagramData 关系,实际:{srels_xml}"
4670 );
4671 assert!(
4672 srels_xml.contains("/diagramLayout"),
4673 "slide1.xml.rels 缺 diagramLayout 关系,实际:{srels_xml}"
4674 );
4675 assert!(
4676 srels_xml.contains("/diagramQuickStyle"),
4677 "slide1.xml.rels 缺 diagramQuickStyle 关系,实际:{srels_xml}"
4678 );
4679 assert!(
4680 srels_xml.contains("/diagramColors"),
4681 "slide1.xml.rels 缺 diagramColors 关系,实际:{srels_xml}"
4682 );
4683 assert!(srels_xml.contains("rIdDgmData1"), "缺 rIdDgmData1");
4685 assert!(srels_xml.contains("rIdDgmLayout1"), "缺 rIdDgmLayout1");
4686 assert!(srels_xml.contains("rIdDgmQs1"), "缺 rIdDgmQs1");
4687 assert!(srels_xml.contains("rIdDgmColors1"), "缺 rIdDgmColors1");
4688 }
4689
4690 #[test]
4692 fn diagram_parts_global_index_across_slides() {
4693 let mut p = Presentation::new().expect("new ok");
4694 let counter = p.id_counter();
4695
4696 let s1 = p.slides_mut().add_slide(counter.clone()).expect("s1");
4698 let e1 = DiagramEntry {
4699 data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
4700 layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
4701 quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
4702 colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
4703 data_xml: "<dgm:dataModel/>".to_string(),
4704 layout_xml: "<dgm:layoutDef/>".to_string(),
4705 quick_style_xml: "<dgm:styleData/>".to_string(),
4706 colors_xml: "<dgm:colorsDef/>".to_string(),
4707 data_rid: "rIdDgmData1".to_string(),
4708 layout_rid: "rIdDgmLayout1".to_string(),
4709 quick_style_rid: "rIdDgmQs1".to_string(),
4710 colors_rid: "rIdDgmColors1".to_string(),
4711 };
4712 s1.register_diagram(e1);
4713
4714 let s2 = p.slides_mut().add_slide(counter).expect("s2");
4716 let e2 = DiagramEntry {
4717 data_partname: crate::opc::part::new_part_name("/ppt/diagrams/data1.xml"),
4718 layout_partname: crate::opc::part::new_part_name("/ppt/diagrams/layout1.xml"),
4719 quick_style_partname: crate::opc::part::new_part_name("/ppt/diagrams/quickStyles1.xml"),
4720 colors_partname: crate::opc::part::new_part_name("/ppt/diagrams/colors1.xml"),
4721 data_xml: "<dgm:dataModel/>".to_string(),
4722 layout_xml: "<dgm:layoutDef/>".to_string(),
4723 quick_style_xml: "<dgm:styleData/>".to_string(),
4724 colors_xml: "<dgm:colorsDef/>".to_string(),
4725 data_rid: "rIdDgmData1".to_string(),
4726 layout_rid: "rIdDgmLayout1".to_string(),
4727 quick_style_rid: "rIdDgmQs1".to_string(),
4728 colors_rid: "rIdDgmColors1".to_string(),
4729 };
4730 s2.register_diagram(e2);
4731
4732 let bytes = p.to_bytes().expect("to_bytes");
4733 let cursor = std::io::Cursor::new(bytes);
4734 let mut zip = zip::ZipArchive::new(cursor).expect("zip open");
4735
4736 assert!(zip.by_name("ppt/diagrams/data1.xml").is_ok());
4738 assert!(zip.by_name("ppt/diagrams/layout1.xml").is_ok());
4739 assert!(
4742 zip.by_name("ppt/diagrams/data2.xml").is_ok(),
4743 "data2.xml missing"
4744 );
4745 assert!(
4746 zip.by_name("ppt/diagrams/layout2.xml").is_ok(),
4747 "layout2.xml missing"
4748 );
4749 assert!(zip.by_name("ppt/diagrams/quickStyles2.xml").is_ok());
4750 assert!(zip.by_name("ppt/diagrams/colors2.xml").is_ok());
4751 }
4752}