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}
39
40#[derive(Clone, Debug, Default)]
44pub struct ColorScheme {
45 pub name: String,
47 pub dk1: Option<ThemeColor>,
49 pub lt1: Option<ThemeColor>,
51 pub dk2: Option<ThemeColor>,
53 pub lt2: Option<ThemeColor>,
55 pub accent1: Option<ThemeColor>,
57 pub accent2: Option<ThemeColor>,
58 pub accent3: Option<ThemeColor>,
59 pub accent4: Option<ThemeColor>,
60 pub accent5: Option<ThemeColor>,
61 pub accent6: Option<ThemeColor>,
62 pub hlink: Option<ThemeColor>,
64 pub fol_hlink: Option<ThemeColor>,
66}
67
68#[derive(Clone, Debug, Default)]
72pub struct FontScheme {
73 pub name: String,
75 pub major_latin: String,
77 pub major_ea: String,
79 pub major_cs: String,
81 pub minor_latin: String,
83 pub minor_ea: String,
85 pub minor_cs: String,
87}
88
89#[derive(Clone, Debug, Default)]
101pub struct FormatScheme {
102 pub name: String,
104 pub raw_xml: String,
108 pub fill_styles: Vec<String>,
112 pub line_styles: Vec<String>,
116 pub effect_styles: Vec<String>,
120 pub bg_fill_styles: Vec<String>,
124}
125
126#[derive(Clone, Debug, Default, PartialEq, Eq)]
128pub enum ThemeColor {
129 Srgb(String),
131 Sys(String, String),
133 #[default]
135 None,
136}
137
138impl ThemeColor {
139 pub fn write_xml(&self, w: &mut XmlWriter) {
147 match self {
148 ThemeColor::Srgb(val) => {
149 w.empty_with("a:srgbClr", &[("val", val.as_str())]);
150 }
151 ThemeColor::Sys(val, last_clr) => {
152 w.empty_with(
153 "a:sysClr",
154 &[("val", val.as_str()), ("lastClr", last_clr.as_str())],
155 );
156 }
157 ThemeColor::None => { }
158 }
159 }
160}
161
162impl ColorScheme {
163 pub fn write_xml(&self, w: &mut XmlWriter) {
170 let name = if self.name.is_empty() {
171 "Office"
172 } else {
173 self.name.as_str()
174 };
175 w.open_with("a:clrScheme", &[("name", name)]);
176 self.write_slot(w, "a:dk1", &self.dk1);
178 self.write_slot(w, "a:lt1", &self.lt1);
179 self.write_slot(w, "a:dk2", &self.dk2);
180 self.write_slot(w, "a:lt2", &self.lt2);
181 self.write_slot(w, "a:accent1", &self.accent1);
182 self.write_slot(w, "a:accent2", &self.accent2);
183 self.write_slot(w, "a:accent3", &self.accent3);
184 self.write_slot(w, "a:accent4", &self.accent4);
185 self.write_slot(w, "a:accent5", &self.accent5);
186 self.write_slot(w, "a:accent6", &self.accent6);
187 self.write_slot(w, "a:hlink", &self.hlink);
188 self.write_slot(w, "a:folHlink", &self.fol_hlink);
189 w.close("a:clrScheme");
190 }
191
192 fn write_slot(&self, w: &mut XmlWriter, tag: &str, color: &Option<ThemeColor>) {
194 w.open(tag);
195 if let Some(c) = color {
196 c.write_xml(w);
197 }
198 w.close(tag);
199 }
200}
201
202impl FontScheme {
203 pub fn write_xml(&self, w: &mut XmlWriter) {
222 let name = if self.name.is_empty() {
223 "Office"
224 } else {
225 self.name.as_str()
226 };
227 w.open_with("a:fontScheme", &[("name", name)]);
228 w.open("a:majorFont");
230 w.empty_with("a:latin", &[("typeface", self.major_latin.as_str())]);
231 w.empty_with("a:ea", &[("typeface", self.major_ea.as_str())]);
232 w.empty_with("a:cs", &[("typeface", self.major_cs.as_str())]);
233 w.close("a:majorFont");
234 w.open("a:minorFont");
236 w.empty_with("a:latin", &[("typeface", self.minor_latin.as_str())]);
237 w.empty_with("a:ea", &[("typeface", self.minor_ea.as_str())]);
238 w.empty_with("a:cs", &[("typeface", self.minor_cs.as_str())]);
239 w.close("a:minorFont");
240 w.close("a:fontScheme");
241 }
242}
243
244impl FormatScheme {
245 pub fn write_xml(&self, w: &mut XmlWriter) {
250 let name = if self.name.is_empty() {
251 "Office"
252 } else {
253 self.name.as_str()
254 };
255 let has_structured = !self.fill_styles.is_empty()
257 || !self.line_styles.is_empty()
258 || !self.effect_styles.is_empty()
259 || !self.bg_fill_styles.is_empty();
260
261 if has_structured {
262 w.open_with("a:fmtScheme", &[("name", name)]);
264 if !self.fill_styles.is_empty() {
265 w.open("a:fillStyleLst");
266 for s in &self.fill_styles {
267 w.raw(s);
268 }
269 w.close("a:fillStyleLst");
270 }
271 if !self.line_styles.is_empty() {
272 w.open("a:lnStyleLst");
273 for s in &self.line_styles {
274 w.raw(s);
275 }
276 w.close("a:lnStyleLst");
277 }
278 if !self.effect_styles.is_empty() {
279 w.open("a:effectStyleLst");
280 for s in &self.effect_styles {
281 w.raw(s);
282 }
283 w.close("a:effectStyleLst");
284 }
285 if !self.bg_fill_styles.is_empty() {
286 w.open("a:bgFillStyleLst");
287 for s in &self.bg_fill_styles {
288 w.raw(s);
289 }
290 w.close("a:bgFillStyleLst");
291 }
292 w.close("a:fmtScheme");
293 } else if self.raw_xml.is_empty() {
294 w.raw(DEFAULT_FMT_SCHEME);
296 } else {
297 w.open_with("a:fmtScheme", &[("name", name)]);
299 w.raw(&self.raw_xml);
300 w.close("a:fmtScheme");
301 }
302 }
303
304 pub fn parse_from_raw_xml(&mut self) {
314 if self.raw_xml.is_empty() {
315 return;
316 }
317 self.fill_styles = collect_style_lst_children(&self.raw_xml, "fillStyleLst");
318 self.line_styles = collect_style_lst_children(&self.raw_xml, "lnStyleLst");
319 self.effect_styles = collect_style_lst_children(&self.raw_xml, "effectStyleLst");
320 self.bg_fill_styles = collect_style_lst_children(&self.raw_xml, "bgFillStyleLst");
321 }
322
323 pub fn fill_style_count(&self) -> usize {
325 self.fill_styles.len()
326 }
327
328 pub fn line_style_count(&self) -> usize {
330 self.line_styles.len()
331 }
332
333 pub fn effect_style_count(&self) -> usize {
335 self.effect_styles.len()
336 }
337
338 pub fn bg_fill_style_count(&self) -> usize {
340 self.bg_fill_styles.len()
341 }
342}
343
344fn collect_style_lst_children(xml: &str, container_local: &str) -> Vec<String> {
354 let mut result = Vec::new();
355 let mut rd = quick_xml::Reader::from_str(xml);
356 rd.config_mut().trim_text(true);
357 let mut buf = Vec::new();
358
359 enum State {
361 Seeking,
362 InContainer,
363 }
364 let mut state = State::Seeking;
365
366 loop {
367 match rd.read_event_into(&mut buf) {
368 Ok(quick_xml::events::Event::Start(e)) => {
369 let name = e.name();
371 let local = local_name_quick(name.as_ref());
372 match state {
373 State::Seeking => {
374 if local == container_local.as_bytes() {
375 state = State::InContainer;
376 }
377 }
378 State::InContainer => {
379 if let Ok(inner) = collect_full_element_str(&mut rd, e.into_owned()) {
381 result.push(inner);
382 }
383 }
384 }
385 }
386 Ok(quick_xml::events::Event::End(e)) => {
387 let name = e.name();
388 let local = local_name_quick(name.as_ref());
389 if let State::InContainer = state {
390 if local == container_local.as_bytes() {
391 state = State::Seeking;
392 }
393 }
394 }
395 Ok(quick_xml::events::Event::Empty(e)) => {
396 let name = e.name();
397 let local = local_name_quick(name.as_ref());
398 if let State::InContainer = state {
399 let _ = local; let mut s = String::new();
403 s.push('<');
404 s.push_str(std::str::from_utf8(name.as_ref()).unwrap_or(""));
405 for a in e.attributes().flatten() {
406 s.push(' ');
407 s.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
408 s.push_str("=\"");
409 s.push_str(
410 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
411 .unwrap_or_default()
412 .as_ref(),
413 );
414 s.push('"');
415 }
416 s.push_str("/>");
417 result.push(s);
418 }
419 }
420 Ok(quick_xml::events::Event::Eof) => break,
421 Err(_) => break,
422 _ => {}
423 }
424 buf.clear();
425 }
426 result
427}
428
429fn local_name_quick(name: &[u8]) -> &[u8] {
431 match name.iter().position(|&b| b == b':') {
433 Some(pos) => &name[pos + 1..],
434 None => name,
435 }
436}
437
438fn collect_full_element_str(
442 rd: &mut quick_xml::Reader<&[u8]>,
443 start: quick_xml::events::BytesStart<'_>,
444) -> quick_xml::Result<String> {
445 let mut result = String::new();
446 let start_name = start.name();
449 let name = std::str::from_utf8(start_name.as_ref()).unwrap_or("");
450 result.push('<');
451 result.push_str(name);
452 for a in start.attributes().flatten() {
453 result.push(' ');
454 result.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
455 result.push_str("=\"");
456 result.push_str(
457 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
458 .unwrap_or_default()
459 .as_ref(),
460 );
461 result.push('"');
462 }
463 result.push('>');
464
465 let target = start.name();
466 let mut depth = 1;
467 let mut buf = Vec::new();
468 loop {
469 match rd.read_event_into(&mut buf) {
470 Ok(quick_xml::events::Event::Start(e)) => {
471 let e_name = e.name();
473 let n = std::str::from_utf8(e_name.as_ref()).unwrap_or("");
474 result.push('<');
475 result.push_str(n);
476 for a in e.attributes().flatten() {
477 result.push(' ');
478 result.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
479 result.push_str("=\"");
480 result.push_str(
481 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
482 .unwrap_or_default()
483 .as_ref(),
484 );
485 result.push('"');
486 }
487 result.push('>');
488 if e.name() == target {
489 depth += 1;
490 }
491 }
492 Ok(quick_xml::events::Event::End(e)) => {
493 let e_name = e.name();
494 let n = std::str::from_utf8(e_name.as_ref()).unwrap_or("");
495 result.push_str("</");
496 result.push_str(n);
497 result.push('>');
498 if e.name() == target {
499 depth -= 1;
500 if depth == 0 {
501 break;
502 }
503 }
504 }
505 Ok(quick_xml::events::Event::Empty(e)) => {
506 let e_name = e.name();
507 let n = std::str::from_utf8(e_name.as_ref()).unwrap_or("");
508 result.push('<');
509 result.push_str(n);
510 for a in e.attributes().flatten() {
511 result.push(' ');
512 result.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
513 result.push_str("=\"");
514 result.push_str(
515 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
516 .unwrap_or_default()
517 .as_ref(),
518 );
519 result.push('"');
520 }
521 result.push_str("/>");
522 }
523 Ok(quick_xml::events::Event::Text(t)) => {
524 let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
528 result.push_str(&quick_xml::escape::unescape(text_str).unwrap_or_default());
529 }
530 Ok(quick_xml::events::Event::Eof) => break,
531 Err(e) => return Err(e),
532 _ => {}
533 }
534 buf.clear();
535 }
536 Ok(result)
537}
538
539impl Theme {
540 pub fn to_xml(&self) -> String {
551 let mut w = XmlWriter::with_decl();
552 let theme_name = if self.name.is_empty() {
553 "Office Theme"
554 } else {
555 self.name.as_str()
556 };
557 w.open_with(
558 "a:theme",
559 &[("xmlns:a", NS_DRAWING_MAIN), ("name", theme_name)],
560 );
561 w.open("a:themeElements");
563 self.color_scheme.write_xml(&mut w);
564 self.font_scheme.write_xml(&mut w);
565 self.format_scheme.write_xml(&mut w);
566 w.close("a:themeElements");
567 w.empty("a:objectDefaults");
569 w.empty("a:extraClrSchemeLst");
571 w.close("a:theme");
572 w.into_string()
573 }
574}
575
576pub fn default_theme_xml() -> String {
581 THEME_XML.to_string()
582}
583
584const 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>"#;
588
589const THEME_XML: &str = r##"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
591<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>"##;
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
599 fn theme_to_xml_generates_valid_xml() {
600 let mut theme = Theme {
601 name: "Test Theme".to_string(),
602 ..Default::default()
603 };
604 theme.color_scheme.name = "Test".to_string();
605 theme.color_scheme.dk1 = Some(ThemeColor::Srgb("000000".to_string()));
606 theme.color_scheme.lt1 = Some(ThemeColor::Srgb("FFFFFF".to_string()));
607 theme.font_scheme.major_latin = "Arial".to_string();
608 theme.font_scheme.minor_latin = "Calibri".to_string();
609
610 let xml = theme.to_xml();
611 assert!(xml.contains(r#"name="Test Theme""#), "theme name missing");
613 assert!(
614 xml.contains(r#"<a:clrScheme name="Test">"#),
615 "clrScheme missing"
616 );
617 assert!(
618 xml.contains(r#"<a:dk1><a:srgbClr val="000000"/></a:dk1>"#),
619 "dk1 missing"
620 );
621 assert!(
622 xml.contains(r#"<a:lt1><a:srgbClr val="FFFFFF"/></a:lt1>"#),
623 "lt1 missing"
624 );
625 assert!(
626 xml.contains(r#"<a:latin typeface="Arial"/>"#),
627 "major latin missing"
628 );
629 assert!(
630 xml.contains(r#"<a:latin typeface="Calibri"/>"#),
631 "minor latin missing"
632 );
633 assert!(xml.contains("<a:fmtScheme"), "fmtScheme missing");
635 }
636
637 #[test]
639 fn theme_color_srgb_write_xml() {
640 let mut w = XmlWriter::new();
641 let color = ThemeColor::Srgb("FF0000".to_string());
642 color.write_xml(&mut w);
643 assert_eq!(w.buf, r#"<a:srgbClr val="FF0000"/>"#);
644 }
645
646 #[test]
648 fn theme_color_sys_write_xml() {
649 let mut w = XmlWriter::new();
650 let color = ThemeColor::Sys("windowText".to_string(), "000000".to_string());
651 color.write_xml(&mut w);
652 assert_eq!(w.buf, r#"<a:sysClr val="windowText" lastClr="000000"/>"#);
653 }
654
655 #[test]
657 fn theme_color_none_write_xml() {
658 let mut w = XmlWriter::new();
659 let color = ThemeColor::None;
660 color.write_xml(&mut w);
661 assert_eq!(w.buf, "");
662 }
663
664 #[test]
666 fn color_scheme_write_xml_order() {
667 let scheme = ColorScheme {
668 name: "Test".to_string(),
669 dk1: Some(ThemeColor::Srgb("000000".to_string())),
670 lt1: Some(ThemeColor::Srgb("FFFFFF".to_string())),
671 accent1: Some(ThemeColor::Srgb("4F81BD".to_string())),
672 ..Default::default()
673 };
674
675 let mut w = XmlWriter::new();
676 scheme.write_xml(&mut w);
677 let xml = &w.buf;
678
679 let dk1_pos = xml.find("<a:dk1>").unwrap();
681 let lt1_pos = xml.find("<a:lt1>").unwrap();
682 let accent1_pos = xml.find("<a:accent1>").unwrap();
683 assert!(dk1_pos < lt1_pos, "dk1 should come before lt1");
684 assert!(lt1_pos < accent1_pos, "lt1 should come before accent1");
685 }
686
687 #[test]
689 fn font_scheme_write_xml() {
690 let fs = FontScheme {
691 name: "Test".to_string(),
692 major_latin: "Calibri Light".to_string(),
693 major_ea: "宋体".to_string(),
694 minor_latin: "Calibri".to_string(),
695 minor_ea: "宋体".to_string(),
696 ..Default::default()
697 };
698
699 let mut w = XmlWriter::new();
700 fs.write_xml(&mut w);
701 let xml = &w.buf;
702
703 assert!(
704 xml.contains(r#"<a:latin typeface="Calibri Light"/>"#),
705 "major latin missing"
706 );
707 assert!(
708 xml.contains(r#"<a:ea typeface="宋体"/>"#),
709 "ea typeface missing"
710 );
711 assert!(
712 xml.contains(r#"<a:latin typeface="Calibri"/>"#),
713 "minor latin missing"
714 );
715 }
716
717 #[test]
719 fn default_theme_xml_is_valid() {
720 let xml = default_theme_xml();
721 assert!(!xml.is_empty());
722 assert!(xml.contains("<a:theme"));
723 assert!(xml.contains("<a:clrScheme"));
724 assert!(xml.contains("<a:fontScheme"));
725 assert!(xml.contains("<a:fmtScheme"));
726 }
727
728 fn sample_fmt_scheme_raw_xml() -> &'static str {
734 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>"#
735 }
736
737 #[test]
745 fn fmt_scheme_parse_from_raw_xml_splits_four_lists() {
746 let mut fmt = FormatScheme {
747 name: "Test".to_string(),
748 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
749 ..Default::default()
750 };
751
752 assert_eq!(fmt.fill_style_count(), 0);
754 assert_eq!(fmt.line_style_count(), 0);
755 assert_eq!(fmt.effect_style_count(), 0);
756 assert_eq!(fmt.bg_fill_style_count(), 0);
757
758 fmt.parse_from_raw_xml();
759
760 assert_eq!(fmt.fill_style_count(), 2, "fillStyleLst 应有 2 个子元素");
762 assert_eq!(fmt.line_style_count(), 2, "lnStyleLst 应有 2 个子元素");
763 assert_eq!(
764 fmt.effect_style_count(),
765 1,
766 "effectStyleLst 应有 1 个子元素"
767 );
768 assert_eq!(
769 fmt.bg_fill_style_count(),
770 1,
771 "bgFillStyleLst 应有 1 个子元素"
772 );
773 }
774
775 #[test]
777 fn fmt_scheme_parsed_children_content_correct() {
778 let mut fmt = FormatScheme {
779 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
780 ..Default::default()
781 };
782 fmt.parse_from_raw_xml();
783
784 let fill0 = &fmt.fill_styles[0];
786 assert!(
787 fill0.starts_with("<a:solidFill>"),
788 "fill[0] 应以 <a:solidFill> 开头"
789 );
790 assert!(
791 fill0.contains(r#"<a:schemeClr val="phClr"/>"#),
792 "fill[0] 应包含 schemeClr"
793 );
794 assert!(
795 fill0.ends_with("</a:solidFill>"),
796 "fill[0] 应以 </a:solidFill> 结尾"
797 );
798
799 let fill1 = &fmt.fill_styles[1];
801 assert!(
802 fill1.starts_with(r#"<a:gradFill rotWithShape="1">"#),
803 "fill[1] 应包含属性"
804 );
805
806 let ln0 = &fmt.line_styles[0];
808 assert!(
809 ln0.starts_with(r#"<a:ln w="9525" cap="flat">"#),
810 "ln[0] 应带属性"
811 );
812
813 let eff0 = &fmt.effect_styles[0];
815 assert!(eff0.contains("<a:outerShdw"), "effect[0] 应包含 outerShdw");
816 }
817
818 #[test]
820 fn fmt_scheme_structured_write_xml_outputs_containers() {
821 let mut fmt = FormatScheme {
822 name: "Office".to_string(),
823 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
824 ..Default::default()
825 };
826 fmt.parse_from_raw_xml();
827
828 let mut w = XmlWriter::new();
829 fmt.write_xml(&mut w);
830 let xml = &w.buf;
831
832 let fill_pos = xml.find("<a:fillStyleLst>").expect("fillStyleLst missing");
834 let ln_pos = xml.find("<a:lnStyleLst>").expect("lnStyleLst missing");
835 let eff_pos = xml
836 .find("<a:effectStyleLst>")
837 .expect("effectStyleLst missing");
838 let bg_pos = xml
839 .find("<a:bgFillStyleLst>")
840 .expect("bgFillStyleLst missing");
841
842 assert!(fill_pos < ln_pos, "fillStyleLst 应在 lnStyleLst 之前");
844 assert!(ln_pos < eff_pos, "lnStyleLst 应在 effectStyleLst 之前");
845 assert!(eff_pos < bg_pos, "effectStyleLst 应在 bgFillStyleLst 之前");
846
847 assert!(
849 xml.contains(r#"<a:fmtScheme name="Office">"#),
850 "fmtScheme 应带 name 属性"
851 );
852
853 assert!(xml.contains("<a:solidFill>"), "应包含 solidFill 子元素");
855 assert!(xml.contains("<a:ln "), "应包含 ln 子元素");
856 }
857
858 #[test]
860 fn fmt_scheme_count_methods_after_parse() {
861 let mut fmt = FormatScheme {
862 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
863 ..Default::default()
864 };
865 fmt.parse_from_raw_xml();
866
867 assert_eq!(fmt.fill_style_count(), 2);
868 assert_eq!(fmt.line_style_count(), 2);
869 assert_eq!(fmt.effect_style_count(), 1);
870 assert_eq!(fmt.bg_fill_style_count(), 1);
871 }
872
873 #[test]
875 fn fmt_scheme_parse_empty_raw_xml_is_noop() {
876 let mut fmt = FormatScheme::default();
877 fmt.parse_from_raw_xml();
879
880 assert_eq!(fmt.fill_style_count(), 0);
881 assert_eq!(fmt.line_style_count(), 0);
882 assert_eq!(fmt.effect_style_count(), 0);
883 assert_eq!(fmt.bg_fill_style_count(), 0);
884 }
885
886 #[test]
888 fn fmt_scheme_write_xml_falls_back_to_raw_xml() {
889 let fmt = FormatScheme {
890 name: "Office".to_string(),
891 raw_xml: r#"<a:customChild/>"#.to_string(),
892 ..Default::default()
893 };
894 let mut w = XmlWriter::new();
897 fmt.write_xml(&mut w);
898 let xml = &w.buf;
899
900 assert!(
902 xml.contains(r#"<a:fmtScheme name="Office">"#),
903 "应使用 raw_xml 路径"
904 );
905 assert!(xml.contains("<a:customChild/>"), "应包含 raw_xml 内容");
906 }
907
908 #[test]
910 fn fmt_scheme_write_xml_uses_default_when_both_empty() {
911 let fmt = FormatScheme::default();
912 let mut w = XmlWriter::new();
915 fmt.write_xml(&mut w);
916 let xml = &w.buf;
917
918 assert!(xml.contains("<a:fmtScheme"), "应包含默认 fmtScheme");
920 assert!(
921 xml.contains("<a:fillStyleLst>"),
922 "默认方案应含 fillStyleLst"
923 );
924 assert!(xml.contains("<a:lnStyleLst>"), "默认方案应含 lnStyleLst");
925 assert!(
926 xml.contains("<a:effectStyleLst>"),
927 "默认方案应含 effectStyleLst"
928 );
929 assert!(
930 xml.contains("<a:bgFillStyleLst>"),
931 "默认方案应含 bgFillStyleLst"
932 );
933 }
934
935 #[test]
940 fn fmt_scheme_parse_default_office_format_scheme() {
941 let theme_xml = default_theme_xml();
943 let fmt_start = theme_xml
944 .find("<a:fmtScheme")
945 .expect("默认主题应包含 <a:fmtScheme");
946 let fmt_end = theme_xml
947 .find("</a:fmtScheme>")
948 .expect("默认主题应包含 </a:fmtScheme>");
949 let inner_start = theme_xml[fmt_start..].find('>').unwrap() + fmt_start + 1;
951 let fmt_inner = &theme_xml[inner_start..fmt_end];
952
953 let mut fmt = FormatScheme {
954 raw_xml: fmt_inner.to_string(),
955 ..Default::default()
956 };
957 fmt.parse_from_raw_xml();
958
959 assert_eq!(
961 fmt.fill_style_count(),
962 3,
963 "默认 fillStyleLst 应有 3 个子元素"
964 );
965 assert_eq!(fmt.line_style_count(), 3, "默认 lnStyleLst 应有 3 个子元素");
966 assert_eq!(
967 fmt.effect_style_count(),
968 3,
969 "默认 effectStyleLst 应有 3 个子元素"
970 );
971 assert_eq!(
972 fmt.bg_fill_style_count(),
973 3,
974 "默认 bgFillStyleLst 应有 3 个子元素"
975 );
976 }
977
978 #[test]
980 fn fmt_scheme_structured_fields_are_mutable() {
981 let mut fmt = FormatScheme {
982 raw_xml: sample_fmt_scheme_raw_xml().to_string(),
983 ..Default::default()
984 };
985 fmt.parse_from_raw_xml();
986
987 fmt.fill_styles[0] = r#"<a:noFill/>"#.to_string();
989
990 let mut w = XmlWriter::new();
991 fmt.write_xml(&mut w);
992 let xml = &w.buf;
993
994 assert!(
996 xml.contains("<a:noFill/>"),
997 "替换后的 fill style 应出现在输出"
998 );
999 assert!(xml.contains("<a:gradFill"), "其他 fill style 应保留");
1001 }
1002
1003 #[test]
1005 fn fmt_scheme_collect_self_closing_children() {
1006 let raw = r#"<a:fillStyleLst><a:noFill/><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst>"#;
1008 let children = collect_style_lst_children(raw, "fillStyleLst");
1009 assert_eq!(children.len(), 2, "应有 2 个子元素");
1010 assert_eq!(children[0], "<a:noFill/>", "自闭子元素应重构为完整 XML");
1012 assert!(
1014 children[1].starts_with("<a:solidFill>"),
1015 "第二个子元素应为 solidFill"
1016 );
1017 }
1018
1019 #[test]
1021 fn local_name_quick_handles_namespaced_elements() {
1022 assert_eq!(local_name_quick(b"a:fillStyleLst"), b"fillStyleLst");
1023 assert_eq!(local_name_quick(b"fillStyleLst"), b"fillStyleLst");
1024 assert_eq!(local_name_quick(b"a:ln"), b"ln");
1025 }
1026
1027 #[test]
1029 fn collect_style_lst_children_returns_empty_when_container_missing() {
1030 let raw = r#"<a:otherLst><a:child/></a:otherLst>"#;
1031 let children = collect_style_lst_children(raw, "fillStyleLst");
1032 assert!(children.is_empty(), "容器不存在时应返回空列表");
1033 }
1034}