1use crate::oxml::ns::NS_DRAWING_MAIN;
19use crate::oxml::writer::XmlWriter;
20
21#[allow(dead_code)]
22const THEME_NAME: &str = "Office Theme";
23
24#[derive(Clone, Debug, Default)]
29pub struct Theme {
30 pub name: String,
32 pub color_scheme: ColorScheme,
34 pub font_scheme: FontScheme,
36 pub format_scheme: FormatScheme,
38 pub partname: String,
42}
43
44#[derive(Clone, Debug, Default)]
48pub struct ColorScheme {
49 pub name: String,
51 pub dk1: Option<ThemeColor>,
53 pub lt1: Option<ThemeColor>,
55 pub dk2: Option<ThemeColor>,
57 pub lt2: Option<ThemeColor>,
59 pub accent1: Option<ThemeColor>,
61 pub accent2: Option<ThemeColor>,
62 pub accent3: Option<ThemeColor>,
63 pub accent4: Option<ThemeColor>,
64 pub accent5: Option<ThemeColor>,
65 pub accent6: Option<ThemeColor>,
66 pub hlink: Option<ThemeColor>,
68 pub fol_hlink: Option<ThemeColor>,
70}
71
72#[derive(Clone, Debug, Default)]
76pub struct FontScheme {
77 pub name: String,
79 pub major_latin: String,
81 pub major_ea: String,
83 pub major_cs: String,
85 pub minor_latin: String,
87 pub minor_ea: String,
89 pub minor_cs: String,
91}
92
93#[derive(Clone, Debug, Default)]
105pub struct FormatScheme {
106 pub name: String,
108 pub raw_xml: String,
112 pub fill_styles: Vec<String>,
116 pub line_styles: Vec<String>,
120 pub effect_styles: Vec<String>,
124 pub bg_fill_styles: Vec<String>,
128}
129
130#[derive(Clone, Debug, Default, PartialEq, Eq)]
132pub enum ThemeColor {
133 Srgb(String),
135 Sys(String, String),
137 #[default]
139 None,
140}
141
142impl ThemeColor {
143 pub fn write_xml(&self, w: &mut XmlWriter) {
151 match self {
152 ThemeColor::Srgb(val) => {
153 w.empty_with("a:srgbClr", &[("val", val.as_str())]);
154 }
155 ThemeColor::Sys(val, last_clr) => {
156 w.empty_with(
157 "a:sysClr",
158 &[("val", val.as_str()), ("lastClr", last_clr.as_str())],
159 );
160 }
161 ThemeColor::None => { }
162 }
163 }
164}
165
166impl ColorScheme {
167 pub fn write_xml(&self, w: &mut XmlWriter) {
174 let name = if self.name.is_empty() {
175 "Office"
176 } else {
177 self.name.as_str()
178 };
179 w.open_with("a:clrScheme", &[("name", name)]);
180 self.write_slot(w, "a:dk1", &self.dk1);
182 self.write_slot(w, "a:lt1", &self.lt1);
183 self.write_slot(w, "a:dk2", &self.dk2);
184 self.write_slot(w, "a:lt2", &self.lt2);
185 self.write_slot(w, "a:accent1", &self.accent1);
186 self.write_slot(w, "a:accent2", &self.accent2);
187 self.write_slot(w, "a:accent3", &self.accent3);
188 self.write_slot(w, "a:accent4", &self.accent4);
189 self.write_slot(w, "a:accent5", &self.accent5);
190 self.write_slot(w, "a:accent6", &self.accent6);
191 self.write_slot(w, "a:hlink", &self.hlink);
192 self.write_slot(w, "a:folHlink", &self.fol_hlink);
193 w.close("a:clrScheme");
194 }
195
196 fn write_slot(&self, w: &mut XmlWriter, tag: &str, color: &Option<ThemeColor>) {
198 w.open(tag);
199 if let Some(c) = color {
200 c.write_xml(w);
201 }
202 w.close(tag);
203 }
204}
205
206impl FontScheme {
207 pub fn write_xml(&self, w: &mut XmlWriter) {
226 let name = if self.name.is_empty() {
227 "Office"
228 } else {
229 self.name.as_str()
230 };
231 w.open_with("a:fontScheme", &[("name", name)]);
232 w.open("a:majorFont");
234 w.empty_with("a:latin", &[("typeface", self.major_latin.as_str())]);
235 w.empty_with("a:ea", &[("typeface", self.major_ea.as_str())]);
236 w.empty_with("a:cs", &[("typeface", self.major_cs.as_str())]);
237 w.close("a:majorFont");
238 w.open("a:minorFont");
240 w.empty_with("a:latin", &[("typeface", self.minor_latin.as_str())]);
241 w.empty_with("a:ea", &[("typeface", self.minor_ea.as_str())]);
242 w.empty_with("a:cs", &[("typeface", self.minor_cs.as_str())]);
243 w.close("a:minorFont");
244 w.close("a:fontScheme");
245 }
246}
247
248impl FormatScheme {
249 pub fn write_xml(&self, w: &mut XmlWriter) {
254 let name = if self.name.is_empty() {
255 "Office"
256 } else {
257 self.name.as_str()
258 };
259 let has_structured = !self.fill_styles.is_empty()
261 || !self.line_styles.is_empty()
262 || !self.effect_styles.is_empty()
263 || !self.bg_fill_styles.is_empty();
264
265 if has_structured {
266 w.open_with("a:fmtScheme", &[("name", name)]);
268 if !self.fill_styles.is_empty() {
269 w.open("a:fillStyleLst");
270 for s in &self.fill_styles {
271 w.raw(s);
272 }
273 w.close("a:fillStyleLst");
274 }
275 if !self.line_styles.is_empty() {
276 w.open("a:lnStyleLst");
277 for s in &self.line_styles {
278 w.raw(s);
279 }
280 w.close("a:lnStyleLst");
281 }
282 if !self.effect_styles.is_empty() {
283 w.open("a:effectStyleLst");
284 for s in &self.effect_styles {
285 w.raw(s);
286 }
287 w.close("a:effectStyleLst");
288 }
289 if !self.bg_fill_styles.is_empty() {
290 w.open("a:bgFillStyleLst");
291 for s in &self.bg_fill_styles {
292 w.raw(s);
293 }
294 w.close("a:bgFillStyleLst");
295 }
296 w.close("a:fmtScheme");
297 } else if self.raw_xml.is_empty() {
298 w.raw(DEFAULT_FMT_SCHEME);
300 } else {
301 w.open_with("a:fmtScheme", &[("name", name)]);
303 w.raw(&self.raw_xml);
304 w.close("a:fmtScheme");
305 }
306 }
307
308 pub fn parse_from_raw_xml(&mut self) {
318 if self.raw_xml.is_empty() {
319 return;
320 }
321 self.fill_styles = collect_style_lst_children(&self.raw_xml, "fillStyleLst");
322 self.line_styles = collect_style_lst_children(&self.raw_xml, "lnStyleLst");
323 self.effect_styles = collect_style_lst_children(&self.raw_xml, "effectStyleLst");
324 self.bg_fill_styles = collect_style_lst_children(&self.raw_xml, "bgFillStyleLst");
325 }
326
327 pub fn fill_style_count(&self) -> usize {
329 self.fill_styles.len()
330 }
331
332 pub fn line_style_count(&self) -> usize {
334 self.line_styles.len()
335 }
336
337 pub fn effect_style_count(&self) -> usize {
339 self.effect_styles.len()
340 }
341
342 pub fn bg_fill_style_count(&self) -> usize {
344 self.bg_fill_styles.len()
345 }
346}
347
348fn collect_style_lst_children(xml: &str, container_local: &str) -> Vec<String> {
358 let mut result = Vec::new();
359 let mut rd = quick_xml::Reader::from_str(xml);
360 rd.config_mut().trim_text(true);
361 let mut buf = Vec::new();
362
363 enum State {
365 Seeking,
366 InContainer,
367 }
368 let mut state = State::Seeking;
369
370 loop {
371 match rd.read_event_into(&mut buf) {
372 Ok(quick_xml::events::Event::Start(e)) => {
373 let name = e.name();
375 let local = local_name_quick(name.as_ref());
376 match state {
377 State::Seeking => {
378 if local == container_local.as_bytes() {
379 state = State::InContainer;
380 }
381 }
382 State::InContainer => {
383 if let Ok(inner) = collect_full_element_str(&mut rd, e.into_owned()) {
385 result.push(inner);
386 }
387 }
388 }
389 }
390 Ok(quick_xml::events::Event::End(e)) => {
391 let name = e.name();
392 let local = local_name_quick(name.as_ref());
393 if let State::InContainer = state {
394 if local == container_local.as_bytes() {
395 state = State::Seeking;
396 }
397 }
398 }
399 Ok(quick_xml::events::Event::Empty(e)) => {
400 let name = e.name();
401 let local = local_name_quick(name.as_ref());
402 if let State::InContainer = state {
403 let _ = local; let mut s = String::new();
407 s.push('<');
408 s.push_str(std::str::from_utf8(name.as_ref()).unwrap_or(""));
409 for a in e.attributes().flatten() {
410 s.push(' ');
411 s.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
412 s.push_str("=\"");
413 s.push_str(
414 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
415 .unwrap_or_default()
416 .as_ref(),
417 );
418 s.push('"');
419 }
420 s.push_str("/>");
421 result.push(s);
422 }
423 }
424 Ok(quick_xml::events::Event::Eof) => break,
425 Err(_) => break,
426 _ => {}
427 }
428 buf.clear();
429 }
430 result
431}
432
433fn local_name_quick(name: &[u8]) -> &[u8] {
435 match name.iter().position(|&b| b == b':') {
437 Some(pos) => &name[pos + 1..],
438 None => name,
439 }
440}
441
442fn collect_full_element_str(
446 rd: &mut quick_xml::Reader<&[u8]>,
447 start: quick_xml::events::BytesStart<'_>,
448) -> quick_xml::Result<String> {
449 let mut result = String::new();
450 let start_name = start.name();
453 let name = std::str::from_utf8(start_name.as_ref()).unwrap_or("");
454 result.push('<');
455 result.push_str(name);
456 for a in start.attributes().flatten() {
457 result.push(' ');
458 result.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
459 result.push_str("=\"");
460 result.push_str(
461 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
462 .unwrap_or_default()
463 .as_ref(),
464 );
465 result.push('"');
466 }
467 result.push('>');
468
469 let target = start.name();
470 let mut depth = 1;
471 let mut buf = Vec::new();
472 loop {
473 match rd.read_event_into(&mut buf) {
474 Ok(quick_xml::events::Event::Start(e)) => {
475 let e_name = e.name();
477 let n = std::str::from_utf8(e_name.as_ref()).unwrap_or("");
478 result.push('<');
479 result.push_str(n);
480 for a in e.attributes().flatten() {
481 result.push(' ');
482 result.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
483 result.push_str("=\"");
484 result.push_str(
485 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
486 .unwrap_or_default()
487 .as_ref(),
488 );
489 result.push('"');
490 }
491 result.push('>');
492 if e.name() == target {
493 depth += 1;
494 }
495 }
496 Ok(quick_xml::events::Event::End(e)) => {
497 let e_name = e.name();
498 let n = std::str::from_utf8(e_name.as_ref()).unwrap_or("");
499 result.push_str("</");
500 result.push_str(n);
501 result.push('>');
502 if e.name() == target {
503 depth -= 1;
504 if depth == 0 {
505 break;
506 }
507 }
508 }
509 Ok(quick_xml::events::Event::Empty(e)) => {
510 let e_name = e.name();
511 let n = std::str::from_utf8(e_name.as_ref()).unwrap_or("");
512 result.push('<');
513 result.push_str(n);
514 for a in e.attributes().flatten() {
515 result.push(' ');
516 result.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
517 result.push_str("=\"");
518 result.push_str(
519 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
520 .unwrap_or_default()
521 .as_ref(),
522 );
523 result.push('"');
524 }
525 result.push_str("/>");
526 }
527 Ok(quick_xml::events::Event::Text(t)) => {
528 let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
532 result.push_str(&quick_xml::escape::unescape(text_str).unwrap_or_default());
533 }
534 Ok(quick_xml::events::Event::Eof) => break,
535 Err(e) => return Err(e),
536 _ => {}
537 }
538 buf.clear();
539 }
540 Ok(result)
541}
542
543impl Theme {
544 pub fn to_xml(&self) -> String {
555 let mut w = XmlWriter::with_decl();
556 let theme_name = if self.name.is_empty() {
557 "Office Theme"
558 } else {
559 self.name.as_str()
560 };
561 w.open_with(
562 "a:theme",
563 &[("xmlns:a", NS_DRAWING_MAIN), ("name", theme_name)],
564 );
565 w.open("a:themeElements");
567 self.color_scheme.write_xml(&mut w);
568 self.font_scheme.write_xml(&mut w);
569 self.format_scheme.write_xml(&mut w);
570 w.close("a:themeElements");
571 w.empty("a:objectDefaults");
573 w.empty("a:extraClrSchemeLst");
575 w.close("a:theme");
576 w.into_string()
577 }
578}
579
580pub fn default_theme_xml() -> String {
585 THEME_XML.to_string()
586}
587
588const DEFAULT_FMT_SCHEME: &str = r#"<a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="1"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="100000"/><a:shade val="100000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="50000"/><a:shade val="100000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="38000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst><a:scene3d><a:camera prst="orthographicFront"><a:rot lat="0" lon="0" rev="0"/></a:camera><a:lightRig rig="threePt" dir="t"><a:rot lat="0" lon="0" rev="1200000"/></a:lightRig></a:scene3d><a:sp3d><a:bevelT w="63500" h="25400"/></a:sp3d></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:shade val="99000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path></a:gradFill></a:bgFillStyleLst></a:fmtScheme>"#;
592
593const THEME_XML: &str = r##"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
595<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F497D"/></a:dk2><a:lt2><a:srgbClr val="EEECE1"/></a:lt2><a:accent1><a:srgbClr val="4F81BD"/></a:accent1><a:accent2><a:srgbClr val="C0504D"/></a:accent2><a:accent3><a:srgbClr val="9BBB59"/></a:accent3><a:accent4><a:srgbClr val="8064A2"/></a:accent4><a:accent5><a:srgbClr val="4BACC6"/></a:accent5><a:accent6><a:srgbClr val="F79646"/></a:accent6><a:hlink><a:srgbClr val="0000FF"/></a:hlink><a:folHlink><a:srgbClr val="800080"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="MS Pゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="宋体"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Times New Roman"/><a:font script="Hebr" typeface="Times New Roman"/><a:font script="Thai" typeface="Angsana New"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="MoolBoran"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Times New Roman"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="MS Pゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="宋体"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Arial"/><a:font script="Hebr" typeface="Arial"/><a:font script="Thai" typeface="Cordia New"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="DaunPenh"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Arial"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="1"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="100000"/><a:shade val="100000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="50000"/><a:shade val="100000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="38000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst><a:scene3d><a:camera prst="orthographicFront"><a:rot lat="0" lon="0" rev="0"/></a:camera><a:lightRig rig="threePt" dir="t"><a:rot lat="0" lon="0" rev="1200000"/></a:lightRig></a:scene3d><a:sp3d><a:bevelT w="63500" h="25400"/></a:sp3d></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:shade val="99000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults><a:spDef><a:spPr/><a:bodyPr/><a:lstStyle/><a:style><a:lnRef idx="1"><a:schemeClr val="accent1"/></a:lnRef><a:fillRef idx="3"><a:schemeClr val="accent1"/></a:fillRef><a:effectRef idx="2"><a:schemeClr val="accent1"/></a:effectRef><a:fontRef idx="minor"><a:schemeClr val="lt1"/></a:fontRef></a:style></a:spDef><a:lnDef><a:spPr/><a:bodyPr/><a:lstStyle/><a:style><a:lnRef idx="2"><a:schemeClr val="accent1"/></a:lnRef><a:fillRef idx="0"><a:schemeClr val="accent1"/></a:fillRef><a:effectRef idx="1"><a:schemeClr val="accent1"/></a:effectRef><a:fontRef idx="minor"><a:schemeClr val="tx1"/></a:fontRef></a:style></a:lnDef></a:objectDefaults><a:extraClrSchemeLst/></a:theme>"##;
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[test]
603 fn theme_to_xml_generates_valid_xml() {
604 let mut theme = Theme {
605 name: "Test Theme".to_string(),
606 ..Default::default()
607 };
608 theme.color_scheme.name = "Test".to_string();
609 theme.color_scheme.dk1 = Some(ThemeColor::Srgb("000000".to_string()));
610 theme.color_scheme.lt1 = Some(ThemeColor::Srgb("FFFFFF".to_string()));
611 theme.font_scheme.major_latin = "Arial".to_string();
612 theme.font_scheme.minor_latin = "Calibri".to_string();
613
614 let xml = theme.to_xml();
615 assert!(xml.contains(r#"name="Test Theme""#), "theme name missing");
617 assert!(
618 xml.contains(r#"<a:clrScheme name="Test">"#),
619 "clrScheme missing"
620 );
621 assert!(
622 xml.contains(r#"<a:dk1><a:srgbClr val="000000"/></a:dk1>"#),
623 "dk1 missing"
624 );
625 assert!(
626 xml.contains(r#"<a:lt1><a:srgbClr val="FFFFFF"/></a:lt1>"#),
627 "lt1 missing"
628 );
629 assert!(
630 xml.contains(r#"<a:latin typeface="Arial"/>"#),
631 "major latin missing"
632 );
633 assert!(
634 xml.contains(r#"<a:latin typeface="Calibri"/>"#),
635 "minor latin missing"
636 );
637 assert!(xml.contains("<a:fmtScheme"), "fmtScheme missing");
639 }
640
641 #[test]
643 fn theme_color_srgb_write_xml() {
644 let mut w = XmlWriter::new();
645 let color = ThemeColor::Srgb("FF0000".to_string());
646 color.write_xml(&mut w);
647 assert_eq!(w.buf, r#"<a:srgbClr val="FF0000"/>"#);
648 }
649
650 #[test]
652 fn theme_color_sys_write_xml() {
653 let mut w = XmlWriter::new();
654 let color = ThemeColor::Sys("windowText".to_string(), "000000".to_string());
655 color.write_xml(&mut w);
656 assert_eq!(w.buf, r#"<a:sysClr val="windowText" lastClr="000000"/>"#);
657 }
658
659 #[test]
661 fn theme_color_none_write_xml() {
662 let mut w = XmlWriter::new();
663 let color = ThemeColor::None;
664 color.write_xml(&mut w);
665 assert_eq!(w.buf, "");
666 }
667
668 #[test]
670 fn color_scheme_write_xml_order() {
671 let scheme = ColorScheme {
672 name: "Test".to_string(),
673 dk1: Some(ThemeColor::Srgb("000000".to_string())),
674 lt1: Some(ThemeColor::Srgb("FFFFFF".to_string())),
675 accent1: Some(ThemeColor::Srgb("4F81BD".to_string())),
676 ..Default::default()
677 };
678
679 let mut w = XmlWriter::new();
680 scheme.write_xml(&mut w);
681 let xml = &w.buf;
682
683 let dk1_pos = xml.find("<a:dk1>").unwrap();
685 let lt1_pos = xml.find("<a:lt1>").unwrap();
686 let accent1_pos = xml.find("<a:accent1>").unwrap();
687 assert!(dk1_pos < lt1_pos, "dk1 should come before lt1");
688 assert!(lt1_pos < accent1_pos, "lt1 should come before accent1");
689 }
690
691 #[test]
693 fn font_scheme_write_xml() {
694 let fs = FontScheme {
695 name: "Test".to_string(),
696 major_latin: "Calibri Light".to_string(),
697 major_ea: "宋体".to_string(),
698 minor_latin: "Calibri".to_string(),
699 minor_ea: "宋体".to_string(),
700 ..Default::default()
701 };
702
703 let mut w = XmlWriter::new();
704 fs.write_xml(&mut w);
705 let xml = &w.buf;
706
707 assert!(
708 xml.contains(r#"<a:latin typeface="Calibri Light"/>"#),
709 "major latin missing"
710 );
711 assert!(
712 xml.contains(r#"<a:ea typeface="宋体"/>"#),
713 "ea typeface missing"
714 );
715 assert!(
716 xml.contains(r#"<a:latin typeface="Calibri"/>"#),
717 "minor latin missing"
718 );
719 }
720
721 #[test]
723 fn default_theme_xml_is_valid() {
724 let xml = default_theme_xml();
725 assert!(!xml.is_empty());
726 assert!(xml.contains("<a:theme"));
727 assert!(xml.contains("<a:clrScheme"));
728 assert!(xml.contains("<a:fontScheme"));
729 assert!(xml.contains("<a:fmtScheme"));
730 }
731
732 fn sample_fmt_scheme_raw_xml() -> &'static str {
738 r#"<a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"/></a:gs></a:gsLst></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525" cap="flat"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="25400"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000"/></a:effectLst></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst>"#
739 }
740
741 #[test]
749 fn fmt_scheme_parse_from_raw_xml_splits_four_lists() {
750 let mut fmt = FormatScheme {
751 name: "Test".to_string(),
752 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
753 ..Default::default()
754 };
755
756 assert_eq!(fmt.fill_style_count(), 0);
758 assert_eq!(fmt.line_style_count(), 0);
759 assert_eq!(fmt.effect_style_count(), 0);
760 assert_eq!(fmt.bg_fill_style_count(), 0);
761
762 fmt.parse_from_raw_xml();
763
764 assert_eq!(fmt.fill_style_count(), 2, "fillStyleLst 应有 2 个子元素");
766 assert_eq!(fmt.line_style_count(), 2, "lnStyleLst 应有 2 个子元素");
767 assert_eq!(
768 fmt.effect_style_count(),
769 1,
770 "effectStyleLst 应有 1 个子元素"
771 );
772 assert_eq!(
773 fmt.bg_fill_style_count(),
774 1,
775 "bgFillStyleLst 应有 1 个子元素"
776 );
777 }
778
779 #[test]
781 fn fmt_scheme_parsed_children_content_correct() {
782 let mut fmt = FormatScheme {
783 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
784 ..Default::default()
785 };
786 fmt.parse_from_raw_xml();
787
788 let fill0 = &fmt.fill_styles[0];
790 assert!(
791 fill0.starts_with("<a:solidFill>"),
792 "fill[0] 应以 <a:solidFill> 开头"
793 );
794 assert!(
795 fill0.contains(r#"<a:schemeClr val="phClr"/>"#),
796 "fill[0] 应包含 schemeClr"
797 );
798 assert!(
799 fill0.ends_with("</a:solidFill>"),
800 "fill[0] 应以 </a:solidFill> 结尾"
801 );
802
803 let fill1 = &fmt.fill_styles[1];
805 assert!(
806 fill1.starts_with(r#"<a:gradFill rotWithShape="1">"#),
807 "fill[1] 应包含属性"
808 );
809
810 let ln0 = &fmt.line_styles[0];
812 assert!(
813 ln0.starts_with(r#"<a:ln w="9525" cap="flat">"#),
814 "ln[0] 应带属性"
815 );
816
817 let eff0 = &fmt.effect_styles[0];
819 assert!(eff0.contains("<a:outerShdw"), "effect[0] 应包含 outerShdw");
820 }
821
822 #[test]
824 fn fmt_scheme_structured_write_xml_outputs_containers() {
825 let mut fmt = FormatScheme {
826 name: "Office".to_string(),
827 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
828 ..Default::default()
829 };
830 fmt.parse_from_raw_xml();
831
832 let mut w = XmlWriter::new();
833 fmt.write_xml(&mut w);
834 let xml = &w.buf;
835
836 let fill_pos = xml.find("<a:fillStyleLst>").expect("fillStyleLst missing");
838 let ln_pos = xml.find("<a:lnStyleLst>").expect("lnStyleLst missing");
839 let eff_pos = xml
840 .find("<a:effectStyleLst>")
841 .expect("effectStyleLst missing");
842 let bg_pos = xml
843 .find("<a:bgFillStyleLst>")
844 .expect("bgFillStyleLst missing");
845
846 assert!(fill_pos < ln_pos, "fillStyleLst 应在 lnStyleLst 之前");
848 assert!(ln_pos < eff_pos, "lnStyleLst 应在 effectStyleLst 之前");
849 assert!(eff_pos < bg_pos, "effectStyleLst 应在 bgFillStyleLst 之前");
850
851 assert!(
853 xml.contains(r#"<a:fmtScheme name="Office">"#),
854 "fmtScheme 应带 name 属性"
855 );
856
857 assert!(xml.contains("<a:solidFill>"), "应包含 solidFill 子元素");
859 assert!(xml.contains("<a:ln "), "应包含 ln 子元素");
860 }
861
862 #[test]
864 fn fmt_scheme_count_methods_after_parse() {
865 let mut fmt = FormatScheme {
866 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
867 ..Default::default()
868 };
869 fmt.parse_from_raw_xml();
870
871 assert_eq!(fmt.fill_style_count(), 2);
872 assert_eq!(fmt.line_style_count(), 2);
873 assert_eq!(fmt.effect_style_count(), 1);
874 assert_eq!(fmt.bg_fill_style_count(), 1);
875 }
876
877 #[test]
879 fn fmt_scheme_parse_empty_raw_xml_is_noop() {
880 let mut fmt = FormatScheme::default();
881 fmt.parse_from_raw_xml();
883
884 assert_eq!(fmt.fill_style_count(), 0);
885 assert_eq!(fmt.line_style_count(), 0);
886 assert_eq!(fmt.effect_style_count(), 0);
887 assert_eq!(fmt.bg_fill_style_count(), 0);
888 }
889
890 #[test]
892 fn fmt_scheme_write_xml_falls_back_to_raw_xml() {
893 let fmt = FormatScheme {
894 name: "Office".to_string(),
895 raw_xml: r#"<a:customChild/>"#.to_string(),
896 ..Default::default()
897 };
898 let mut w = XmlWriter::new();
901 fmt.write_xml(&mut w);
902 let xml = &w.buf;
903
904 assert!(
906 xml.contains(r#"<a:fmtScheme name="Office">"#),
907 "应使用 raw_xml 路径"
908 );
909 assert!(xml.contains("<a:customChild/>"), "应包含 raw_xml 内容");
910 }
911
912 #[test]
914 fn fmt_scheme_write_xml_uses_default_when_both_empty() {
915 let fmt = FormatScheme::default();
916 let mut w = XmlWriter::new();
919 fmt.write_xml(&mut w);
920 let xml = &w.buf;
921
922 assert!(xml.contains("<a:fmtScheme"), "应包含默认 fmtScheme");
924 assert!(
925 xml.contains("<a:fillStyleLst>"),
926 "默认方案应含 fillStyleLst"
927 );
928 assert!(xml.contains("<a:lnStyleLst>"), "默认方案应含 lnStyleLst");
929 assert!(
930 xml.contains("<a:effectStyleLst>"),
931 "默认方案应含 effectStyleLst"
932 );
933 assert!(
934 xml.contains("<a:bgFillStyleLst>"),
935 "默认方案应含 bgFillStyleLst"
936 );
937 }
938
939 #[test]
944 fn fmt_scheme_parse_default_office_format_scheme() {
945 let theme_xml = default_theme_xml();
947 let fmt_start = theme_xml
948 .find("<a:fmtScheme")
949 .expect("默认主题应包含 <a:fmtScheme");
950 let fmt_end = theme_xml
951 .find("</a:fmtScheme>")
952 .expect("默认主题应包含 </a:fmtScheme>");
953 let inner_start = theme_xml[fmt_start..].find('>').unwrap() + fmt_start + 1;
955 let fmt_inner = &theme_xml[inner_start..fmt_end];
956
957 let mut fmt = FormatScheme {
958 raw_xml: fmt_inner.to_string(),
959 ..Default::default()
960 };
961 fmt.parse_from_raw_xml();
962
963 assert_eq!(
965 fmt.fill_style_count(),
966 3,
967 "默认 fillStyleLst 应有 3 个子元素"
968 );
969 assert_eq!(fmt.line_style_count(), 3, "默认 lnStyleLst 应有 3 个子元素");
970 assert_eq!(
971 fmt.effect_style_count(),
972 3,
973 "默认 effectStyleLst 应有 3 个子元素"
974 );
975 assert_eq!(
976 fmt.bg_fill_style_count(),
977 3,
978 "默认 bgFillStyleLst 应有 3 个子元素"
979 );
980 }
981
982 #[test]
984 fn fmt_scheme_structured_fields_are_mutable() {
985 let mut fmt = FormatScheme {
986 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
987 ..Default::default()
988 };
989 fmt.parse_from_raw_xml();
990
991 fmt.fill_styles[0] = r#"<a:noFill/>"#.to_string();
993
994 let mut w = XmlWriter::new();
995 fmt.write_xml(&mut w);
996 let xml = &w.buf;
997
998 assert!(
1000 xml.contains("<a:noFill/>"),
1001 "替换后的 fill style 应出现在输出"
1002 );
1003 assert!(xml.contains("<a:gradFill"), "其他 fill style 应保留");
1005 }
1006
1007 #[test]
1009 fn fmt_scheme_collect_self_closing_children() {
1010 let raw = r#"<a:fillStyleLst><a:noFill/><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst>"#;
1012 let children = collect_style_lst_children(raw, "fillStyleLst");
1013 assert_eq!(children.len(), 2, "应有 2 个子元素");
1014 assert_eq!(children[0], "<a:noFill/>", "自闭子元素应重构为完整 XML");
1016 assert!(
1018 children[1].starts_with("<a:solidFill>"),
1019 "第二个子元素应为 solidFill"
1020 );
1021 }
1022
1023 #[test]
1025 fn local_name_quick_handles_namespaced_elements() {
1026 assert_eq!(local_name_quick(b"a:fillStyleLst"), b"fillStyleLst");
1027 assert_eq!(local_name_quick(b"fillStyleLst"), b"fillStyleLst");
1028 assert_eq!(local_name_quick(b"a:ln"), b"ln");
1029 }
1030
1031 #[test]
1033 fn collect_style_lst_children_returns_empty_when_container_missing() {
1034 let raw = r#"<a:otherLst><a:child/></a:otherLst>"#;
1035 let children = collect_style_lst_children(raw, "fillStyleLst");
1036 assert!(children.is_empty(), "容器不存在时应返回空列表");
1037 }
1038}