#![allow(clippy::field_reassign_with_default)]
use quick_xml::events::Event;
use quick_xml::reader::Reader;
use crate::oxml::color::{Color, PresetColor, SchemeColor};
use crate::oxml::shape::{
Connector, GraphicFrame, Group, GroupChild, Pic, ShapeLocks, ShapeStyle, Sp, StyleRef,
};
use crate::oxml::simpletypes::{
Alignment, MsoAnchor, PresetGeometry, TabAlignment, TextWrapping, Underline,
};
use crate::oxml::slide::Sld;
use crate::oxml::slide::{
MorphOption, SplitOrientation, Transition, TransitionDirection, TransitionSpeed, TransitionType,
};
use crate::oxml::sppr::{
ArrowHead, ArrowSize, ArrowType, Backdrop, Bevel, Camera, CameraPreset, CustomGeometry,
EffectList, Fill, GeomRect, Geometry, GlowEffect, GradientFill, GradientPath, GradientStop,
GradientType, LightRig, LightRigDirection, LightRigType, Line, LineJoin, MaterialPreset, Path,
PathSegment, PatternFill, Point3d, ReflectionEffect, Rotation3d, Scene3d, ShadowEffect,
ShapeProperties, SoftEdgeEffect, Sp3d,
};
use crate::oxml::txbody::{
BodyProperties, BulletStyle, Field, FieldType, Hyperlink, Paragraph, ParagraphProperties, Run,
RunProperties, TabStop, TextBody,
};
use crate::oxml::SlideShape as OxmlSlideShape;
use crate::units::{Emu, Pt, RGBColor};
pub fn parse_sld(xml: &str) -> crate::Result<Sld> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut sld = Sld::default();
let mut state: u8 = 0;
let mut sp_bufs: Vec<String> = Vec::new();
let mut pic_bufs: Vec<String> = Vec::new();
let mut cxn_bufs: Vec<String> = Vec::new();
let mut grp_bufs: Vec<String> = Vec::new();
let mut gfx_bufs: Vec<String> = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match state {
0 if local == b"sld" => {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"id" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u32>()
{
sld.id = v;
}
}
}
state = 1;
}
1 if local == b"cSld" => {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"name" {
sld.name = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
}
1 if local == b"spTree" => {
state = 2;
}
1 if local == b"transition" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
if let Ok(tr) = parse_transition(&inner) {
sld.transition = Some(tr);
}
}
2 if local == b"sp" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
sp_bufs.push(inner);
}
2 if local == b"pic" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
pic_bufs.push(inner);
}
2 if local == b"cxnSp" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
cxn_bufs.push(inner);
}
2 if local == b"grpSp" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
grp_bufs.push(inner);
}
2 if local == b"graphicFrame" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
gfx_bufs.push(inner);
}
_ => {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if state == 2 {
if local == b"sp" {
} else if local == b"pic" {
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if state == 1 && local == b"sld" {
break;
} else if state == 2 && local == b"spTree" {
state = 1;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("sld parse: {e}"))),
_ => {}
}
buf.clear();
}
for s in sp_bufs {
match parse_sp(&s) {
Ok(sp) => sld.shapes.push(OxmlSlideShape::Sp(sp)),
Err(e) => {
return Err(e);
}
}
}
for p in pic_bufs {
if let Ok(pic) = parse_pic(&p) {
sld.shapes.push(OxmlSlideShape::Pic(pic));
}
}
for c in cxn_bufs {
if let Ok(cxn) = parse_cxn_sp(&c) {
sld.shapes.push(OxmlSlideShape::CxnSp(cxn));
}
}
for g in grp_bufs {
if let Ok(grp) = parse_grp_sp(&g) {
sld.shapes.push(OxmlSlideShape::Group(Box::new(grp)));
}
}
for f in gfx_bufs {
if let Ok(frame) = parse_graphic_frame(&f) {
sld.shapes.push(OxmlSlideShape::GraphicFrame(frame));
}
}
Ok(sld)
}
pub fn parse_transition(xml: &str) -> crate::Result<Transition> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut tr = Transition::default();
tr.advance_click = true;
let mut in_transition = false;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if !in_transition && local == b"transition" {
in_transition = true;
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match key {
b"spd" => {
tr.speed = match v.as_ref() {
"slow" => TransitionSpeed::Slow,
"fast" => TransitionSpeed::Fast,
_ => TransitionSpeed::Medium,
};
}
b"advClick" => {
tr.advance_click = v == "1" || v == "true";
}
b"advTm" => {
tr.advance_after_ms = v.parse::<u32>().ok();
}
_ => {}
}
}
} else if in_transition {
parse_transition_type_child(local, &e, &mut tr, false)?;
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if !in_transition && local == b"transition" {
in_transition = true;
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match key {
b"spd" => {
tr.speed = match v.as_ref() {
"slow" => TransitionSpeed::Slow,
"fast" => TransitionSpeed::Fast,
_ => TransitionSpeed::Medium,
};
}
b"advClick" => {
tr.advance_click = v == "1" || v == "true";
}
b"advTm" => {
tr.advance_after_ms = v.parse::<u32>().ok();
}
_ => {}
}
}
} else if in_transition {
parse_transition_type_child(local, &e, &mut tr, true)?;
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if in_transition && local == b"transition" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("transition parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(tr)
}
fn parse_transition_type_child(
local: &[u8],
e: &quick_xml::events::BytesStart<'_>,
tr: &mut Transition,
_is_empty: bool,
) -> crate::Result<()> {
let attr = |key: &[u8]| -> Option<String> {
for a in e.attributes().flatten() {
if a.key.as_ref() == key {
return Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
None
};
match local {
b"fade" => {
let thru_blk = attr(b"thruBlk")
.map(|v| v == "1" || v == "true")
.unwrap_or(false);
tr.transition_type = TransitionType::Fade { thru_blk };
}
b"push" => {
let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
tr.transition_type = TransitionType::Push { dir };
}
b"wipe" => {
let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
tr.transition_type = TransitionType::Wipe { dir };
}
b"split" => {
let orient = match attr(b"orient").as_deref() {
Some("vert") => SplitOrientation::Vertical,
_ => SplitOrientation::Horizontal,
};
let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
tr.transition_type = TransitionType::Split { orient, dir };
}
b"cover" => {
let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
tr.transition_type = TransitionType::Cover { dir };
}
b"pull" => {
let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
tr.transition_type = TransitionType::Pull { dir };
}
b"cut" => {
let thru_blk = attr(b"thruBlk")
.map(|v| v == "1" || v == "true")
.unwrap_or(false);
tr.transition_type = TransitionType::Cut { thru_blk };
}
b"zoom" => {
let dir = parse_transition_dir(&attr(b"dir").unwrap_or_default());
tr.transition_type = TransitionType::Zoom { dir };
}
b"morph" => {
let option = match attr(b"option").as_deref() {
Some("byWord") => MorphOption::ByWord,
Some("byChar") => MorphOption::ByChar,
_ => MorphOption::ByObject,
};
tr.transition_type = TransitionType::Morph { option };
}
_ => {
}
}
Ok(())
}
fn parse_transition_dir(s: &str) -> TransitionDirection {
match s {
"l" => TransitionDirection::Left,
"r" => TransitionDirection::Right,
"u" => TransitionDirection::Up,
"d" => TransitionDirection::Down,
"lu" => TransitionDirection::LeftUp,
"ld" => TransitionDirection::LeftDown,
"ru" => TransitionDirection::RightUp,
"rd" => TransitionDirection::RightDown,
_ => TransitionDirection::Right,
}
}
pub fn find_relationship_target(rels_xml: &str, rid: &str) -> crate::Result<String> {
let rels = crate::opc::rels::Relationships::from_xml(rels_xml)?;
rels.get(rid)
.map(|r| r.target.as_str().to_string())
.ok_or_else(|| crate::Error::opc(format!("relationship not found: {rid}")))
}
pub fn all_relationships(rels_xml: &str) -> crate::Result<Vec<(String, String, String)>> {
use crate::opc::rels::Relationships;
let rels = Relationships::from_xml(rels_xml)?;
Ok(rels
.iter()
.map(|r| {
(
r.id.clone(),
r.reltype.uri().to_string(),
r.target.as_str().to_string(),
)
})
.collect())
}
pub fn parse_sp(xml: &str) -> crate::Result<Sp> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut sp = Sp::default();
let mut state = 0u8;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match state {
0 if local == b"sp" => {
state = 1;
}
1 if local == b"nvSpPr" => {
let mut nv_depth = 1i32;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e2)) => {
nv_depth += 1;
let name2 = e2.name();
let local2 = local_name(name2.as_ref());
if local2 == b"cNvSpPr" {
for a in e2.attributes().flatten() {
if a.key.as_ref() == b"txBox" {
let v = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default();
if v == "1" || v == "true" {
sp.c_nv_sp_pr_tx_box = true;
}
}
}
}
}
Ok(Event::End(_)) => {
nv_depth -= 1;
if nv_depth == 0 {
break;
}
}
Ok(Event::Empty(e2)) => {
let name2 = e2.name();
let local2 = local_name(name2.as_ref());
if local2 == b"cNvPr" {
for a in e2.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
if let Ok(v) = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse::<u32>()
{
sp.id = v;
}
}
b"name" => {
sp.name = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
} else if local2 == b"cNvSpPr" {
for a in e2.attributes().flatten() {
if a.key.as_ref() == b"txBox" {
let v = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default();
if v == "1" || v == "true" {
sp.c_nv_sp_pr_tx_box = true;
}
}
}
} else if local2 == b"spLocks" {
sp.locks = Some(parse_sp_locks_attrs(&e2));
} else if local2 == b"ph" {
sp.is_placeholder = true;
for a in e2.attributes().flatten() {
match a.key.as_ref() {
b"type" => {
sp.ph_type = Some(
String::from_utf8_lossy(a.value.as_ref())
.into_owned(),
)
}
b"idx" => {
if let Ok(v) = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse::<u32>()
{
sp.ph_idx = Some(v);
}
}
_ => {}
}
}
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
}
1 if local == b"spPr" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
sp.properties = parse_sppr(&inner)?;
state = 2;
}
_ if state < 2 && local == b"spPr" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
sp.properties = parse_sppr(&inner)?;
state = 2;
}
2 if local == b"txBody" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
sp.text = parse_txbody(&inner)?;
state = 3;
}
_ if state >= 2 && local == b"style" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
sp.style = Some(parse_shape_style(&inner)?);
}
_ => {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if state == 1 && local == b"ph" {
sp.is_placeholder = true;
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"type" => {
sp.ph_type =
Some(String::from_utf8_lossy(a.value.as_ref()).into_owned())
}
b"idx" => {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u32>()
{
sp.ph_idx = Some(v);
}
}
_ => {}
}
}
} else if state == 1 && local == b"cNvPr" {
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u32>()
{
sp.id = v;
}
}
b"name" => {
sp.name = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
} else if state == 1 && local == b"cNvSpPr" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"txBox" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if v == "1" || v == "true" {
sp.c_nv_sp_pr_tx_box = true;
}
}
}
} else if state == 1 && local == b"spPr" {
state = 2;
} else if state == 2 && local == b"prstGeom" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"prst" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(g) = v.parse::<PresetGeometry>() {
sp.properties.geometry = Some(Geometry::preset(g));
}
}
}
} else if state == 2 && local == b"custGeom" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
if let Ok(cg) = parse_custom_geometry(&inner) {
sp.properties.geometry = Some(Geometry::Custom(cg));
}
} else if state >= 2 && local == b"style" {
sp.style = Some(ShapeStyle::default());
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if state == 3 && local == b"sp" {
break;
}
if state == 2 && local == b"sp" {
break;
}
if state == 1 && local == b"sp" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("sp parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(sp)
}
pub fn parse_sppr(xml: &str) -> crate::Result<ShapeProperties> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut sp = ShapeProperties::default();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"spPr" {
} else if local == b"xfrm" {
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"rot" => {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i32>()
{
sp.xfrm.rot = Some(v);
}
}
b"flipH" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if v == "1" {
sp.xfrm.flip_h = true;
}
}
b"flipV" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if v == "1" {
sp.xfrm.flip_v = true;
}
}
_ => {}
}
}
parse_xfrm_into(&mut rd, &mut sp);
} else if local == b"prstGeom" {
let mut prst_val: Option<String> = None;
for a in e.attributes().flatten() {
if a.key.as_ref() == b"prst" {
prst_val = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
let inner = collect_full_element(&mut rd, e.into_owned())?;
if let Some(v) = prst_val {
if let Ok(g) = v.parse::<PresetGeometry>() {
let adjustments = parse_av_lst(&inner);
sp.geometry = Some(Geometry::Preset(g, adjustments));
}
}
} else if local == b"custGeom" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
if let Ok(cg) = parse_custom_geometry(&inner) {
sp.geometry = Some(Geometry::Custom(cg));
}
} else if local == b"noFill" {
sp.fill = Fill::None;
} else if local == b"solidFill" {
let color = parse_solid_fill(&mut rd, e.into_owned())?;
sp.fill = Fill::Solid(color);
} else if local == b"gradFill" {
let grad = parse_grad_fill(&mut rd, e.into_owned())?;
sp.fill = Fill::Gradient(grad);
} else if local == b"pattFill" {
let patt = parse_patt_fill(&mut rd, e.into_owned())?;
sp.fill = Fill::Pattern(patt);
} else if local == b"blipFill" {
let (rid, mode) = parse_blip_fill(&mut rd, e.into_owned())?;
if !rid.is_empty() {
sp.fill = Fill::Blip { rid, mode };
} else {
}
} else if local == b"ln" {
let ln = parse_ln(&mut rd, e.into_owned())?;
sp.line = Some(ln);
} else if local == b"effectLst" {
let effects = parse_effect_lst(&mut rd, e.into_owned())?;
sp.effects = Some(effects);
} else if local == b"scene3d" {
let scene = parse_scene_3d(&mut rd, e.into_owned())?;
sp.scene3d = Some(scene);
} else if local == b"sp3d" {
let sp3d = parse_sp_3d(&mut rd, e.into_owned())?;
sp.sp3d = Some(sp3d);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"prstGeom" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"prst" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(g) = v.parse::<PresetGeometry>() {
sp.geometry = Some(Geometry::preset(g));
}
}
}
} else if local == b"noFill" {
sp.fill = Fill::None;
} else if local == b"custGeom" {
sp.geometry = Some(Geometry::Custom(CustomGeometry::default()));
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("spPr parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(sp)
}
fn parse_xfrm_into<R: std::io::BufRead>(rd: &mut Reader<R>, sp: &mut ShapeProperties) {
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"off" {
let (mut x, mut y) = (None, None);
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"x" => {
x = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
b"y" => {
y = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
_ => {}
}
}
sp.xfrm.off_x = x.map(Emu);
sp.xfrm.off_y = y.map(Emu);
} else if local == b"ext" {
let (mut cx, mut cy) = (None, None);
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"cx" => {
cx = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
b"cy" => {
cy = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
_ => {}
}
}
sp.xfrm.ext_cx = cx.map(Emu);
sp.xfrm.ext_cy = cy.map(Emu);
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"xfrm" {
return;
}
}
Ok(Event::Eof) => return,
_ => {}
}
buf.clear();
}
}
fn parse_av_lst(xml: &str) -> Vec<crate::oxml::sppr::AdjustmentValue> {
use crate::oxml::sppr::AdjustmentValue;
let mut result = Vec::new();
let mut rd = Reader::from_str(xml);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
if local_name(e.name().as_ref()) == b"gd" {
let mut name = String::new();
let mut fmla = String::new();
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"name" => name = v.to_string(),
b"fmla" => fmla = v.to_string(),
_ => {}
}
}
if let Some(raw) = parse_fmla_val(&fmla) {
result.push(AdjustmentValue::new(name, raw));
}
}
}
Ok(Event::Start(e)) => {
if local_name(e.name().as_ref()) == b"gd" {
let mut name = String::new();
let mut fmla = String::new();
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"name" => name = v.to_string(),
b"fmla" => fmla = v.to_string(),
_ => {}
}
}
let _ = collect_full_element(&mut rd, e.into_owned());
if let Some(raw) = parse_fmla_val(&fmla) {
result.push(AdjustmentValue::new(name, raw));
}
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
result
}
fn parse_fmla_val(fmla: &str) -> Option<i64> {
let trimmed = fmla.trim();
if let Some(rest) = trimmed.strip_prefix("val ") {
rest.trim().parse::<i64>().ok()
} else if let Some(rest) = trimmed.strip_prefix("val") {
rest.trim().parse::<i64>().ok()
} else {
None
}
}
fn parse_custom_geometry(xml: &str) -> crate::Result<CustomGeometry> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut geom = CustomGeometry::default();
let mut state: u8 = 0;
let mut current_path: Option<Path> = None;
let mut bez_pts: Vec<(i64, i64)> = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let ev_name = e.name();
let local = local_name(ev_name.as_ref());
match state {
0 => match local {
b"fill" => {
let text = read_element_text(&mut rd, b"fill");
geom.fill = Some(text);
}
b"stroke" => {
let text = read_element_text(&mut rd, b"stroke");
geom.stroke = Some(text);
}
b"rect" => {
geom.rect = Some(parse_geom_rect(&e));
}
b"pathLst" => {
state = 1;
}
b"avLst" => {
}
_ => {}
},
1 => {
if local == b"path" {
let mut p = Path::default();
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
match key {
b"w" => p.width = v.parse().unwrap_or(0),
b"h" => p.height = v.parse().unwrap_or(0),
b"fill" => p.fill = Some(v),
b"stroke" => p.stroke = Some(v),
_ => {}
}
}
current_path = Some(p);
state = 2;
}
}
2 => {
match local {
b"moveTo" | b"lnTo" => {
let pt = read_single_pt(&mut rd, local);
if let Some((x, y)) = pt {
let seg = if local == b"moveTo" {
PathSegment::MoveTo { x, y }
} else {
PathSegment::LineTo { x, y }
};
if let Some(p) = &mut current_path {
p.segments.push(seg);
}
}
}
b"cubicBezTo" => {
bez_pts.clear();
read_multiple_pts(&mut rd, b"cubicBezTo", &mut bez_pts);
if bez_pts.len() >= 3 {
let seg = PathSegment::CubicBezTo {
x1: bez_pts[0].0,
y1: bez_pts[0].1,
x2: bez_pts[1].0,
y2: bez_pts[1].1,
x3: bez_pts[2].0,
y3: bez_pts[2].1,
};
if let Some(p) = &mut current_path {
p.segments.push(seg);
}
}
}
b"quadBezTo" => {
bez_pts.clear();
read_multiple_pts(&mut rd, b"quadBezTo", &mut bez_pts);
if bez_pts.len() >= 2 {
let seg = PathSegment::QuadBezTo {
x1: bez_pts[0].0,
y1: bez_pts[0].1,
x2: bez_pts[1].0,
y2: bez_pts[1].1,
};
if let Some(p) = &mut current_path {
p.segments.push(seg);
}
}
}
b"arcTo" => {
let seg = parse_arc_to(&e);
if let Some(p) = &mut current_path {
p.segments.push(seg);
}
}
_ => {}
}
}
_ => {}
}
}
Ok(Event::Empty(e)) => {
let ev_name = e.name();
let local = local_name(ev_name.as_ref());
match state {
0 => {
if local == b"rect" {
geom.rect = Some(parse_geom_rect(&e));
}
}
2 => {
if local == b"close" {
if let Some(p) = &mut current_path {
p.segments.push(PathSegment::Close);
}
} else if local == b"arcTo" {
let seg = parse_arc_to(&e);
if let Some(p) = &mut current_path {
p.segments.push(seg);
}
} else if local == b"moveTo" || local == b"lnTo" {
}
}
_ => {}
}
}
Ok(Event::End(e)) => {
let ev_name = e.name();
let local = local_name(ev_name.as_ref());
match state {
2 => {
if local == b"path" {
if let Some(p) = current_path.take() {
geom.path_list.push(p);
}
state = 1;
}
}
1 => {
if local == b"pathLst" {
state = 0;
}
}
0 if local == b"custGeom" => {
return Ok(geom);
}
_ => {}
}
}
Ok(Event::Eof) => return Ok(geom),
Err(e) => return Err(crate::Error::Xml(format!("parse_custom_geometry: {e}"))),
_ => {}
}
buf.clear();
}
}
fn read_element_text<R: std::io::BufRead>(rd: &mut Reader<R>, expected_end: &[u8]) -> String {
let mut buf = Vec::new();
let mut text = String::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Text(t)) => {
text.push_str(&t.decode().unwrap_or_default());
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == expected_end {
return text;
}
}
Ok(Event::Eof) => return text,
_ => {}
}
buf.clear();
}
}
fn parse_geom_rect(e: &quick_xml::events::BytesStart<'_>) -> GeomRect {
let mut r = GeomRect::default();
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
match a.key.as_ref() {
b"l" => r.l = v,
b"t" => r.t = v,
b"r" => r.r = v,
b"b" => r.b = v,
_ => {}
}
}
r
}
fn read_single_pt<R: std::io::BufRead>(rd: &mut Reader<R>, parent: &[u8]) -> Option<(i64, i64)> {
let mut buf = Vec::new();
let mut result = None;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let ev_name = e.name();
if local_name(ev_name.as_ref()) == b"pt" {
result = parse_pt_attrs(&e);
}
}
Ok(Event::Start(e)) => {
let ev_name = e.name();
if local_name(ev_name.as_ref()) == b"pt" {
result = parse_pt_attrs(&e);
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == parent {
return result;
}
}
Ok(Event::Eof) => return result,
_ => {}
}
buf.clear();
}
}
fn read_multiple_pts<R: std::io::BufRead>(
rd: &mut Reader<R>,
parent: &[u8],
pts: &mut Vec<(i64, i64)>,
) {
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let ev_name = e.name();
if local_name(ev_name.as_ref()) == b"pt" {
if let Some(p) = parse_pt_attrs(&e) {
pts.push(p);
}
}
}
Ok(Event::Start(e)) => {
let ev_name = e.name();
if local_name(ev_name.as_ref()) == b"pt" {
if let Some(p) = parse_pt_attrs(&e) {
pts.push(p);
}
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == parent {
return;
}
}
Ok(Event::Eof) => return,
_ => {}
}
buf.clear();
}
}
fn parse_pt_attrs(e: &quick_xml::events::BytesStart<'_>) -> Option<(i64, i64)> {
let mut x: Option<i64> = None;
let mut y: Option<i64> = None;
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"x" => x = v.parse().ok(),
b"y" => y = v.parse().ok(),
_ => {}
}
}
match (x, y) {
(Some(x), Some(y)) => Some((x, y)),
_ => None,
}
}
fn parse_arc_to(e: &quick_xml::events::BytesStart<'_>) -> PathSegment {
let mut w_r: i64 = 0;
let mut h_r: i64 = 0;
let mut st_ang: i32 = 0;
let mut sw_ang: i32 = 0;
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"wR" => w_r = v.parse().unwrap_or(0),
b"hR" => h_r = v.parse().unwrap_or(0),
b"stAng" => st_ang = v.parse().unwrap_or(0),
b"swAng" => sw_ang = v.parse().unwrap_or(0),
_ => {}
}
}
PathSegment::ArcTo {
w_r,
h_r,
st_ang,
sw_ang,
}
}
fn parse_solid_fill<R: std::io::BufRead>(
rd: &mut Reader<R>,
_start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<Color> {
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"srgbClr" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if v.len() == 6 {
let r = u8::from_str_radix(&v[0..2], 16).unwrap_or(0);
let g = u8::from_str_radix(&v[2..4], 16).unwrap_or(0);
let b = u8::from_str_radix(&v[4..6], 16).unwrap_or(0);
return Ok(Color::RGB(RGBColor(r, g, b)));
}
}
}
} else if local == b"schemeClr" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(sc) = v.parse::<SchemeColor>() {
return Ok(Color::Scheme(sc));
}
}
}
} else if local == b"prstClr" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(p) = v.parse::<PresetColor>() {
return Ok(Color::Preset(p));
}
}
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
if local_name(name.as_ref()) == b"solidFill" {
return Ok(Color::None);
}
}
Ok(Event::Eof) => return Ok(Color::None),
_ => {}
}
buf.clear();
}
}
fn parse_grad_fill<R: std::io::BufRead>(
rd: &mut Reader<R>,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<GradientFill> {
let mut flip = None;
let mut rot_with_shape = None;
for a in start.attributes().flatten() {
match a.key.as_ref() {
b"flip" => {
flip = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
b"rotWithShape" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
rot_with_shape = Some(v == "1");
}
_ => {}
}
}
let mut stops: Vec<GradientStop> = Vec::new();
let mut gradient_type = GradientType::Linear(0);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"gsLst" {
let mut gs_depth = 1i32;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(ref e2)) => {
if local_name(e2.name().as_ref()) == b"gs" && gs_depth == 1 {
let mut pos: u32 = 0;
for a in e2.attributes().flatten() {
if a.key.as_ref() == b"pos" {
pos = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse()
.unwrap_or(0);
}
}
gs_depth += 1;
let color = parse_color_child(rd, b"gs")?;
stops.push(GradientStop { pos, color });
} else {
gs_depth += 1;
}
}
Ok(Event::End(ref e2)) => {
let n2 = e2.name();
let ln = local_name(n2.as_ref());
if ln == b"gs" {
gs_depth -= 1;
} else if ln == b"gsLst" {
break;
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
} else if local == b"lin" {
let mut ang: i32 = 0;
for a in e.attributes().flatten() {
if a.key.as_ref() == b"ang" {
ang = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.unwrap_or(0);
}
}
gradient_type = GradientType::Linear(ang);
if e.is_empty() {
} else {
let _ = collect_full_element(rd, e.into_owned())?;
}
} else if local == b"path" {
let mut path_type = GradientPath::Circle;
for a in e.attributes().flatten() {
if a.key.as_ref() == b"path" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
path_type = match v.as_ref() {
"circle" => GradientPath::Circle,
"rect" => GradientPath::Rect,
"shape" => GradientPath::Shape,
_ => GradientPath::Circle,
};
}
}
gradient_type = GradientType::Path(path_type);
let _ = collect_full_element(rd, e.into_owned())?;
} else {
let _ = collect_full_element(rd, e.into_owned())?;
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"lin" {
let mut ang: i32 = 0;
for a in e.attributes().flatten() {
if a.key.as_ref() == b"ang" {
ang = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.unwrap_or(0);
}
}
gradient_type = GradientType::Linear(ang);
} else if local == b"path" {
let mut path_type = GradientPath::Circle;
for a in e.attributes().flatten() {
if a.key.as_ref() == b"path" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
path_type = match v.as_ref() {
"circle" => GradientPath::Circle,
"rect" => GradientPath::Rect,
"shape" => GradientPath::Shape,
_ => GradientPath::Circle,
};
}
}
gradient_type = GradientType::Path(path_type);
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"gradFill" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("gradFill parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(GradientFill {
stops,
gradient_type,
flip,
rot_with_shape,
})
}
fn parse_patt_fill<R: std::io::BufRead>(
rd: &mut Reader<R>,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<PatternFill> {
let mut prst = String::new();
for a in start.attributes().flatten() {
if a.key.as_ref() == b"prst" {
prst = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
let mut fg_color = Color::None;
let mut bg_color = Color::None;
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"fgClr" {
fg_color = parse_color_child(rd, b"fgClr")?;
} else if local == b"bgClr" {
bg_color = parse_color_child(rd, b"bgClr")?;
} else {
let _ = collect_full_element(rd, e.into_owned())?;
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"pattFill" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("pattFill parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(PatternFill {
prst,
fg_color,
bg_color,
})
}
fn parse_blip_fill<R: std::io::BufRead>(
rd: &mut Reader<R>,
_start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<(String, crate::oxml::sppr::BlipFillMode)> {
let mut rid = String::new();
let mut mode = crate::oxml::sppr::BlipFillMode::default(); let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"blip" {
for a in e.attributes().flatten() {
let key = a.key.as_ref();
if key == b"r:embed" || key.ends_with(b":embed") {
rid = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
let _ = collect_full_element(rd, e.into_owned())?;
} else if local == b"stretch" {
let _ = collect_full_element(rd, e.into_owned())?;
mode = crate::oxml::sppr::BlipFillMode::Stretch;
} else if local == b"tile" {
mode = parse_tile_attrs(&e);
let _ = collect_full_element(rd, e.into_owned())?;
} else if local == b"srcRect" {
let _ = collect_full_element(rd, e.into_owned())?;
} else {
let _ = collect_full_element(rd, e.into_owned())?;
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"blip" {
for a in e.attributes().flatten() {
let key = a.key.as_ref();
if key == b"r:embed" || key.ends_with(b":embed") {
rid = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
} else if local == b"stretch" {
mode = crate::oxml::sppr::BlipFillMode::Stretch;
} else if local == b"tile" {
mode = parse_tile_attrs(&e);
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"blipFill" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("blipFill parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok((rid, mode))
}
fn parse_color_child<R: std::io::BufRead>(
rd: &mut Reader<R>,
parent: &[u8],
) -> crate::Result<Color> {
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if let Some(c) = parse_color_from_event(&e) {
return Ok(c);
}
let _ = local;
}
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"srgbClr" || local == b"schemeClr" || local == b"prstClr" {
if let Some(c) = parse_color_from_event(&e) {
let _ = collect_full_element(rd, e.into_owned())?;
return Ok(c);
}
} else {
let _ = collect_full_element(rd, e.into_owned())?;
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == parent {
return Ok(Color::None);
}
}
Ok(Event::Eof) => return Ok(Color::None),
Err(_) => return Ok(Color::None),
_ => {}
}
buf.clear();
}
}
fn parse_color_from_event(e: &quick_xml::events::BytesStart<'_>) -> Option<Color> {
let name = e.name();
let local = local_name(name.as_ref());
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if local == b"srgbClr" && v.len() == 6 {
let r = u8::from_str_radix(&v[0..2], 16).unwrap_or(0);
let g = u8::from_str_radix(&v[2..4], 16).unwrap_or(0);
let b = u8::from_str_radix(&v[4..6], 16).unwrap_or(0);
return Some(Color::RGB(RGBColor(r, g, b)));
} else if local == b"schemeClr" {
if let Ok(sc) = v.parse::<SchemeColor>() {
return Some(Color::Scheme(sc));
}
} else if local == b"prstClr" {
if let Ok(p) = v.parse::<PresetColor>() {
return Some(Color::Preset(p));
}
}
}
}
None
}
fn parse_ln<R: std::io::BufRead>(
rd: &mut Reader<R>,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<Line> {
let mut ln = Line::default();
for a in start.attributes().flatten() {
match a.key.as_ref() {
b"w" => {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i64>()
{
ln.width = Some(Emu(v));
}
}
b"cap" => {
ln.cap = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
b"cmpd" => {
ln.compound = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
_ => {}
}
}
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"solidFill" {
ln.color = parse_solid_fill(rd, e.into_owned())?;
} else if local == b"noFill" {
ln.no_fill = true;
let _ = collect_full_element(rd, e.into_owned());
} else if local == b"prstDash" {
parse_prstdash_into(rd, &mut ln);
} else if local == b"gradFill" {
let grad = parse_grad_fill(rd, e.into_owned())?;
ln.fill = Fill::Gradient(grad);
} else if local == b"pattFill" {
let patt = parse_patt_fill(rd, e.into_owned())?;
ln.fill = Fill::Pattern(patt);
} else if local == b"headEnd" {
ln.head_end = Some(parse_arrow_head(&e));
let _ = collect_full_element(rd, e.into_owned());
} else if local == b"tailEnd" {
ln.tail_end = Some(parse_arrow_head(&e));
let _ = collect_full_element(rd, e.into_owned());
} else if local == b"round" {
ln.join = Some(LineJoin::Round);
let _ = collect_full_element(rd, e.into_owned());
} else if local == b"miter" {
let mut lim = 800000i32;
for a in e.attributes().flatten() {
if a.key.as_ref() == b"lim" {
lim = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.unwrap_or(800000);
}
}
ln.join = Some(LineJoin::Miter(lim));
let _ = collect_full_element(rd, e.into_owned());
} else if local == b"bevel" {
ln.join = Some(LineJoin::Bevel);
let _ = collect_full_element(rd, e.into_owned());
} else {
let _ = collect_full_element(rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"noFill" {
ln.no_fill = true;
} else if local == b"headEnd" {
ln.head_end = Some(parse_arrow_head(&e));
} else if local == b"tailEnd" {
ln.tail_end = Some(parse_arrow_head(&e));
} else if local == b"round" {
ln.join = Some(LineJoin::Round);
} else if local == b"miter" {
let mut lim = 800000i32;
for a in e.attributes().flatten() {
if a.key.as_ref() == b"lim" {
lim = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.unwrap_or(800000);
}
}
ln.join = Some(LineJoin::Miter(lim));
} else if local == b"bevel" {
ln.join = Some(LineJoin::Bevel);
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"ln" {
return Ok(ln);
}
}
Ok(Event::Eof) => return Ok(ln),
_ => {}
}
buf.clear();
}
}
fn parse_arrow_head(e: &quick_xml::events::BytesStart<'_>) -> ArrowHead {
let mut arrow_type = ArrowType::None;
let mut width = ArrowSize::Medium;
let mut length = ArrowSize::Medium;
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"type" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
arrow_type = match v.as_ref() {
"none" => ArrowType::None,
"triangle" => ArrowType::Triangle,
"stealth" => ArrowType::Stealth,
"diamond" => ArrowType::Diamond,
"oval" => ArrowType::Oval,
"arrow" => ArrowType::Arrow,
_ => ArrowType::None,
};
}
b"w" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
width = match v.as_ref() {
"sm" => ArrowSize::Small,
"lg" => ArrowSize::Large,
_ => ArrowSize::Medium,
};
}
b"len" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
length = match v.as_ref() {
"sm" => ArrowSize::Small,
"lg" => ArrowSize::Large,
_ => ArrowSize::Medium,
};
}
_ => {}
}
}
ArrowHead {
arrow_type,
width,
length,
}
}
fn parse_prstdash_into<R: std::io::BufRead>(rd: &mut Reader<R>, ln: &mut Line) {
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"prst" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(d) = v.parse::<crate::oxml::sppr::Dash>() {
ln.dash = Some(d);
}
}
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
if local_name(name.as_ref()) == b"prstDash" {
return;
}
}
Ok(Event::Eof) => return,
_ => {}
}
buf.clear();
}
}
pub fn parse_txbody(xml: &str) -> crate::Result<TextBody> {
let mut tb = TextBody::new();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut p_bufs: Vec<String> = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"txBody" {
} else if local == b"p" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
p_bufs.push(inner);
} else if local == b"bodyPr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
let bp = parse_body_pr(&inner)?;
tb.body_properties = Some(bp);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"bodyPr" {
let bp = parse_body_pr_attrs(&e);
tb.body_properties = Some(bp);
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("txBody parse: {e}"))),
_ => {}
}
buf.clear();
}
for p_xml in p_bufs {
if let Ok(p) = parse_paragraph(&p_xml) {
tb.paragraphs.push(p);
}
}
Ok(tb)
}
fn parse_effect_lst<R: std::io::BufRead>(
rd: &mut Reader<R>,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<EffectList> {
let mut effects = EffectList::default();
let mut buf = Vec::new();
let _ = start;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match local {
b"outerShdw" => {
let shdw = parse_shadow_attrs(&e, false);
let color = parse_color_child(rd, b"outerShdw")?;
let mut shdw = shdw;
shdw.color = color;
effects.outer_shadow = Some(shdw);
}
b"innerShdw" => {
let shdw = parse_shadow_attrs(&e, true);
let color = parse_color_child(rd, b"innerShdw")?;
let mut shdw = shdw;
shdw.color = color;
effects.inner_shadow = Some(shdw);
}
b"glow" => {
let mut glow = GlowEffect::default();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"rad" {
glow.rad = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.unwrap_or(0);
}
}
glow.color = parse_color_child(rd, b"glow")?;
effects.glow = Some(glow);
}
b"softEdge" => {
let mut se = SoftEdgeEffect::default();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"rad" {
se.rad = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.unwrap_or(0);
}
}
let _ = collect_full_element(rd, e.into_owned());
effects.soft_edge = Some(se);
}
b"reflection" => {
let mut refl = ReflectionEffect::default();
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match key {
b"blurRad" => refl.blur_rad = v.parse().ok(),
b"stA" => refl.st_a = v.parse().ok(),
b"stPos" => refl.st_pos = v.parse().ok(),
b"endA" => refl.end_a = v.parse().ok(),
b"endPos" => refl.end_pos = v.parse().ok(),
b"dist" => refl.dist = v.parse().ok(),
b"dir" => refl.dir = v.parse().ok(),
b"rotWithShape" => {
refl.rot_with_shape = Some(v == "1" || v == "true")
}
_ => {}
}
}
let _ = collect_full_element(rd, e.into_owned());
effects.reflection = Some(refl);
}
_ => {
let _ = collect_full_element(rd, e.into_owned());
}
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"outerShdw" {
let mut shdw = parse_shadow_attrs(&e, false);
shdw.color = Color::None;
effects.outer_shadow = Some(shdw);
} else if local == b"innerShdw" {
let mut shdw = parse_shadow_attrs(&e, true);
shdw.color = Color::None;
effects.inner_shadow = Some(shdw);
} else if local == b"glow" {
let mut glow = GlowEffect::default();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"rad" {
glow.rad = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.unwrap_or(0);
}
}
effects.glow = Some(glow);
} else if local == b"softEdge" {
let mut se = SoftEdgeEffect::default();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"rad" {
se.rad = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.unwrap_or(0);
}
}
effects.soft_edge = Some(se);
} else if local == b"reflection" {
let mut refl = ReflectionEffect::default();
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match key {
b"blurRad" => refl.blur_rad = v.parse().ok(),
b"stA" => refl.st_a = v.parse().ok(),
b"stPos" => refl.st_pos = v.parse().ok(),
b"endA" => refl.end_a = v.parse().ok(),
b"endPos" => refl.end_pos = v.parse().ok(),
b"dist" => refl.dist = v.parse().ok(),
b"dir" => refl.dir = v.parse().ok(),
b"rotWithShape" => refl.rot_with_shape = Some(v == "1" || v == "true"),
_ => {}
}
}
effects.reflection = Some(refl);
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"effectLst" {
return Ok(effects);
}
}
Ok(Event::Eof) => return Ok(effects),
Err(e) => return Err(crate::Error::Xml(format!("effectLst parse: {e}"))),
_ => {}
}
buf.clear();
}
}
fn parse_rotation_3d(e: &quick_xml::events::BytesStart<'_>) -> Rotation3d {
let mut rot = Rotation3d::default();
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"lat" => rot.lat = v.parse().unwrap_or(0),
b"lon" => rot.lon = v.parse().unwrap_or(0),
b"rev" => rot.rev = v.parse().unwrap_or(0),
_ => {}
}
}
rot
}
fn parse_scene_3d<R: std::io::BufRead>(
rd: &mut Reader<R>,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<Scene3d> {
let mut scene = Scene3d::default();
let mut buf = Vec::new();
let _ = start;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"camera" {
let mut camera = Camera::default();
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"prst" => camera.preset = CameraPreset::parse(&v),
b"fov" => camera.fov = v.parse().unwrap_or(0),
b"zoom" => camera.zoom = v.parse().unwrap_or(0),
_ => {}
}
}
let mut buf2 = Vec::new();
loop {
match rd.read_event_into(&mut buf2) {
Ok(Event::Start(e2)) => {
let n2 = e2.name();
if local_name(n2.as_ref()) == b"rot" {
camera.rotation = Some(parse_rotation_3d(&e2));
let _ = collect_full_element(rd, e2.into_owned());
}
}
Ok(Event::End(e2)) => {
let n2 = e2.name();
if local_name(n2.as_ref()) == b"camera" {
break;
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf2.clear();
}
scene.camera = camera;
} else if local == b"lightRig" {
let mut rig = LightRig::default();
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"rig" => rig.rig = LightRigType::parse(&v),
b"dir" => rig.dir = LightRigDirection::parse(&v),
_ => {}
}
}
let mut buf2 = Vec::new();
loop {
match rd.read_event_into(&mut buf2) {
Ok(Event::Start(e2)) => {
let n2 = e2.name();
if local_name(n2.as_ref()) == b"rot" {
rig.rotation = Some(parse_rotation_3d(&e2));
let _ = collect_full_element(rd, e2.into_owned());
}
}
Ok(Event::End(e2)) => {
let n2 = e2.name();
if local_name(n2.as_ref()) == b"lightRig" {
break;
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf2.clear();
}
scene.light_rig = rig;
} else if local == b"backdrop" {
let backdrop = parse_backdrop(rd, e.into_owned())?;
scene.backdrop = Some(backdrop);
} else {
let _ = collect_full_element(rd, e.into_owned());
}
}
Ok(Event::End(e)) => {
let name = e.name();
if local_name(name.as_ref()) == b"scene3d" {
return Ok(scene);
}
}
Ok(Event::Eof) => return Ok(scene),
Err(e) => return Err(crate::Error::Xml(format!("scene3d parse: {e}"))),
_ => {}
}
buf.clear();
}
}
fn parse_backdrop<R: std::io::BufRead>(
rd: &mut Reader<R>,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<Backdrop> {
let mut bd = Backdrop::default();
let mut buf = Vec::new();
let _ = start;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"anchor" {
let mut pt = Point3d::default();
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"x" => pt.x = v.parse().unwrap_or(0),
b"y" => pt.y = v.parse().unwrap_or(0),
b"z" => pt.z = v.parse().unwrap_or(0),
_ => {}
}
}
bd.anchor = Some(pt);
} else if local == b"floor" {
bd.floor = true;
} else if local == b"wall" {
bd.wall = true;
} else if local == b"l" {
bd.left = true;
} else if local == b"r" {
bd.right = true;
} else if local == b"t" {
bd.top = true;
} else if local == b"b" {
bd.bottom = true;
}
}
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"floor" {
bd.floor = true;
} else if local == b"wall" {
bd.wall = true;
} else if local == b"l" {
bd.left = true;
} else if local == b"r" {
bd.right = true;
} else if local == b"t" {
bd.top = true;
} else if local == b"b" {
bd.bottom = true;
}
let _ = collect_full_element(rd, e.into_owned());
}
Ok(Event::End(e)) => {
let name = e.name();
if local_name(name.as_ref()) == b"backdrop" {
return Ok(bd);
}
}
Ok(Event::Eof) => return Ok(bd),
Err(e) => return Err(crate::Error::Xml(format!("backdrop parse: {e}"))),
_ => {}
}
buf.clear();
}
}
fn parse_sp_3d<R: std::io::BufRead>(
rd: &mut Reader<R>,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<Sp3d> {
let mut sp3d = Sp3d::default();
for a in start.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"extrusionH" => sp3d.extrusion_h = v.parse().unwrap_or(0),
b"contourW" => sp3d.contour_w = v.parse().unwrap_or(0),
b"prstMaterial" => sp3d.prst_material = MaterialPreset::parse(&v),
_ => {}
}
}
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match local {
b"bevelT" => {
sp3d.bevel_top = Some(parse_bevel_attrs(&e));
}
b"bevelB" => {
sp3d.bevel_bottom = Some(parse_bevel_attrs(&e));
}
_ => {}
}
}
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match local {
b"bevelT" => {
let bvl = parse_bevel_attrs(&e);
let _ = collect_full_element(rd, e.into_owned());
sp3d.bevel_top = Some(bvl);
}
b"bevelB" => {
let bvl = parse_bevel_attrs(&e);
let _ = collect_full_element(rd, e.into_owned());
sp3d.bevel_bottom = Some(bvl);
}
b"extrusionClr" => {
let color = parse_color_child(rd, b"extrusionClr")?;
sp3d.extrusion_color = Some(color);
}
b"contourClr" => {
let color = parse_color_child(rd, b"contourClr")?;
sp3d.contour_color = Some(color);
}
_ => {
let _ = collect_full_element(rd, e.into_owned());
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
if local_name(name.as_ref()) == b"sp3d" {
return Ok(sp3d);
}
}
Ok(Event::Eof) => return Ok(sp3d),
Err(e) => return Err(crate::Error::Xml(format!("sp3d parse: {e}"))),
_ => {}
}
buf.clear();
}
}
fn parse_bevel_attrs(e: &quick_xml::events::BytesStart<'_>) -> Bevel {
let mut bvl = Bevel::default();
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"w" => bvl.w = v.parse().unwrap_or(0),
b"h" => bvl.h = v.parse().unwrap_or(0),
_ => {}
}
}
bvl
}
fn parse_shadow_attrs(e: &quick_xml::events::BytesStart<'_>, is_inner: bool) -> ShadowEffect {
let mut shdw = ShadowEffect::default();
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match key {
b"dir" => shdw.dir = v.parse().unwrap_or(0),
b"dist" => shdw.dist = v.parse().unwrap_or(0),
b"blurRad" => shdw.blur_rad = v.parse().unwrap_or(0),
b"rotWithShape" if !is_inner => {
shdw.rot_with_shape = Some(v == "1" || v == "true");
}
_ => {}
}
}
shdw
}
fn parse_body_pr(xml: &str) -> crate::Result<BodyProperties> {
let mut bp = BodyProperties::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"bodyPr" {
apply_body_pr_attrs(&mut bp, &e);
} else if local == b"spAutoFit" {
bp.sp_auto_fit = true;
let _ = collect_full_element(&mut rd, e.into_owned());
} else if local == b"normAutofit" {
bp.norm_autofit = true;
let _ = collect_full_element(&mut rd, e.into_owned());
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"bodyPr" {
apply_body_pr_attrs(&mut bp, &e);
} else if local == b"spAutoFit" {
bp.sp_auto_fit = true;
} else if local == b"normAutofit" {
bp.norm_autofit = true;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("bodyPr parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(bp)
}
fn parse_body_pr_attrs(e: &quick_xml::events::BytesStart<'_>) -> BodyProperties {
let mut bp = BodyProperties::default();
apply_body_pr_attrs(&mut bp, e);
bp
}
fn apply_body_pr_attrs(bp: &mut BodyProperties, e: &quick_xml::events::BytesStart<'_>) {
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"numCol" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
bp.num_cols = v.parse::<u32>().ok();
}
b"spcCol" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(n) = v.parse::<i64>() {
bp.col_spacing = Some(Emu(n));
}
}
b"anchor" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
bp.anchor = match v.as_ref() {
"t" => Some(MsoAnchor::Top),
"ctr" => Some(MsoAnchor::Middle),
"b" => Some(MsoAnchor::Bottom),
_ => None,
};
}
b"wrap" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
bp.wrap = match v.as_ref() {
"square" => Some(TextWrapping::Square),
"none" => Some(TextWrapping::None),
_ => None,
};
}
b"vert" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
bp.vertical = Some(v);
}
b"rot" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
bp.rotation = v.parse::<i32>().ok();
}
b"lIns" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(n) = v.parse::<i64>() {
bp.insets.get_or_insert_with(Default::default).left = Emu(n);
}
}
b"tIns" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(n) = v.parse::<i64>() {
bp.insets.get_or_insert_with(Default::default).top = Emu(n);
}
}
b"rIns" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(n) = v.parse::<i64>() {
bp.insets.get_or_insert_with(Default::default).right = Emu(n);
}
}
b"bIns" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(n) = v.parse::<i64>() {
bp.insets.get_or_insert_with(Default::default).bottom = Emu(n);
}
}
_ => {}
}
}
}
pub fn parse_paragraph(xml: &str) -> crate::Result<Paragraph> {
let mut p = Paragraph::new();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut in_ppr = false;
let mut ppr_done = false;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"p" {
} else if local == b"pPr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
p.properties = parse_paragraph_properties(&inner)?;
ppr_done = true;
} else if local == b"r" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
if let Ok(r) = parse_run(&inner) {
p.runs.push(r);
}
} else if local == b"fld" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
if let Ok(f) = parse_field(&inner) {
p.fields.push(f);
}
} else if local == b"br" {
p.runs.push(Run::line_break());
} else if local == b"endParaRPr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
p.end_properties = Some(parse_run_properties(&inner)?);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
in_ppr = false;
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"pPr" {
p.properties = parse_paragraph_properties_attrs(&e);
ppr_done = true;
} else if local == b"endParaRPr" {
p.end_properties = Some(parse_run_properties_from_bytes_start(&e));
} else if local == b"fld" {
if let Some(f) = parse_field_attrs(&e) {
p.fields.push(f);
}
}
}
Ok(Event::End(_)) => {
if !ppr_done {
in_ppr = false;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("paragraph parse: {e}"))),
_ => {}
}
buf.clear();
}
let _ = in_ppr;
Ok(p)
}
fn parse_paragraph_properties(xml: &str) -> crate::Result<ParagraphProperties> {
let mut ppr = ParagraphProperties::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"pPr" {
} else if local == b"lnSpc" {
parse_ln_spc_into(&mut rd, &mut ppr, e.into_owned())?;
} else if local == b"spcBef" {
parse_spc_bef_into(&mut rd, &mut ppr, e.into_owned())?;
} else if local == b"spcAft" {
parse_spc_aft_into(&mut rd, &mut ppr, e.into_owned())?;
} else if local == b"buNone" {
ppr.bullet = false;
ppr.bullet_style = Some(BulletStyle::None);
let _ = collect_full_element(&mut rd, e.into_owned());
} else if local == b"buChar" {
let bs = parse_bu_char(&e);
ppr.bullet = true;
ppr.bullet_style = Some(bs);
let _ = collect_full_element(&mut rd, e.into_owned());
} else if local == b"buAutoNum" {
let bs = parse_bu_auto_num(&e);
ppr.bullet = true;
ppr.bullet_style = Some(bs);
let _ = collect_full_element(&mut rd, e.into_owned());
} else if local == b"tabLst" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
ppr.tab_stops = parse_tab_lst(&inner)?;
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"buNone" {
ppr.bullet = false;
ppr.bullet_style = Some(BulletStyle::None);
} else if local == b"buChar" {
ppr.bullet = true;
ppr.bullet_style = Some(parse_bu_char(&e));
} else if local == b"buAutoNum" {
ppr.bullet = true;
ppr.bullet_style = Some(parse_bu_auto_num(&e));
} else if local == b"tab" {
if let Some(tab) = parse_tab(&e) {
ppr.tab_stops.push(tab);
}
} else {
apply_ppr_attrs(&e, &mut ppr);
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("pPr parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(ppr)
}
fn parse_paragraph_properties_attrs(e: &quick_xml::events::BytesStart<'_>) -> ParagraphProperties {
let mut ppr = ParagraphProperties::default();
apply_ppr_attrs(e, &mut ppr);
ppr
}
fn parse_bu_char(e: &quick_xml::events::BytesStart<'_>) -> BulletStyle {
let mut ch = String::new();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"char" {
ch = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
BulletStyle::Char { char: ch }
}
fn parse_bu_auto_num(e: &quick_xml::events::BytesStart<'_>) -> BulletStyle {
let mut auto_num_type = String::new();
let mut start_at: Option<u32> = None;
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"type" => {
auto_num_type = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
b"startAt" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
start_at = v.parse::<u32>().ok();
}
_ => {}
}
}
BulletStyle::AutoNum {
auto_num_type,
start_at,
}
}
fn parse_tab_lst(xml: &str) -> crate::Result<Vec<TabStop>> {
let mut tabs = Vec::new();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"tabLst" {
} else if local == b"tab" {
if let Some(tab) = parse_tab(&e) {
tabs.push(tab);
}
let _ = collect_full_element(&mut rd, e.into_owned());
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"tab" {
if let Some(tab) = parse_tab(&e) {
tabs.push(tab);
}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("tabLst parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(tabs)
}
fn parse_tab(e: &quick_xml::events::BytesStart<'_>) -> Option<TabStop> {
let mut pos: Option<i64> = None;
let mut alignment = TabAlignment::Left;
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"pos" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
pos = v.parse::<i64>().ok();
}
b"algn" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(al) = v.parse::<TabAlignment>() {
alignment = al;
}
}
_ => {}
}
}
pos.map(|p| TabStop {
pos: Emu(p),
alignment,
})
}
fn parse_field(xml: &str) -> crate::Result<Field> {
let mut field = Field::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"fld" {
apply_field_attrs(&mut field, &e);
} else if local == b"rPr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
field.properties = parse_run_properties(&inner)?;
} else if local == b"t" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
field.text = extract_text_content(&inner);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"fld" {
apply_field_attrs(&mut field, &e);
} else if local == b"rPr" {
field.properties = parse_run_properties_from_bytes_start(&e);
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("fld parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(field)
}
fn parse_field_attrs(e: &quick_xml::events::BytesStart<'_>) -> Option<Field> {
let mut field = Field::default();
let mut has_type = false;
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
field.id = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
b"type" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
field.field_type = FieldType::from_str_value(&v);
has_type = true;
}
_ => {}
}
}
if has_type {
Some(field)
} else {
None
}
}
fn apply_field_attrs(field: &mut Field, e: &quick_xml::events::BytesStart<'_>) {
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
field.id = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
b"type" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
field.field_type = FieldType::from_str_value(&v);
}
_ => {}
}
}
}
fn extract_text_content(xml: &str) -> String {
let mut text = String::new();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Text(t)) => {
text.push_str(&t.decode().unwrap_or_default());
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
text
}
fn apply_ppr_attrs(e: &quick_xml::events::BytesStart<'_>, ppr: &mut ParagraphProperties) {
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"algn" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if let Ok(al) = v.parse::<Alignment>() {
ppr.alignment = Some(al);
}
}
b"lvl" => {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u8>()
{
ppr.level = v;
}
}
b"indent" | b"marL" => {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i64>()
{
ppr.indent.left = Some(Emu(v));
}
}
#[allow(unreachable_patterns)]
b"indent" | b"r" => {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i64>()
{
ppr.indent.right = Some(Emu(v));
}
}
#[allow(unreachable_patterns)]
b"indent" | b"firstLine" => {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i64>()
{
ppr.indent.first_line = Some(Emu(v));
}
}
_ => {}
}
}
}
fn parse_ln_spc_into<R: std::io::BufRead>(
rd: &mut Reader<R>,
ppr: &mut ParagraphProperties,
_start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<()> {
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"spcPct" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i32>()
{
ppr.line_spacing_pct = Some(v);
}
}
}
} else if local == b"spcPts" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i32>()
{
ppr.line_spacing = Some(v);
}
}
}
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"lnSpc" {
return Ok(());
}
}
Ok(Event::Eof) => return Ok(()),
_ => {}
}
buf.clear();
}
}
fn parse_spc_bef_into<R: std::io::BufRead>(
rd: &mut Reader<R>,
ppr: &mut ParagraphProperties,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<()> {
let _ = start;
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"spcPts" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i32>()
{
ppr.space_before = Some(Emu(v as i64 * 100)); }
}
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
if local_name(name.as_ref()) == b"spcBef" {
return Ok(());
}
}
Ok(Event::Eof) => return Ok(()),
_ => {}
}
buf.clear();
}
}
fn parse_spc_aft_into<R: std::io::BufRead>(
rd: &mut Reader<R>,
ppr: &mut ParagraphProperties,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<()> {
let _ = start;
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"spcPts" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i32>()
{
ppr.space_after = Some(Emu(v as i64 * 100));
}
}
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
if local_name(name.as_ref()) == b"spcAft" {
return Ok(());
}
}
Ok(Event::Eof) => return Ok(()),
_ => {}
}
buf.clear();
}
}
pub fn parse_run(xml: &str) -> crate::Result<Run> {
let mut run = Run::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut rpr_props: Option<RunProperties> = None;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"r" {
} else if local == b"rPr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
rpr_props = Some(parse_run_properties(&inner)?);
} else if local == b"t" {
let txt = collect_inner_text_only(&mut rd)?;
run.text = txt;
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"rPr" {
rpr_props = Some(parse_run_properties_from_bytes_start(&e));
} else if local == b"t" {
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("run parse: {e}"))),
_ => {}
}
buf.clear();
}
if let Some(p) = rpr_props {
run.properties = p;
}
Ok(run)
}
pub fn collect_full_element<R: std::io::BufRead>(
rd: &mut Reader<R>,
start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<String> {
let mut out = String::new();
out.push('<');
let name = start.name();
out.push_str(&String::from_utf8_lossy(name.as_ref()));
for a in start.attributes().flatten() {
out.push(' ');
out.push_str(&String::from_utf8_lossy(a.key.as_ref()));
out.push_str("=\"");
out.push_str(
&a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default(),
);
out.push('"');
}
out.push('>');
let mut depth = 1i32;
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
depth += 1;
write_event_start(&mut out, &e);
}
Ok(Event::End(e)) => {
depth -= 1;
write_event_end(&mut out, &e);
if depth == 0 {
return Ok(out);
}
}
Ok(Event::Empty(e)) => {
write_event_empty(&mut out, &e);
}
Ok(Event::Text(t)) => {
out.push_str(&t.decode().unwrap_or_default());
}
Ok(Event::CData(c)) => {
out.push_str("<![CDATA[");
let cd = String::from_utf8_lossy(c.as_ref());
out.push_str(&cd);
out.push_str("]]>");
}
Ok(Event::Eof) => return Ok(out),
Err(e) => return Err(crate::Error::Xml(format!("collect: {e}"))),
_ => {}
}
buf.clear();
}
}
pub fn collect_full_element_skipping<R: std::io::BufRead>(
rd: &mut Reader<R>,
_start: quick_xml::events::BytesStart<'static>,
) {
let mut depth = 1i32;
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(_)) => {
depth += 1;
}
Ok(Event::End(_)) => {
depth -= 1;
if depth == 0 {
return;
}
}
Ok(Event::Eof) => return,
_ => {}
}
buf.clear();
}
}
fn write_event_start(out: &mut String, e: &quick_xml::events::BytesStart<'_>) {
out.push('<');
let name = e.name();
out.push_str(&String::from_utf8_lossy(name.as_ref()));
for a in e.attributes().flatten() {
out.push(' ');
out.push_str(&String::from_utf8_lossy(a.key.as_ref()));
out.push_str("=\"");
out.push_str(
&a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default(),
);
out.push('"');
}
out.push('>');
}
fn write_event_empty(out: &mut String, e: &quick_xml::events::BytesStart<'_>) {
out.push('<');
let name = e.name();
out.push_str(&String::from_utf8_lossy(name.as_ref()));
for a in e.attributes().flatten() {
out.push(' ');
out.push_str(&String::from_utf8_lossy(a.key.as_ref()));
out.push_str("=\"");
out.push_str(
&a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default(),
);
out.push('"');
}
out.push_str("/>");
}
fn write_event_end(out: &mut String, e: &quick_xml::events::BytesEnd<'_>) {
out.push_str("</");
let name = e.name();
out.push_str(&String::from_utf8_lossy(name.as_ref()));
out.push('>');
}
pub fn parse_run_properties(xml: &str) -> crate::Result<RunProperties> {
let mut rp = RunProperties::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"rPr" || local == b"endParaRPr" {
rp = parse_run_properties_from_bytes_start(&e);
} else if local == b"solidFill" {
rp.color = parse_solid_fill(&mut rd, e.into_owned())?;
} else if local == b"highlight" {
let color = parse_solid_fill(&mut rd, e.into_owned())?;
rp.highlight = Some(color);
} else if local == b"latin" {
rp.latin_font = Some(collect_attr_value(&mut rd, "typeface")?);
} else if local == b"ea" {
rp.eastasia_font = Some(collect_attr_value(&mut rd, "typeface")?);
} else if local == b"cs" {
rp.cs_font = Some(collect_attr_value(&mut rd, "typeface")?);
} else if local == b"hlinkClick" {
let hl = parse_hyperlink_attrs(&e);
collect_full_element_skipping(&mut rd, e.into_owned());
rp.hlink_click = Some(hl);
} else if local == b"hlinkHover" {
let hl = parse_hyperlink_attrs(&e);
collect_full_element_skipping(&mut rd, e.into_owned());
rp.hlink_hover = Some(hl);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"latin" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"typeface" {
rp.latin_font = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
} else if local == b"ea" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"typeface" {
rp.eastasia_font = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
} else if local == b"cs" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"typeface" {
rp.cs_font = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
} else if local == b"hlinkClick" {
rp.hlink_click = Some(parse_hyperlink_attrs(&e));
} else if local == b"hlinkHover" {
rp.hlink_hover = Some(parse_hyperlink_attrs(&e));
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("rPr parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(rp)
}
fn parse_hyperlink_attrs(e: &quick_xml::events::BytesStart<'_>) -> Hyperlink {
let mut hl = Hyperlink::default();
let mut has_any = false;
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
if key == b"r:id" || key.ends_with(b":id") {
hl.rid = Some(val);
has_any = true;
} else if key == b"tooltip" {
hl.tooltip = Some(val);
has_any = true;
} else if key == b"action" {
hl.action = Some(val);
has_any = true;
}
}
if !has_any {
hl.invalid = true;
}
hl
}
fn parse_tile_attrs(e: &quick_xml::events::BytesStart<'_>) -> crate::oxml::sppr::BlipFillMode {
let mut tx = None;
let mut ty = None;
let mut sx = None;
let mut sy = None;
let mut flip = None;
let mut algn = None;
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
match key {
b"tx" => tx = val.parse().ok(),
b"ty" => ty = val.parse().ok(),
b"sx" => sx = val.parse().ok(),
b"sy" => sy = val.parse().ok(),
b"flip" => flip = Some(val),
b"algn" => algn = Some(val),
_ => {}
}
}
crate::oxml::sppr::BlipFillMode::Tile {
tx,
ty,
sx,
sy,
flip,
algn,
}
}
fn parse_sp_locks_attrs(e: &quick_xml::events::BytesStart<'_>) -> ShapeLocks {
let mut locks = ShapeLocks::default();
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
let is_true = v == "1" || v == "true";
match key {
b"noGrp" => locks.no_grp = is_true,
b"noDrilldown" => locks.no_drilldown = is_true,
b"noSelect" => locks.no_select = is_true,
b"noChangeAspect" => locks.no_change_aspect = is_true,
b"noMove" => locks.no_move = is_true,
b"noResize" => locks.no_resize = is_true,
b"noRot" => locks.no_rot = is_true,
b"noEditPoints" => locks.no_edit_points = is_true,
b"noAdjustHandles" => locks.no_adjust_handles = is_true,
b"noChangeArrowheads" => locks.no_change_arrowheads = is_true,
b"noChangeShapeType" => locks.no_change_shape_type = is_true,
b"noCrop" => locks.no_crop = is_true,
_ => {}
}
}
locks
}
pub fn parse_shape_style(xml: &str) -> crate::Result<ShapeStyle> {
let mut style = ShapeStyle::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match local {
b"lnRef" => {
style.line_ref = Some(parse_style_ref(&mut rd, &e)?);
}
b"fillRef" => {
style.fill_ref = Some(parse_style_ref(&mut rd, &e)?);
}
b"effectRef" => {
style.effect_ref = Some(parse_style_ref(&mut rd, &e)?);
}
b"fontRef" => {
style.font_ref = Some(parse_style_ref(&mut rd, &e)?);
}
b"style" => {
}
_ => {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match local {
b"lnRef" => {
style.line_ref = Some(parse_style_ref_empty(&e));
}
b"fillRef" => {
style.fill_ref = Some(parse_style_ref_empty(&e));
}
b"effectRef" => {
style.effect_ref = Some(parse_style_ref_empty(&e));
}
b"fontRef" => {
style.font_ref = Some(parse_style_ref_empty(&e));
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("style parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(style)
}
fn parse_style_ref<R: std::io::BufRead>(
rd: &mut Reader<R>,
e: &quick_xml::events::BytesStart<'_>,
) -> crate::Result<StyleRef> {
let mut sr = parse_style_ref_empty(e);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e2)) => {
let name2 = e2.name();
let local = local_name(name2.as_ref());
if local == b"schemeClr" {
for a in e2.attributes().flatten() {
if a.key.as_ref() == b"val" {
sr.scheme_color = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
}
}
Ok(Event::End(_)) => return Ok(sr),
Ok(Event::Eof) => return Ok(sr),
_ => {}
}
buf.clear();
}
}
fn parse_style_ref_empty(e: &quick_xml::events::BytesStart<'_>) -> StyleRef {
let mut sr = StyleRef::default();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"idx" {
sr.idx = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
sr
}
fn collect_attr_value<R: std::io::BufRead>(rd: &mut Reader<R>, key: &str) -> crate::Result<String> {
let mut buf = Vec::new();
let mut val = String::new();
let key_b = key.as_bytes();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
for a in e.attributes().flatten() {
if a.key.as_ref() == key_b {
val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
}
Ok(Event::End(_)) => return Ok(val),
Ok(Event::Eof) => return Ok(val),
_ => {}
}
buf.clear();
}
}
fn local_name(name: &[u8]) -> &[u8] {
match name.iter().position(|&c| c == b':') {
Some(i) => &name[i + 1..],
None => name,
}
}
fn collect_inner_text_only<R: std::io::BufRead>(rd: &mut Reader<R>) -> crate::Result<String> {
let mut out = String::new();
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Text(t)) => {
out.push_str(&t.decode().unwrap_or_default());
}
Ok(Event::CData(c)) => {
out.push_str(&String::from_utf8_lossy(c.as_ref()));
}
Ok(Event::End(_)) => return Ok(out),
Ok(Event::Eof) => return Ok(out),
_ => {}
}
buf.clear();
}
}
pub fn parse_run_properties_from_bytes_start(
e: &quick_xml::events::BytesStart<'_>,
) -> RunProperties {
let mut rp = RunProperties::default();
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"sz" => {
if let Ok(p) = v.parse::<i32>() {
rp.size = Some(Pt(p as f64 / 100.0));
}
}
b"b" => {
if v == "1" || v == "true" {
rp.bold = true;
}
}
b"i" => {
if v == "1" || v == "true" {
rp.italic = true;
}
}
b"u" => {
if let Ok(u) = v.parse::<Underline>() {
rp.underline = Some(u);
}
}
b"strike" => {
if v == "sngStrike" {
rp.strike = true;
}
if v == "dblStrike" {
rp.strike_dbl = true;
}
}
b"baseline" => {
if let Ok(v) = v.parse::<i32>() {
rp.baseline = Some(v);
}
}
b"kern" => {
if let Ok(v) = v.parse::<i32>() {
rp.kerning = Some(v);
}
}
b"spc" => {
if let Ok(v) = v.parse::<i32>() {
rp.spc = Some(v);
}
}
b"lang" => {
rp.lang = Some(v.to_string());
}
_ => {}
}
}
rp
}
pub fn parse_pic(xml: &str) -> crate::Result<Pic> {
let mut pic = Pic::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut in_blipfill = false;
let mut blipfill_depth: i32 = 0;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"pic" {
} else if local == b"nvPicPr" {
let mut nv_depth = 1i32;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(_)) => nv_depth += 1,
Ok(Event::End(_)) => {
nv_depth -= 1;
if nv_depth == 0 {
break;
}
}
Ok(Event::Empty(e2)) => {
let name2 = e2.name();
let local2 = local_name(name2.as_ref());
if local2 == b"cNvPr" {
for a in e2.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
if let Ok(v) = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse::<u32>()
{
pic.id = v;
}
}
b"name" => {
pic.name = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
} else if local == b"blipFill" {
in_blipfill = true;
blipfill_depth = 1;
} else if local == b"spPr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
pic.properties = parse_sppr(&inner)?;
} else if in_blipfill && local == b"blip" {
for a in e.attributes().flatten() {
let key = a.key.as_ref();
if key == b"r:embed" || key.ends_with(b":embed") {
pic.rid = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
} else if in_blipfill && local == b"srcRect" {
let (mut l, mut t, mut r, mut b) = (None, None, None, None);
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"l" => l = v.parse().ok(),
b"t" => t = v.parse().ok(),
b"r" => r = v.parse().ok(),
b"b" => b = v.parse().ok(),
_ => {}
}
}
if let (Some(ll), Some(tt), Some(rr), Some(bb)) = (l, t, r, b) {
pic.src_rect = Some((ll, tt, rr, bb));
}
} else if in_blipfill && local == b"stretch" {
let _ = collect_full_element(&mut rd, e.into_owned())?;
pic.fill_mode = crate::oxml::sppr::BlipFillMode::Stretch;
} else if in_blipfill && local == b"tile" {
let mode = parse_tile_attrs(&e);
let _ = collect_full_element(&mut rd, e.into_owned())?;
pic.fill_mode = mode;
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if in_blipfill && local == b"blip" {
for a in e.attributes().flatten() {
let key = a.key.as_ref();
if key == b"r:embed" || key.ends_with(b":embed") {
pic.rid = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
} else if in_blipfill && local == b"srcRect" {
let (mut l, mut t, mut r, mut b) = (None, None, None, None);
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"l" => l = v.parse().ok(),
b"t" => t = v.parse().ok(),
b"r" => r = v.parse().ok(),
b"b" => b = v.parse().ok(),
_ => {}
}
}
if let (Some(ll), Some(tt), Some(rr), Some(bb)) = (l, t, r, b) {
pic.src_rect = Some((ll, tt, rr, bb));
}
} else if in_blipfill && local == b"stretch" {
pic.fill_mode = crate::oxml::sppr::BlipFillMode::Stretch;
} else if in_blipfill && local == b"tile" {
pic.fill_mode = parse_tile_attrs(&e);
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if in_blipfill && local == b"blipFill" {
blipfill_depth -= 1;
if blipfill_depth == 0 {
in_blipfill = false;
}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("pic parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(pic)
}
pub fn parse_cxn_sp(xml: &str) -> crate::Result<Connector> {
let mut cxn = Connector::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"cxnSp" {
} else if local == b"nvCxnSpPr" {
let mut nv_depth = 1i32;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(_)) => nv_depth += 1,
Ok(Event::End(_)) => {
nv_depth -= 1;
if nv_depth == 0 {
break;
}
}
Ok(Event::Empty(e2)) => {
let name2 = e2.name();
let local2 = local_name(name2.as_ref());
if local2 == b"cNvPr" {
for a in e2.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
if let Ok(v) = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse::<u32>()
{
cxn.id = v;
}
}
b"name" => {
cxn.name = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
} else if local2 == b"stCxn" {
let mut sid: Option<u32> = None;
let mut idx: Option<u32> = None;
for a in e2.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
sid = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse()
.ok();
}
b"idx" => {
idx = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse()
.ok();
}
_ => {}
}
}
if let (Some(s), Some(i)) = (sid, idx) {
cxn.st_cxn = Some((s, i));
}
} else if local2 == b"endCxn" {
let mut sid: Option<u32> = None;
let mut idx: Option<u32> = None;
for a in e2.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
sid = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse()
.ok();
}
b"idx" => {
idx = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse()
.ok();
}
_ => {}
}
}
if let (Some(s), Some(i)) = (sid, idx) {
cxn.end_cxn = Some((s, i));
}
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
} else if local == b"spPr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
cxn.properties = parse_sppr(&inner)?;
} else if local == b"style" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
cxn.style = Some(parse_shape_style(&inner)?);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"cxnSp" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("cxnSp parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(cxn)
}
pub fn parse_grp_sp(xml: &str) -> crate::Result<Group> {
let mut grp = Group::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut child_sp_bufs: Vec<String> = Vec::new();
let mut child_pic_bufs: Vec<String> = Vec::new();
let mut child_cxn_bufs: Vec<String> = Vec::new();
let mut child_grp_bufs: Vec<String> = Vec::new();
let mut child_gfx_bufs: Vec<String> = Vec::new();
let mut entered_root = false;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"grpSp" {
if !entered_root {
entered_root = true;
} else {
let inner = collect_full_element(&mut rd, e.into_owned())?;
child_grp_bufs.push(inner);
}
} else if local == b"nvGrpSpPr" {
let mut nv_depth = 1i32;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(_)) => nv_depth += 1,
Ok(Event::End(_)) => {
nv_depth -= 1;
if nv_depth == 0 {
break;
}
}
Ok(Event::Empty(e2)) => {
let name2 = e2.name();
let local2 = local_name(name2.as_ref());
if local2 == b"cNvPr" {
for a in e2.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
if let Ok(v) = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse::<u32>()
{
grp.id = v;
}
}
b"name" => {
grp.name = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
} else if local == b"grpSpPr" {
parse_grp_sppr_into(&mut rd, &mut grp, e.into_owned())?;
} else if local == b"sp" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
child_sp_bufs.push(inner);
} else if local == b"pic" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
child_pic_bufs.push(inner);
} else if local == b"cxnSp" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
child_cxn_bufs.push(inner);
} else if local == b"graphicFrame" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
child_gfx_bufs.push(inner);
} else if local == b"style" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
grp.style = Some(parse_shape_style(&inner)?);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"grpSp" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("grpSp parse: {e}"))),
_ => {}
}
buf.clear();
}
for s in child_sp_bufs {
if let Ok(sp) = parse_sp(&s) {
grp.children.push(GroupChild::Sp(sp));
}
}
for p in child_pic_bufs {
if let Ok(pic) = parse_pic(&p) {
grp.children.push(GroupChild::Pic(pic));
}
}
for c in child_cxn_bufs {
if let Ok(cxn) = parse_cxn_sp(&c) {
grp.children.push(GroupChild::CxnSp(cxn));
}
}
for g in child_grp_bufs {
if let Ok(sub) = parse_grp_sp(&g) {
grp.children.push(GroupChild::Group(Box::new(sub)));
}
}
for f in child_gfx_bufs {
if let Ok(frame) = parse_graphic_frame(&f) {
grp.children.push(GroupChild::GraphicFrame(frame));
}
}
Ok(grp)
}
fn parse_grp_sppr_into<R: std::io::BufRead>(
rd: &mut Reader<R>,
grp: &mut Group,
_start: quick_xml::events::BytesStart<'static>,
) -> crate::Result<()> {
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"off" {
let (mut x, mut y) = (None, None);
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"x" => {
x = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
b"y" => {
y = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
_ => {}
}
}
if let (Some(x), Some(y)) = (x, y) {
grp.off = (Emu(x), Emu(y));
}
} else if local == b"ext" {
let (mut cx, mut cy) = (None, None);
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"cx" => {
cx = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
b"cy" => {
cy = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
_ => {}
}
}
if let (Some(cx), Some(cy)) = (cx, cy) {
grp.ext = (Emu(cx), Emu(cy));
}
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"grpSpPr" {
return Ok(());
}
}
Ok(Event::Eof) => return Ok(()),
_ => {}
}
buf.clear();
}
}
pub fn parse_graphic_frame(xml: &str) -> crate::Result<GraphicFrame> {
let mut frame = GraphicFrame::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"graphicFrame" {
} else if local == b"nvGraphicFramePr" {
let mut nv_depth = 1i32;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(_)) => nv_depth += 1,
Ok(Event::End(_)) => {
nv_depth -= 1;
if nv_depth == 0 {
break;
}
}
Ok(Event::Empty(e2)) => {
let name2 = e2.name();
let local2 = local_name(name2.as_ref());
if local2 == b"cNvPr" {
for a in e2.attributes().flatten() {
match a.key.as_ref() {
b"id" => {
if let Ok(v) = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse::<u32>()
{
frame.id = v;
}
}
b"name" => {
frame.name = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
} else if local == b"xfrm" {
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"rot" => {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i32>()
{
frame.properties.xfrm.rot = Some(v);
}
}
b"flipH" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if v == "1" {
frame.properties.xfrm.flip_h = true;
}
}
b"flipV" => {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
if v == "1" {
frame.properties.xfrm.flip_v = true;
}
}
_ => {}
}
}
parse_xfrm_into(&mut rd, &mut frame.properties);
} else if local == b"graphic" {
parse_graphic_into(&mut rd, &mut frame)?;
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"graphicFrame" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("graphicFrame parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(frame)
}
fn parse_graphic_into<R: std::io::BufRead>(
rd: &mut Reader<R>,
frame: &mut GraphicFrame,
) -> crate::Result<()> {
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"graphicData" {
let mut uri = String::new();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"uri" {
uri = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
if uri == "http://schemas.openxmlformats.org/drawingml/2006/table" {
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e2)) => {
if local_name(e2.name().as_ref()) == b"tbl" {
let tbl_xml = collect_full_element(rd, e2.into_owned())?;
if let Ok(tbl) = parse_table(&tbl_xml) {
frame.graphic = crate::oxml::shape::Graphic::Table(tbl);
}
} else {
let _ = collect_full_element(rd, e2.into_owned());
}
}
Ok(Event::End(e2)) => {
if local_name(e2.name().as_ref()) == b"graphicData" {
break;
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
} else if uri == "http://schemas.openxmlformats.org/drawingml/2006/diagram" {
let raw_xml = collect_full_element(rd, e.into_owned())?;
let (dm_rid, lo_rid, qs_rid, cs_rid) = parse_smartart_rel_ids(&raw_xml);
frame.graphic = crate::oxml::shape::Graphic::SmartArt(
crate::oxml::shape::SmartArtRef {
raw_xml,
dm_rid,
lo_rid,
qs_rid,
cs_rid,
},
);
} else if uri == "http://schemas.openxmlformats.org/drawingml/2006/chart" {
let raw_xml = collect_full_element(rd, e.into_owned())?;
let rid = parse_chart_rid(&raw_xml);
frame.graphic =
crate::oxml::shape::Graphic::Chart(crate::oxml::chart::Chart::new(
crate::oxml::chart::ChartType::Column,
crate::oxml::chart::ChartData::default(),
));
if let crate::oxml::shape::Graphic::Chart(c) = &mut frame.graphic {
c.rid = rid;
}
} else {
let _ = collect_full_element(rd, e.into_owned());
}
} else {
let _ = collect_full_element(rd, e.into_owned());
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"graphic" {
return Ok(());
}
}
Ok(Event::Eof) => return Ok(()),
_ => {}
}
buf.clear();
}
}
fn parse_smartart_rel_ids(
raw_xml: &str,
) -> (
Option<String>,
Option<String>,
Option<String>,
Option<String>,
) {
fn find_attr(xml: &str, attr_name: &str) -> Option<String> {
let pattern = format!("{}=\"", attr_name);
let pos = xml.find(&pattern)?;
let value_start = pos + pattern.len();
let value_end = xml[value_start..].find('"')?;
Some(xml[value_start..value_start + value_end].to_string())
}
let dm = find_attr(raw_xml, "r:dm");
let lo = find_attr(raw_xml, "r:lo");
let qs = find_attr(raw_xml, "r:qs");
let cs = find_attr(raw_xml, "r:cs");
(dm, lo, qs, cs)
}
fn parse_chart_rid(raw_xml: &str) -> String {
let pattern = "r:id=\"";
if let Some(pos) = raw_xml.find(pattern) {
let value_start = pos + pattern.len();
if let Some(end) = raw_xml[value_start..].find('"') {
return raw_xml[value_start..value_start + end].to_string();
}
}
String::new()
}
fn parse_table(xml: &str) -> crate::Result<crate::oxml::table::Table> {
use crate::oxml::table::{Col, Table};
let mut tbl = Table::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut tr_bufs: Vec<String> = Vec::new();
let mut grid_col_widths: Vec<Emu> = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"tbl" {
} else if local == b"tblPr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
parse_tbl_pr_into(&inner, &mut tbl);
} else if local == b"tblGrid" {
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e2)) => {
if local_name(e2.name().as_ref()) == b"gridCol" {
let mut w: Option<i64> = None;
for a in e2.attributes().flatten() {
if a.key.as_ref() == b"w" {
w = a
.normalized_value(
quick_xml::XmlVersion::Implicit1_0,
)
.unwrap_or_default()
.parse()
.ok();
}
}
grid_col_widths.push(Emu(w.unwrap_or(0)));
}
}
Ok(Event::End(e2)) => {
if local_name(e2.name().as_ref()) == b"tblGrid" {
break;
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
} else if local == b"tr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
tr_bufs.push(inner);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("tbl parse: {e}"))),
_ => {}
}
buf.clear();
}
tbl.cols = grid_col_widths.iter().map(|w| Col { width: *w }).collect();
for tr_xml in tr_bufs {
if let Ok(row) = parse_table_row(&tr_xml) {
tbl.rows.push(row);
}
}
Ok(tbl)
}
fn parse_tbl_pr_into(xml: &str, tbl: &mut crate::oxml::table::Table) {
use crate::oxml::table::TableStyle;
let look = &mut tbl.tbl_look;
let mut rd = Reader::from_str(xml);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
if local_name(e.name().as_ref()) == b"tblLook" {
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"val" => look.val = v.to_string(),
b"firstRow" => look.first_row = v == "1",
b"lastRow" => look.last_row = v == "1",
b"firstColumn" => look.first_column = v == "1",
b"lastColumn" => look.last_column = v == "1",
b"noHBand" => look.no_h_band = v == "1",
b"noVBand" => look.no_v_band = v == "1",
_ => {}
}
}
}
if local_name(e.name().as_ref()) == b"tableStyleId" {
}
}
Ok(Event::Start(e)) => {
if local_name(e.name().as_ref()) == b"tableStyleId" {
let mut guid = String::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Text(t)) => {
guid.push_str(&t.decode().unwrap_or_default());
}
Ok(Event::End(e2)) => {
if local_name(e2.name().as_ref()) == b"tableStyleId" {
break;
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
let trimmed = guid.trim();
if !trimmed.is_empty() {
tbl.table_style = Some(TableStyle::new(trimmed));
}
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
}
fn parse_table_row(xml: &str) -> crate::Result<crate::oxml::table::Row> {
use crate::oxml::table::Row;
let mut row = Row::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut tc_bufs: Vec<String> = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"tr" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"h" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i64>()
{
row.height = Emu(v);
}
}
}
} else if local == b"tc" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
tc_bufs.push(inner);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(_e)) => {
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"tr" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("tr parse: {e}"))),
_ => {}
}
buf.clear();
}
for tc_xml in tc_bufs {
if let Ok(cell) = parse_table_cell(&tc_xml) {
row.cells.push(cell);
}
}
Ok(row)
}
fn parse_table_cell(xml: &str) -> crate::Result<crate::oxml::table::Cell> {
let mut cell = crate::oxml::table::Cell::default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"tc" {
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"gridSpan" => {
if let Ok(g) = v.parse::<u32>() {
cell.grid_span = g;
}
}
b"rowSpan" => {
if let Ok(r) = v.parse::<u32>() {
cell.row_span = r;
}
}
b"hMerge" if v == "1" => {
cell.h_merge = true;
}
b"vMerge" if v == "1" => {
cell.v_merge = true;
}
_ => {}
}
}
} else if local == b"txBody" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
cell.text = parse_txbody(&inner)?;
} else if local == b"tcPr" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
parse_tc_pr_into(&inner, &mut cell);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"tc" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("tc parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(cell)
}
fn parse_tc_pr_into(xml: &str, cell: &mut crate::oxml::table::Cell) {
use crate::oxml::table::VerticalAnchor;
let mut rd = Reader::from_str(xml);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"tcPr" {
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"marT" => {
if let Ok(m) = v.parse::<i64>() {
cell.margin.0 = Some(Emu(m));
}
}
b"marL" => {
if let Ok(m) = v.parse::<i64>() {
cell.margin.1 = Some(Emu(m));
}
}
b"marB" => {
if let Ok(m) = v.parse::<i64>() {
cell.margin.2 = Some(Emu(m));
}
}
b"marR" => {
if let Ok(m) = v.parse::<i64>() {
cell.margin.3 = Some(Emu(m));
}
}
b"anchor" => {
cell.anchor = match &*v {
"t" => VerticalAnchor::Top,
"ctr" => VerticalAnchor::Middle,
"b" => VerticalAnchor::Bottom,
_ => VerticalAnchor::Middle,
};
}
_ => {}
}
}
} else if local == b"solidFill" {
if let Ok(color) = parse_solid_fill(&mut rd, e.into_owned()) {
cell.fill = color;
}
} else if local == b"lnL" {
let border = parse_cell_border(&mut rd, e.into_owned());
cell.border_left = Some(border);
} else if local == b"lnR" {
let border = parse_cell_border(&mut rd, e.into_owned());
cell.border_right = Some(border);
} else if local == b"lnT" {
let border = parse_cell_border(&mut rd, e.into_owned());
cell.border_top = Some(border);
} else if local == b"lnB" {
let border = parse_cell_border(&mut rd, e.into_owned());
cell.border_bottom = Some(border);
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"tcPr" {
for a in e.attributes().flatten() {
let v = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match a.key.as_ref() {
b"marT" => {
if let Ok(m) = v.parse::<i64>() {
cell.margin.0 = Some(Emu(m));
}
}
b"marL" => {
if let Ok(m) = v.parse::<i64>() {
cell.margin.1 = Some(Emu(m));
}
}
b"marB" => {
if let Ok(m) = v.parse::<i64>() {
cell.margin.2 = Some(Emu(m));
}
}
b"marR" => {
if let Ok(m) = v.parse::<i64>() {
cell.margin.3 = Some(Emu(m));
}
}
b"anchor" => {
cell.anchor = match &*v {
"t" => VerticalAnchor::Top,
"ctr" => VerticalAnchor::Middle,
"b" => VerticalAnchor::Bottom,
_ => VerticalAnchor::Middle,
};
}
_ => {}
}
}
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"tcPr" {
return;
}
}
Ok(Event::Eof) => return,
_ => {}
}
buf.clear();
}
}
fn parse_cell_border<R: std::io::BufRead>(
rd: &mut Reader<R>,
start: quick_xml::events::BytesStart<'static>,
) -> crate::oxml::table::CellBorder {
use crate::oxml::table::CellBorder;
let mut border = CellBorder::default();
for a in start.attributes().flatten() {
if a.key.as_ref() == b"w" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<i64>()
{
border.width = Emu(v);
}
}
}
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"noFill" {
border.no_fill = true;
}
}
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"solidFill" {
if let Ok(color) = parse_solid_fill(rd, e.into_owned()) {
border.color = color;
}
} else {
let _ = collect_full_element(rd, e.into_owned());
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"lnL" || local == b"lnR" || local == b"lnT" || local == b"lnB" {
return border;
}
}
Ok(Event::Eof) => return border,
_ => {}
}
buf.clear();
}
}
pub fn parse_notes(xml: &str) -> crate::Result<TextBody> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut tx_buf: Option<String> = None;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"txBody" && tx_buf.is_none() {
let inner = collect_full_element(&mut rd, e.into_owned())?;
tx_buf = Some(inner);
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("notes parse: {e}"))),
_ => {}
}
buf.clear();
}
if let Some(buf) = tx_buf {
parse_txbody(&buf)
} else {
Ok(TextBody::new())
}
}
pub fn parse_comments(xml: &str) -> crate::Result<crate::oxml::comments::CommentList> {
use crate::oxml::comments::{Comment, CommentList};
let mut lst = CommentList::new();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut cur_author_id: Option<u32> = None;
let mut cur_dt: String = String::new();
let mut cur_idx: Option<u32> = None;
let mut in_cm = false;
let mut in_text = false;
let mut cur_pos_x: i64 = 0;
let mut cur_pos_y: i64 = 0;
let mut cur_text = String::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"cm" {
in_cm = true;
cur_author_id = None;
cur_dt.clear();
cur_idx = None;
cur_pos_x = 0;
cur_pos_y = 0;
cur_text.clear();
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
match key {
b"authorId" => {
cur_author_id = val.parse::<u32>().ok();
}
b"dt" => {
cur_dt = val;
}
b"idx" => {
cur_idx = val.parse::<u32>().ok();
}
_ => {}
}
}
} else if in_cm && local == b"text" {
in_text = true;
cur_text.clear();
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if in_cm && local == b"pos" {
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
match key {
b"x" => {
cur_pos_x = val.parse::<i64>().unwrap_or(0);
}
b"y" => {
cur_pos_y = val.parse::<i64>().unwrap_or(0);
}
_ => {}
}
}
}
}
Ok(Event::Text(t)) => {
if in_text {
cur_text.push_str(&t.decode().unwrap_or_default());
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"cm" && in_cm {
let c = Comment {
author_id: cur_author_id.unwrap_or(0),
date_time: std::mem::take(&mut cur_dt),
idx: cur_idx.unwrap_or(0),
pos_x: cur_pos_x,
pos_y: cur_pos_y,
text: std::mem::take(&mut cur_text),
};
lst.push(c);
in_cm = false;
in_text = false;
} else if local == b"text" && in_text {
in_text = false;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("comments parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(lst)
}
pub fn parse_comment_authors(xml: &str) -> crate::Result<crate::oxml::comments::CommentAuthorList> {
use crate::oxml::comments::{CommentAuthor, CommentAuthorList};
let mut lst = CommentAuthorList::new();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"cmAuthor" {
let mut id: u32 = 0;
let mut name_val = String::new();
let mut initials = String::new();
for a in e.attributes().flatten() {
let key = a.key.as_ref();
let val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
match key {
b"id" => {
id = val.parse::<u32>().unwrap_or(0);
}
b"name" => {
name_val = val;
}
b"initials" => {
initials = val;
}
_ => {}
}
}
lst.push(CommentAuthor::new(id, name_val, initials));
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("commentAuthors parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(lst)
}
#[allow(clippy::type_complexity)]
pub fn parse_pres_root(
xml: &str,
) -> crate::Result<(
Vec<(u32, String)>,
Option<Emu>,
Option<Emu>,
Vec<(u32, String)>,
crate::oxml::section::SectionList,
Vec<(u32, String)>,
)> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut sld_ids: Vec<(u32, String)> = Vec::new();
let mut sld_sz: (Option<Emu>, Option<Emu>) = (None, None);
let mut sld_master_ids: Vec<(u32, String)> = Vec::new();
let mut notes_master_ids: Vec<(u32, String)> = Vec::new();
let mut sections = crate::oxml::section::SectionList::default();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"sldId" {
let mut id: Option<u32> = None;
let mut rid: Option<String> = None;
for a in e.attributes().flatten() {
let k = a.key.as_ref();
if k == b"id" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u32>()
{
id = Some(v);
}
} else if k == b"r:id" || k.ends_with(b":id") {
rid = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
if let (Some(i), Some(r)) = (id, rid) {
sld_ids.push((i, r));
}
} else if local == b"sldMasterId" {
let mut id: Option<u32> = None;
let mut rid: Option<String> = None;
for a in e.attributes().flatten() {
let k = a.key.as_ref();
if k == b"id" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u32>()
{
id = Some(v);
}
} else if k == b"r:id" || k.ends_with(b":id") {
rid = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
if let (Some(i), Some(r)) = (id, rid) {
sld_master_ids.push((i, r));
}
} else if local == b"notesMasterId" {
let mut id: Option<u32> = None;
let mut rid: Option<String> = None;
for a in e.attributes().flatten() {
let k = a.key.as_ref();
if k == b"id" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u32>()
{
id = Some(v);
}
} else if k == b"r:id" || k.ends_with(b":id") {
rid = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
if let (Some(i), Some(r)) = (id, rid) {
notes_master_ids.push((i, r));
}
} else if local == b"sldSz" {
let mut cx: Option<i64> = None;
let mut cy: Option<i64> = None;
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"cx" => {
cx = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
b"cy" => {
cy = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse()
.ok()
}
_ => {}
}
}
sld_sz = (cx.map(Emu), cy.map(Emu));
}
}
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"sldMasterId" {
let mut id: Option<u32> = None;
let mut rid: Option<String> = None;
for a in e.attributes().flatten() {
let k = a.key.as_ref();
if k == b"id" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u32>()
{
id = Some(v);
}
} else if k == b"r:id" || k.ends_with(b":id") {
rid = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
if let (Some(i), Some(r)) = (id, rid) {
sld_master_ids.push((i, r));
}
let _ = collect_full_element(&mut rd, e.into_owned());
} else if local == b"notesMasterId" {
let mut id: Option<u32> = None;
let mut rid: Option<String> = None;
for a in e.attributes().flatten() {
let k = a.key.as_ref();
if k == b"id" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u32>()
{
id = Some(v);
}
} else if k == b"r:id" || k.ends_with(b":id") {
rid = Some(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
if let (Some(i), Some(r)) = (id, rid) {
notes_master_ids.push((i, r));
}
let _ = collect_full_element(&mut rd, e.into_owned());
} else if local == b"extLst" {
let ext_lst_xml = collect_full_element(&mut rd, e.into_owned())?;
let parsed = parse_sections_from_ext_lst(&ext_lst_xml)?;
if !parsed.is_empty() {
sections = parsed;
}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("pres root parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok((
sld_ids,
sld_sz.0,
sld_sz.1,
sld_master_ids,
sections,
notes_master_ids,
))
}
fn parse_sections_from_ext_lst(xml: &str) -> crate::Result<crate::oxml::section::SectionList> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut result = crate::oxml::section::SectionList::default();
let mut state: u8 = 0;
let mut cur_name = String::new();
let mut cur_ids: Vec<u32> = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match state {
0 if local == b"ext" => {
let mut uri_val = String::new();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"uri" {
uri_val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
if uri_val == crate::oxml::section::SECTION_EXT_URI {
state = 1;
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
1 if local == b"sectionLst" => {
state = 2;
}
2 if local == b"section" => {
cur_name.clear();
cur_ids.clear();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"name" {
cur_name = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
state = 3;
}
3 if local == b"sldIdLst" => {
}
_ => {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if state == 3 && local == b"sldId" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"id" {
if let Ok(v) = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.parse::<u32>()
{
cur_ids.push(v);
}
}
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match state {
3 if local == b"section" => {
result.push(crate::oxml::section::Section {
name: std::mem::take(&mut cur_name),
slide_ids: std::mem::take(&mut cur_ids),
});
state = 2;
}
2 if local == b"sectionLst" => {
state = 1;
}
1 if local == b"ext" => {
state = 0;
}
0 if local == b"extLst" => {
break;
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("sectionLst parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(result)
}
pub fn parse_sld_master(xml: &str) -> crate::Result<crate::oxml::slidemaster::SldMaster> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut master = crate::oxml::slidemaster::SldMaster::default();
let mut state: u8 = 0;
let mut sp_bufs: Vec<String> = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match state {
0 if local == b"sldMaster" => {
state = 1;
}
1 if local == b"cSld" => {}
1 if local == b"spTree" => {
state = 2;
}
1 if local == b"sldLayoutIdLst" => {
state = 3;
}
2 if local == b"sp" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
sp_bufs.push(inner);
}
_ => {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if state == 3 && local == b"sldLayoutId" {
for a in e.attributes().flatten() {
let k = a.key.as_ref();
if k == b"r:id" || k.ends_with(b":id") {
let rid = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
master.layout_rids.push(rid);
}
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if (state == 2 && local == b"spTree") || (state == 3 && local == b"sldLayoutIdLst")
{
state = 1;
} else if state == 1 && local == b"sldMaster" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("sldMaster parse: {e}"))),
_ => {}
}
buf.clear();
}
for s in sp_bufs {
if let Ok(sp) = parse_sp(&s) {
master.shapes.push(sp);
}
}
Ok(master)
}
pub fn parse_notes_master(xml: &str) -> crate::Result<crate::oxml::notesmaster::NotesMaster> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut master = crate::oxml::notesmaster::NotesMaster::default();
let mut state: u8 = 0;
let mut sp_bufs: Vec<String> = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match state {
0 if local == b"notesMaster" => {
state = 1;
}
1 if local == b"cSld" => {}
1 if local == b"spTree" => {
state = 2;
}
2 if local == b"sp" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
sp_bufs.push(inner);
}
_ => {
let is_clr_map = local == b"clrMap";
let is_notes_style = local == b"notesStyle";
let full_xml = collect_full_element(&mut rd, e.into_owned())?;
if is_clr_map {
master.clr_map_xml = Some(full_xml);
} else if is_notes_style {
master.notes_style_xml = Some(full_xml);
}
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if state == 2 && local == b"spTree" {
state = 1;
} else if state == 1 && local == b"notesMaster" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("notesMaster parse: {e}"))),
_ => {}
}
buf.clear();
}
for s in sp_bufs {
if let Ok(sp) = parse_sp(&s) {
master.shapes.push(sp);
}
}
Ok(master)
}
pub fn parse_sld_layout(xml: &str) -> crate::Result<crate::oxml::slidelayout::SldLayout> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut layout = crate::oxml::slidelayout::SldLayout::default();
let mut state: u8 = 0;
let mut sp_bufs: Vec<String> = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match state {
0 if local == b"sldLayout" => {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"type" {
layout.type_ = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
state = 1;
}
1 if local == b"cSld" => {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"name" {
layout.name = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
}
1 if local == b"spTree" => {
state = 2;
}
2 if local == b"sp" => {
let inner = collect_full_element(&mut rd, e.into_owned())?;
sp_bufs.push(inner);
}
_ => {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if state == 2 && local == b"spTree" {
state = 1;
} else if state == 1 && local == b"sldLayout" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("sldLayout parse: {e}"))),
_ => {}
}
buf.clear();
}
for s in sp_bufs {
if let Ok(sp) = parse_sp(&s) {
layout.shapes.push(sp);
}
}
Ok(layout)
}
pub fn parse_theme(xml: &str) -> crate::Result<crate::oxml::theme::Theme> {
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut theme = crate::oxml::theme::Theme::default();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"theme" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"name" {
theme.name = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
} else if local == b"clrScheme" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
theme.color_scheme = parse_clr_scheme(&inner);
} else if local == b"fontScheme" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
theme.font_scheme = parse_font_scheme(&inner);
} else if local == b"fmtScheme" {
let inner = collect_full_element(&mut rd, e.into_owned())?;
let (fmt_name, raw) = parse_fmt_scheme(&inner);
theme.format_scheme.name = fmt_name;
theme.format_scheme.raw_xml = raw;
theme.format_scheme.parse_from_raw_xml();
} else if local == b"themeElements" {
} else {
let _ = collect_full_element(&mut rd, e.into_owned());
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("theme parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(theme)
}
fn parse_fmt_scheme(xml: &str) -> (String, String) {
let mut fmt_name = String::new();
let mut raw_xml = String::new();
let mut rd = Reader::from_str(xml);
let mut buf = Vec::new();
let mut in_fmt_scheme = false;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let ev_name = e.name();
let local = local_name(ev_name.as_ref());
if !in_fmt_scheme && local == b"fmtScheme" {
in_fmt_scheme = true;
for a in e.attributes().flatten() {
if a.key.as_ref() == b"name" {
fmt_name = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
} else if in_fmt_scheme {
let inner = collect_full_element(&mut rd, e.into_owned()).unwrap_or_default();
raw_xml.push_str(&inner);
}
}
Ok(Event::Empty(e)) => {
let ev_name = e.name();
let local = local_name(ev_name.as_ref());
if in_fmt_scheme {
let mut s = String::new();
s.push('<');
s.push_str(std::str::from_utf8(local).unwrap_or(""));
for a in e.attributes().flatten() {
s.push(' ');
s.push_str(std::str::from_utf8(a.key.as_ref()).unwrap_or(""));
s.push_str("=\"");
s.push_str(
&a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default(),
);
s.push('"');
}
s.push_str("/>");
raw_xml.push_str(&s);
}
}
Ok(Event::End(e)) => {
let ev_name = e.name();
let local = local_name(ev_name.as_ref());
if in_fmt_scheme && local == b"fmtScheme" {
break;
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
(fmt_name, raw_xml)
}
fn parse_clr_scheme(xml: &str) -> crate::oxml::theme::ColorScheme {
use crate::oxml::theme::{ColorScheme, ThemeColor};
let mut scheme = ColorScheme::default();
let mut rd = Reader::from_str(xml);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
let slot: &mut Option<ThemeColor> = match local {
b"dk1" => &mut scheme.dk1,
b"lt1" => &mut scheme.lt1,
b"dk2" => &mut scheme.dk2,
b"lt2" => &mut scheme.lt2,
b"accent1" => &mut scheme.accent1,
b"accent2" => &mut scheme.accent2,
b"accent3" => &mut scheme.accent3,
b"accent4" => &mut scheme.accent4,
b"accent5" => &mut scheme.accent5,
b"accent6" => &mut scheme.accent6,
b"hlink" => &mut scheme.hlink,
b"folHlink" => &mut scheme.fol_hlink,
b"clrScheme" => {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"name" {
scheme.name = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
continue;
}
_ => {
let _ = collect_full_element(&mut rd, e.into_owned());
continue;
}
};
*slot = Some(read_color_child(&mut rd, local));
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"clrScheme" {
break;
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
scheme
}
fn read_color_child<R: std::io::BufRead>(
rd: &mut Reader<R>,
parent_local: &[u8],
) -> crate::oxml::theme::ThemeColor {
use crate::oxml::theme::ThemeColor;
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"srgbClr" {
let mut val = String::new();
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
let _ = collect_full_element(rd, e.into_owned());
return ThemeColor::Srgb(val);
} else if local == b"sysClr" {
let mut val = String::new();
let mut last_clr = String::new();
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"val" => {
val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
b"lastClr" => {
last_clr = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
let _ = collect_full_element(rd, e.into_owned());
return ThemeColor::Sys(val, last_clr);
} else {
let _ = collect_full_element(rd, e.into_owned());
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"srgbClr" {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"val" {
return ThemeColor::Srgb(
a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string(),
);
}
}
} else if local == b"sysClr" {
let mut val = String::new();
let mut last_clr = String::new();
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"val" => {
val = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
b"lastClr" => {
last_clr = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
return ThemeColor::Sys(val, last_clr);
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == parent_local {
return ThemeColor::None;
}
}
Ok(Event::Eof) => return ThemeColor::None,
_ => {}
}
buf.clear();
}
}
fn parse_font_scheme(xml: &str) -> crate::oxml::theme::FontScheme {
let mut scheme = crate::oxml::theme::FontScheme::default();
let mut rd = Reader::from_str(xml);
let mut buf = Vec::new();
let mut state: u8 = 0;
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"fontScheme" {
state = 1;
for a in e.attributes().flatten() {
if a.key.as_ref() == b"name" {
scheme.name = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
} else if state == 1 && local == b"majorFont" {
let (latin, ea, cs) = read_font_typefaces(&mut rd);
scheme.major_latin = latin;
scheme.major_ea = ea;
scheme.major_cs = cs;
} else if state == 1 && local == b"minorFont" {
let (latin, ea, cs) = read_font_typefaces(&mut rd);
scheme.minor_latin = latin;
scheme.minor_ea = ea;
scheme.minor_cs = cs;
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == b"fontScheme" {
break;
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
scheme
}
fn read_font_typefaces<R: std::io::BufRead>(rd: &mut Reader<R>) -> (String, String, String) {
let mut buf = Vec::new();
let mut latin = String::new();
let mut ea = String::new();
let mut cs = String::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
let typeface = extract_typeface(&e);
match local {
b"latin" => latin = typeface,
b"ea" => ea = typeface,
b"cs" => cs = typeface,
_ => {}
}
}
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
let typeface = extract_typeface(&e);
match local {
b"latin" => latin = typeface,
b"ea" => ea = typeface,
b"cs" => cs = typeface,
_ => {}
}
let _ = collect_full_element(rd, e.into_owned());
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"majorFont" || local == b"minorFont" {
return (latin, ea, cs);
}
}
Ok(Event::Eof) => return (latin, ea, cs),
_ => {}
}
buf.clear();
}
}
fn extract_typeface(e: &quick_xml::events::BytesStart<'_>) -> String {
for a in e.attributes().flatten() {
if a.key.as_ref() == b"typeface" {
return a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
}
}
String::new()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oxml::txbody::TextBody;
#[test]
fn parse_backdrop_full() {
let xml = r#"<?xml version="1.0"?>
<a:backdrop xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:anchor x="100000" y="200000" z="300000"/>
<a:floor/>
<a:wall/>
<a:l/>
<a:r/>
<a:t/>
<a:b/>
</a:backdrop>"#;
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let start = loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"backdrop" => {
break e.into_owned();
}
Ok(Event::Eof) => panic!("backdrop not found"),
_ => {}
}
buf.clear();
};
let bd = parse_backdrop(&mut rd, start).expect("parse backdrop");
assert_eq!(
bd.anchor,
Some(Point3d {
x: 100000,
y: 200000,
z: 300000
})
);
assert!(bd.floor);
assert!(bd.wall);
assert!(bd.left);
assert!(bd.right);
assert!(bd.top);
assert!(bd.bottom);
}
#[test]
fn parse_backdrop_partial() {
let xml = r#"<?xml version="1.0"?>
<a:backdrop xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:floor/>
<a:wall/>
</a:backdrop>"#;
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let start = loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"backdrop" => {
break e.into_owned();
}
Ok(Event::Eof) => panic!("backdrop not found"),
_ => {}
}
buf.clear();
};
let bd = parse_backdrop(&mut rd, start).expect("parse backdrop");
assert!(bd.anchor.is_none());
assert!(bd.floor);
assert!(bd.wall);
assert!(!bd.left);
assert!(!bd.right);
assert!(!bd.top);
assert!(!bd.bottom);
}
#[test]
fn parse_scene_3d_with_backdrop() {
let xml = r#"<?xml version="1.0"?>
<a:scene3d xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:camera prst="orthographicFront"/>
<a:lightRig rig="balanced" dir="t"/>
<a:backdrop>
<a:anchor x="0" y="0" z="0"/>
<a:floor/>
<a:wall/>
</a:backdrop>
</a:scene3d>"#;
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let start = loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"scene3d" => {
break e.into_owned();
}
Ok(Event::Eof) => panic!("scene3d not found"),
_ => {}
}
buf.clear();
};
let scene = parse_scene_3d(&mut rd, start).expect("parse scene3d");
let bd = scene.backdrop.expect("backdrop should be parsed");
assert!(bd.floor);
assert!(bd.wall);
assert!(!bd.left);
assert_eq!(bd.anchor, Some(Point3d { x: 0, y: 0, z: 0 }));
}
#[test]
fn parse_scene_3d_without_backdrop() {
let xml = r#"<?xml version="1.0"?>
<a:scene3d xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:camera prst="orthographicFront"/>
<a:lightRig rig="balanced" dir="t"/>
</a:scene3d>"#;
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
let start = loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) if local_name(e.name().as_ref()) == b"scene3d" => {
break e.into_owned();
}
Ok(Event::Eof) => panic!("scene3d not found"),
_ => {}
}
buf.clear();
};
let scene = parse_scene_3d(&mut rd, start).expect("parse scene3d");
assert!(scene.backdrop.is_none());
}
#[test]
fn parse_minimal_sld() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="TextBox 1"/>
<p:cNvSpPr txBox="1"/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr>
<a:xfrm>
<a:off x="914400" y="914400"/>
<a:ext cx="3657600" cy="457200"/>
</a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:noFill/>
</p:spPr>
<p:txBody>
<a:bodyPr wrap="square" rtlCol="0"><a:noAutofit/></a:bodyPr>
<a:p>
<a:r><a:rPr lang="en-US"/><a:t>Hello</a:t></a:r>
</a:p>
</p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
<p:clrMapOvr/>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
assert_eq!(sld.shapes.len(), 1);
if let OxmlSlideShape::Sp(sp) = &sld.shapes[0] {
assert_eq!(sp.id, 2);
assert_eq!(sp.name, "TextBox 1");
assert!(sp.c_nv_sp_pr_tx_box);
assert_eq!(sp.text.paragraphs.len(), 1);
assert_eq!(sp.text.paragraphs[0].runs.len(), 1);
assert_eq!(sp.text.paragraphs[0].runs[0].text, "Hello");
} else {
panic!("expected Sp");
}
}
#[test]
fn parse_empty_txbody_keeps_one_paragraph() {
let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:bodyPr/>
</p:txBody>"#;
let tb = parse_txbody(xml).unwrap();
assert!(tb.paragraphs.is_empty());
let tb2 = TextBody::new();
assert!(tb2.paragraphs.is_empty());
}
#[test]
fn parse_pres_root_extracts_sld_id_lst_and_size() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
<p:sldIdLst>
<p:sldId id="256" r:id="rId2"/>
<p:sldId id="257" r:id="rId3"/>
</p:sldIdLst>
<p:sldSz cx="9144000" cy="6858000"/>
</p:presentation>"#;
let (ids, w, h, master_ids, _sections, _notes_master_ids) =
parse_pres_root(xml).expect("parse ok");
assert_eq!(ids.len(), 2);
assert_eq!(ids[0], (256, "rId2".to_string()));
assert_eq!(ids[1], (257, "rId3".to_string()));
assert_eq!(w.map(|v| v.0), Some(9_144_000));
assert_eq!(h.map(|v| v.0), Some(6_858_000));
assert_eq!(master_ids.len(), 1);
assert_eq!(master_ids[0], (2147483648, "rId1".to_string()));
}
#[test]
fn parse_notes_returns_text_body() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr/>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Notes Placeholder"/>
<p:cNvSpPr txBox="1"/>
<p:nvPr><p:ph type="body" idx="1"/></p:nvPr>
</p:nvSpPr>
<p:spPr/>
<p:txBody>
<a:bodyPr/>
<a:p><a:r><a:t>hello notes</a:t></a:r></a:p>
</p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
</p:notes>"#;
let tb = parse_notes(xml).expect("parse notes ok");
assert_eq!(tb.paragraphs.len(), 1);
assert_eq!(tb.paragraphs[0].runs.len(), 1);
assert_eq!(tb.paragraphs[0].runs[0].text, "hello notes");
}
#[test]
fn parse_pic_extracts_rid_and_src_rect() {
let xml1 = r#"<p:pic xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:nvPicPr><p:cNvPr id="42" name="MyPic"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
<p:blipFill>
<a:blip r:embed="rIdImg5"/>
<a:srcRect l="1000" t="2000" r="3000" b="4000"/>
</p:blipFill>
<p:spPr/>
</p:pic>"#;
let p1 = parse_pic(xml1).expect("parse pic 1");
assert_eq!(p1.id, 42);
assert_eq!(p1.name, "MyPic");
assert_eq!(p1.rid, "rIdImg5");
assert_eq!(p1.src_rect, Some((1000, 2000, 3000, 4000)));
let xml2 = r#"<p:pic xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:nvPicPr><p:cNvPr id="7" name="p2"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
<p:blipFill><a:blip r:embed="rIdImg99"></a:blip></p:blipFill>
<p:spPr/>
</p:pic>"#;
let p2 = parse_pic(xml2).expect("parse pic 2");
assert_eq!(p2.id, 7);
assert_eq!(p2.rid, "rIdImg99");
}
#[test]
fn parse_run_with_typeface_works() {
let xml = r#"<a:r xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:rPr>
<a:latin typeface="Calibri"/>
<a:ea typeface="宋体"/>
<a:cs typeface="Times New Roman"/>
</a:rPr>
<a:t>hello</a:t>
</a:r>"#;
let r = parse_run(xml).expect("parse run");
assert_eq!(r.text, "hello");
assert_eq!(r.properties.latin_font.as_deref(), Some("Calibri"));
assert_eq!(r.properties.eastasia_font.as_deref(), Some("宋体"));
assert_eq!(r.properties.cs_font.as_deref(), Some("Times New Roman"));
}
#[test]
fn write_sp_placeholder_defaults_to_body() {
use crate::oxml::shape::Sp;
use crate::oxml::writer::XmlWriter;
let sp = Sp {
id: 3,
name: "Ph".into(),
is_placeholder: true,
ph_idx: Some(0),
ph_type: None, ..Default::default()
};
let mut w = XmlWriter::new();
sp.write_xml(&mut w);
let s = w.into_string();
assert!(
s.contains(r#"<p:ph type="body" idx="0"/>"#),
"应写出 type=\"body\",实际:{s}"
);
}
#[test]
fn connector_set_begin_end_recomputes_xfrm() {
use crate::oxml::simpletypes::MsoConnectorType;
use crate::shape::connector::Connector;
use crate::units::EmuPoint;
let mut c = Connector::new_with_type("c1", MsoConnectorType::Straight);
c.set_begin(EmuPoint::new(1000, 2000));
c.set_end(EmuPoint::new(5000, 6000));
assert_eq!(c.properties().xfrm.off_x.unwrap().value(), 1000);
assert_eq!(c.properties().xfrm.off_y.unwrap().value(), 2000);
assert_eq!(c.properties().xfrm.ext_cx.unwrap().value(), 4000);
assert_eq!(c.properties().xfrm.ext_cy.unwrap().value(), 4000);
}
#[test]
fn parse_minimal_sld_master() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Title Placeholder 1"/>
<p:cNvSpPr txBox="1"/>
<p:nvPr><p:ph type="title" idx="0"/></p:nvPr>
</p:nvSpPr>
<p:spPr><a:xfrm><a:off x="457200" y="274638"/><a:ext cx="8229600" cy="1143000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>
</p:sldMaster>"#;
let master = parse_sld_master(xml).unwrap();
assert_eq!(master.shapes.len(), 1, "应解析出 1 个 shape");
assert_eq!(master.shapes[0].id, 2);
assert_eq!(master.shapes[0].name, "Title Placeholder 1");
assert!(master.shapes[0].is_placeholder);
assert_eq!(master.shapes[0].ph_type.as_deref(), Some("title"));
assert_eq!(master.layout_rids.len(), 1, "应解析出 1 个 layout rid");
assert_eq!(master.layout_rids[0], "rId1");
}
#[test]
fn parse_minimal_sld_layout() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
type="title" preserve="1">
<p:cSld name="Title Slide">
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Title 1"/>
<p:cNvSpPr txBox="1"/>
<p:nvPr><p:ph type="title" idx="0"/></p:nvPr>
</p:nvSpPr>
<p:spPr><a:xfrm><a:off x="685800" y="2130425"/><a:ext cx="7772400" cy="1470025"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
<p:clrMapOvr bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2"/>
</p:sldLayout>"#;
let layout = parse_sld_layout(xml).unwrap();
assert_eq!(layout.type_, "title");
assert_eq!(layout.name, "Title Slide");
assert_eq!(layout.shapes.len(), 1, "应解析出 1 个 shape");
assert_eq!(layout.shapes[0].name, "Title 1");
assert!(layout.shapes[0].is_placeholder);
}
#[test]
fn parse_theme_extracts_name_and_colors() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<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:majorFont>
<a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
</a:fontScheme>
<a:fmtScheme name="Office">
<a:fillStyleLst>
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
</a:fillStyleLst>
<a:lnStyleLst/>
<a:effectStyleLst/>
<a:bgFillStyleLst/>
</a:fmtScheme>
</a:themeElements>
</a:theme>"#;
let theme = parse_theme(xml).unwrap();
assert_eq!(theme.name, "Office Theme");
assert_eq!(theme.color_scheme.name, "Office");
assert_eq!(
format!("{:?}", theme.color_scheme.dk1),
r#"Some(Sys("windowText", "000000"))"#,
"dk1 mismatch"
);
assert_eq!(
format!("{:?}", theme.color_scheme.lt1),
r#"Some(Sys("window", "FFFFFF"))"#,
"lt1 mismatch"
);
assert_eq!(
format!("{:?}", theme.color_scheme.accent1),
r#"Some(Srgb("4F81BD"))"#,
"accent1 mismatch"
);
assert_eq!(
format!("{:?}", theme.color_scheme.hlink),
r#"Some(Srgb("0000FF"))"#,
"hlink mismatch"
);
assert_eq!(theme.font_scheme.name, "Office");
assert_eq!(theme.font_scheme.major_latin, "Calibri");
assert_eq!(theme.font_scheme.minor_latin, "Calibri");
assert_eq!(theme.format_scheme.name, "Office");
assert!(
!theme.format_scheme.raw_xml.is_empty(),
"fmtScheme raw_xml should not be empty"
);
assert!(
theme.format_scheme.raw_xml.contains("<a:fillStyleLst>"),
"fmtScheme raw_xml should contain fillStyleLst"
);
}
#[test]
fn parse_theme_font_scheme_ea_cs() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Test">
<a:themeElements>
<a:clrScheme name="Test"/>
<a:fontScheme name="Test">
<a:majorFont>
<a:latin typeface="Calibri Light"/>
<a:ea typeface="宋体"/>
<a:cs typeface="Times New Roman"/>
</a:majorFont>
<a:minorFont>
<a:latin typeface="Calibri"/>
<a:ea typeface="黑体"/>
<a:cs typeface="Arial"/>
</a:minorFont>
</a:fontScheme>
<a:fmtScheme name="Test"/>
</a:themeElements>
</a:theme>"#;
let theme = parse_theme(xml).unwrap();
assert_eq!(theme.font_scheme.major_latin, "Calibri Light");
assert_eq!(theme.font_scheme.major_ea, "宋体");
assert_eq!(theme.font_scheme.major_cs, "Times New Roman");
assert_eq!(theme.font_scheme.minor_latin, "Calibri");
assert_eq!(theme.font_scheme.minor_ea, "黑体");
assert_eq!(theme.font_scheme.minor_cs, "Arial");
}
#[test]
fn theme_round_trip() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="RoundTrip">
<a:themeElements>
<a:clrScheme name="RT">
<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="RT">
<a:majorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
<a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
</a:fontScheme>
<a:fmtScheme name="Office"/>
</a:themeElements>
</a:theme>"#;
let theme1 = parse_theme(xml).unwrap();
assert_eq!(theme1.name, "RoundTrip");
assert_eq!(theme1.color_scheme.name, "RT");
assert_eq!(theme1.font_scheme.name, "RT");
let serialized = theme1.to_xml();
let theme2 = parse_theme(&serialized).unwrap();
assert_eq!(theme2.name, theme1.name);
assert_eq!(theme2.color_scheme.name, theme1.color_scheme.name);
assert_eq!(theme2.color_scheme.dk1, theme1.color_scheme.dk1);
assert_eq!(theme2.color_scheme.accent1, theme1.color_scheme.accent1);
assert_eq!(theme2.font_scheme.name, theme1.font_scheme.name);
assert_eq!(
theme2.font_scheme.major_latin,
theme1.font_scheme.major_latin
);
assert_eq!(
theme2.font_scheme.minor_latin,
theme1.font_scheme.minor_latin
);
}
#[test]
fn parse_cxn_sp_extracts_geometry_and_connections() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:cxnSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:nvCxnSpPr>
<p:cNvPr id="10" name="Connector1"/>
<p:cNvCxnSpPr/>
<p:nvPr>
<p:stCxn id="2" idx="3"/>
<p:endCxn id="5" idx="1"/>
</p:nvPr>
</p:nvCxnSpPr>
<p:spPr>
<a:xfrm>
<a:off x="1000" y="2000"/>
<a:ext cx="5000" cy="4000"/>
</a:xfrm>
<a:prstGeom prst="line"><a:avLst/></a:prstGeom>
</p:spPr>
</p:cxnSp>"#;
let cxn = parse_cxn_sp(xml).expect("parse cxnSp ok");
assert_eq!(cxn.id, 10);
assert_eq!(cxn.name, "Connector1");
assert_eq!(cxn.st_cxn, Some((2, 3)), "stCxn 应解析出 (id=2, idx=3)");
assert_eq!(cxn.end_cxn, Some((5, 1)), "endCxn 应解析出 (id=5, idx=1)");
assert_eq!(cxn.properties.xfrm.off_x.unwrap().value(), 1000);
assert_eq!(cxn.properties.xfrm.off_y.unwrap().value(), 2000);
assert_eq!(cxn.properties.xfrm.ext_cx.unwrap().value(), 5000);
assert_eq!(cxn.properties.xfrm.ext_cy.unwrap().value(), 4000);
}
#[test]
fn parse_cxn_sp_without_connections() {
let xml = r#"<p:cxnSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:nvCxnSpPr><p:cNvPr id="20" name="FreeLine"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
<p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="100" cy="200"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
</p:cxnSp>"#;
let cxn = parse_cxn_sp(xml).unwrap();
assert_eq!(cxn.id, 20);
assert_eq!(cxn.name, "FreeLine");
assert!(cxn.st_cxn.is_none(), "无 stCxn 时应为 None");
assert!(cxn.end_cxn.is_none(), "无 endCxn 时应为 None");
}
#[test]
fn parse_cxn_sp_with_style() {
let xml = r#"<p:cxnSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:nvCxnSpPr><p:cNvPr id="30" name="StyledConn"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
<p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="100" cy="200"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
<p:style>
<a:lnRef idx="2"><a:schemeClr val="accent1"/></a:lnRef>
<a:fillRef idx="0"><a:schemeClr val="accent2"/></a:fillRef>
</p:style>
</p:cxnSp>"#;
let cxn = parse_cxn_sp(xml).expect("parse cxnSp ok");
let style = cxn.style.as_ref().expect("style 应存在");
let ln = style.line_ref.as_ref().expect("line_ref 应存在");
assert_eq!(ln.idx.as_deref(), Some("2"));
assert_eq!(ln.scheme_color.as_deref(), Some("accent1"));
let fill = style.fill_ref.as_ref().expect("fill_ref 应存在");
assert_eq!(fill.idx.as_deref(), Some("0"));
assert_eq!(fill.scheme_color.as_deref(), Some("accent2"));
}
#[test]
fn parse_grp_sp_with_style() {
let xml = r#"<p:grpSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:nvGrpSpPr><p:cNvPr id="40" name="StyledGroup"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="1000" cy="1000"/><a:chOff x="0" y="0"/><a:chExt cx="1000" cy="1000"/></a:xfrm></p:grpSpPr>
<p:style>
<a:lnRef idx="1"><a:schemeClr val="tx1"/></a:lnRef>
<a:fontRef idx="minor"><a:schemeClr val="dk1"/></a:fontRef>
</p:style>
</p:grpSp>"#;
let grp = parse_grp_sp(xml).expect("parse grpSp ok");
assert_eq!(grp.id, 40);
let style = grp.style.as_ref().expect("style 应存在");
let ln = style.line_ref.as_ref().expect("line_ref 应存在");
assert_eq!(ln.idx.as_deref(), Some("1"));
assert_eq!(ln.scheme_color.as_deref(), Some("tx1"));
let font = style.font_ref.as_ref().expect("font_ref 应存在");
assert_eq!(font.idx.as_deref(), Some("minor"));
assert_eq!(font.scheme_color.as_deref(), Some("dk1"));
}
#[test]
fn parse_grp_sp_extracts_children() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:grpSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:nvGrpSpPr>
<p:cNvPr id="100" name="Group1"/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="8000" cy="6000"/>
<a:chOff x="0" y="0"/>
<a:chExt cx="8000" cy="6000"/>
</a:xfrm>
</p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="101" name="Child1"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
<p:spPr><a:xfrm><a:off x="100" y="100"/><a:ext cx="2000" cy="1000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
<p:txBody><a:bodyPr/><a:p><a:r><a:t>child text</a:t></a:r></a:p></p:txBody>
</p:sp>
<p:cxnSp>
<p:nvCxnSpPr><p:cNvPr id="102" name="Link"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
<p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="3000" cy="3000"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
</p:cxnSp>
</p:grpSp>"#;
let grp = parse_grp_sp(xml).expect("parse grpSp ok");
assert_eq!(grp.id, 100);
assert_eq!(grp.name, "Group1");
assert_eq!(grp.children.len(), 2, "应解析出 2 个子形状");
match &grp.children[0] {
GroupChild::Sp(sp) => {
assert_eq!(sp.id, 101);
assert_eq!(sp.name, "Child1");
assert_eq!(sp.text.paragraphs[0].runs[0].text, "child text");
}
other => panic!("第一个子形状应为 Sp,实际:{other:?}"),
}
match &grp.children[1] {
GroupChild::CxnSp(cxn) => {
assert_eq!(cxn.id, 102);
assert_eq!(cxn.name, "Link");
}
other => panic!("第二个子形状应为 CxnSp,实际:{other:?}"),
}
}
#[test]
fn parse_graphic_frame_with_table() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:graphicFrame xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:nvGraphicFramePr>
<p:cNvPr id="200" name="TableFrame"/>
<p:cNvGraphicFramePr/>
<p:nvPr/>
</p:nvGraphicFramePr>
<p:xfrm>
<a:off x="457200" y="1828800"/>
<a:ext cx="8229600" cy="1143000"/>
</p:xfrm>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
<a:tbl>
<a:tblPr firstRow="1" bandRow="1"/>
<a:tblGrid>
<a:gridCol w="4114800"/>
<a:gridCol w="4114800"/>
</a:tblGrid>
<a:tr h="370840" h="0">
<a:tc>
<a:txBody><a:bodyPr/><a:p><a:r><a:t>A1</a:t></a:r></a:p></a:txBody>
<a:tcPr/>
</a:tc>
<a:tc>
<a:txBody><a:bodyPr/><a:p><a:r><a:t>B1</a:t></a:r></a:p></a:txBody>
<a:tcPr/>
</a:tc>
</a:tr>
<a:tr h="370840" h="0">
<a:tc>
<a:txBody><a:bodyPr/><a:p><a:r><a:t>A2</a:t></a:r></a:p></a:txBody>
<a:tcPr/>
</a:tc>
<a:tc>
<a:txBody><a:bodyPr/><a:p><a:r><a:t>B2</a:t></a:r></a:p></a:txBody>
<a:tcPr/>
</a:tc>
</a:tr>
</a:tbl>
</a:graphicData>
</a:graphic>
</p:graphicFrame>"#;
let frame = parse_graphic_frame(xml).expect("parse graphicFrame ok");
assert_eq!(frame.id, 200);
assert_eq!(frame.name, "TableFrame");
assert_eq!(frame.properties.xfrm.off_x.unwrap().value(), 457200);
assert_eq!(frame.properties.xfrm.off_y.unwrap().value(), 1828800);
assert_eq!(frame.properties.xfrm.ext_cx.unwrap().value(), 8229600);
assert_eq!(frame.properties.xfrm.ext_cy.unwrap().value(), 1143000);
match &frame.graphic {
crate::oxml::shape::Graphic::Table(tbl) => {
assert_eq!(tbl.cols.len(), 2, "应有 2 列");
assert_eq!(tbl.rows.len(), 2, "应有 2 行");
assert_eq!(tbl.cols[0].width.value(), 4114800);
let cell_a1 = &tbl.rows[0].cells[0];
let text_a1: String = cell_a1
.text
.paragraphs
.iter()
.flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
.collect();
assert_eq!(text_a1, "A1");
let cell_b2 = &tbl.rows[1].cells[1];
let text_b2: String = cell_b2
.text
.paragraphs
.iter()
.flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
.collect();
assert_eq!(text_b2, "B2");
}
_ => panic!("期望 Table 变体"),
}
}
#[test]
fn parse_sld_with_mixed_shape_types() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="2" name="TextBox"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
<p:spPr><a:xfrm><a:off x="100" y="100"/><a:ext cx="2000" cy="1000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
<p:txBody><a:bodyPr/><a:p><a:r><a:t>text</a:t></a:r></a:p></p:txBody>
</p:sp>
<p:pic>
<p:nvPicPr><p:cNvPr id="3" name="Pic1"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
<p:blipFill><a:blip r:embed="rIdImg1"/></p:blipFill>
<p:spPr><a:xfrm><a:off x="5000" y="5000"/><a:ext cx="3000" cy="2000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
</p:pic>
<p:cxnSp>
<p:nvCxnSpPr><p:cNvPr id="4" name="Conn1"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
<p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="8000" cy="6000"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
</p:cxnSp>
<p:grpSp>
<p:nvGrpSpPr><p:cNvPr id="5" name="Group1"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="1000" cy="1000"/><a:chOff x="0" y="0"/><a:chExt cx="1000" cy="1000"/></a:xfrm></p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="6" name="Inner"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
<p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="500" cy="500"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
<p:txBody><a:bodyPr/><a:p><a:r><a:t>inner</a:t></a:r></a:p></p:txBody>
</p:sp>
</p:grpSp>
</p:spTree>
</p:cSld>
<p:clrMapOvr/>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse sld ok");
assert_eq!(sld.shapes.len(), 4, "应解析出 4 个形状");
assert!(
matches!(&sld.shapes[0], OxmlSlideShape::Sp(s) if s.id == 2 && s.name == "TextBox")
);
assert!(
matches!(&sld.shapes[1], OxmlSlideShape::Pic(p) if p.id == 3 && p.rid == "rIdImg1")
);
assert!(
matches!(&sld.shapes[2], OxmlSlideShape::CxnSp(c) if c.id == 4 && c.name == "Conn1")
);
match &sld.shapes[3] {
OxmlSlideShape::Group(grp) => {
assert_eq!(grp.id, 5);
assert_eq!(grp.name, "Group1");
assert_eq!(grp.children.len(), 1, "组合内应有 1 个子形状");
assert!(
matches!(&grp.children[0], GroupChild::Sp(s) if s.id == 6 && s.name == "Inner")
);
}
other => panic!("第 4 个形状应为 Group,实际:{other:?}"),
}
}
#[test]
fn round_trip_sld_preserves_shapes() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="2" name="TB1"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
<p:spPr><a:xfrm><a:off x="914400" y="457200"/><a:ext cx="3657600" cy="457200"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
<p:txBody><a:bodyPr/><a:p><a:r><a:t>round-trip</a:t></a:r></a:p></p:txBody>
</p:sp>
<p:cxnSp>
<p:nvCxnSpPr><p:cNvPr id="3" name="C1"/><p:cNvCxnSpPr/><p:nvPr><p:stCxn id="2" idx="0"/><p:endCxn id="5" idx="1"/></p:nvPr></p:nvCxnSpPr>
<p:spPr><a:xfrm><a:off x="100" y="200"/><a:ext cx="3000" cy="4000"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom></p:spPr>
</p:cxnSp>
</p:spTree>
</p:cSld>
<p:clrMapOvr/>
</p:sld>"#;
let sld1 = parse_sld(xml).expect("first parse ok");
assert_eq!(sld1.shapes.len(), 2);
let rewritten = sld1.to_xml();
let sld2 = parse_sld(&rewritten).expect("second parse ok");
assert_eq!(sld2.shapes.len(), 2, "round-trip 后形状数应不变");
match (&sld1.shapes[0], &sld2.shapes[0]) {
(OxmlSlideShape::Sp(a), OxmlSlideShape::Sp(b)) => {
assert_eq!(a.id, b.id, "Sp id 应保留");
assert_eq!(a.name, b.name, "Sp name 应保留");
assert_eq!(
a.text.paragraphs[0].runs[0].text, b.text.paragraphs[0].runs[0].text,
"Sp 文本应保留"
);
assert_eq!(
a.properties.xfrm.off_x, b.properties.xfrm.off_x,
"Sp off_x 应保留"
);
assert_eq!(
a.properties.xfrm.ext_cx, b.properties.xfrm.ext_cx,
"Sp ext_cx 应保留"
);
}
_ => panic!("形状类型不匹配"),
}
match (&sld1.shapes[1], &sld2.shapes[1]) {
(OxmlSlideShape::CxnSp(a), OxmlSlideShape::CxnSp(b)) => {
assert_eq!(a.id, b.id, "CxnSp id 应保留");
assert_eq!(a.name, b.name, "CxnSp name 应保留");
assert_eq!(a.st_cxn, b.st_cxn, "stCxn 挂接应保留");
assert_eq!(a.end_cxn, b.end_cxn, "endCxn 挂接应保留");
}
_ => panic!("形状类型不匹配"),
}
}
#[test]
fn round_trip_table_preserves_cells() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:graphicFrame>
<p:nvGraphicFramePr><p:cNvPr id="10" name="Tbl1"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
<p:xfrm><a:off x="0" y="0"/><a:ext cx="8000" cy="4000"/></p:xfrm>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
<a:tbl>
<a:tblPr/>
<a:tblGrid><a:gridCol w="4000"/><a:gridCol w="4000"/></a:tblGrid>
<a:tr h="2000">
<a:tc><a:txBody><a:bodyPr/><a:p><a:r><a:t>R1C1</a:t></a:r></a:p></a:txBody><a:tcPr/></a:tc>
<a:tc><a:txBody><a:bodyPr/><a:p><a:r><a:t>R1C2</a:t></a:r></a:p></a:txBody><a:tcPr/></a:tc>
</a:tr>
</a:tbl>
</a:graphicData>
</a:graphic>
</p:graphicFrame>
</p:spTree>
</p:cSld>
<p:clrMapOvr/>
</p:sld>"#;
let sld1 = parse_sld(xml).expect("first parse ok");
let rewritten = sld1.to_xml();
let sld2 = parse_sld(&rewritten).expect("second parse ok");
assert_eq!(sld1.shapes.len(), sld2.shapes.len(), "形状数应一致");
match (&sld1.shapes[0], &sld2.shapes[0]) {
(OxmlSlideShape::GraphicFrame(f1), OxmlSlideShape::GraphicFrame(f2)) => {
assert_eq!(f1.id, f2.id, "GraphicFrame id 应保留");
assert_eq!(f1.name, f2.name, "GraphicFrame name 应保留");
match (&f1.graphic, &f2.graphic) {
(
crate::oxml::shape::Graphic::Table(t1),
crate::oxml::shape::Graphic::Table(t2),
) => {
assert_eq!(t1.cols.len(), t2.cols.len(), "列数应保留");
assert_eq!(t1.rows.len(), t2.rows.len(), "行数应保留");
let get_text = |t: &crate::oxml::table::Table| -> String {
t.rows[0].cells[0]
.text
.paragraphs
.iter()
.flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
.collect()
};
assert_eq!(get_text(t1), get_text(t2), "单元格文本应保留");
assert_eq!(get_text(t1), "R1C1");
}
_ => panic!("期望两侧均为 Table 变体"),
}
}
_ => panic!("形状类型不匹配"),
}
}
#[test]
fn parse_sld_empty_sp_tree() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
</p:spTree>
</p:cSld>
<p:clrMapOvr/>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
assert_eq!(sld.shapes.len(), 0, "空 spTree 应解析出 0 个形状");
}
#[test]
fn parse_grp_sp_nested_group() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:grpSp xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:nvGrpSpPr><p:cNvPr id="1" name="Outer"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="10000" cy="10000"/><a:chOff x="0" y="0"/><a:chExt cx="10000" cy="10000"/></a:xfrm></p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="2" name="OuterChild"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
<p:spPr><a:xfrm><a:off x="100" y="100"/><a:ext cx="1000" cy="1000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
<p:txBody><a:bodyPr/><a:p><a:r><a:t>outer</a:t></a:r></a:p></p:txBody>
</p:sp>
<p:grpSp>
<p:nvGrpSpPr><p:cNvPr id="3" name="Inner"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm><a:off x="2000" y="2000"/><a:ext cx="5000" cy="5000"/><a:chOff x="0" y="0"/><a:chExt cx="5000" cy="5000"/></a:xfrm></p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="4" name="InnerChild"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
<p:spPr><a:xfrm><a:off x="50" y="50"/><a:ext cx="500" cy="500"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
<p:txBody><a:bodyPr/><a:p><a:r><a:t>inner</a:t></a:r></a:p></p:txBody>
</p:sp>
</p:grpSp>
</p:grpSp>"#;
let grp = parse_grp_sp(xml).expect("parse ok");
assert_eq!(grp.id, 1);
assert_eq!(grp.name, "Outer");
assert_eq!(
grp.children.len(),
2,
"外层组合应有 2 个子形状(sp + 嵌套 grpSp)"
);
assert!(
matches!(&grp.children[0], GroupChild::Sp(s) if s.id == 2 && s.name == "OuterChild")
);
match &grp.children[1] {
GroupChild::Group(inner) => {
assert_eq!(inner.id, 3);
assert_eq!(inner.name, "Inner");
assert_eq!(inner.children.len(), 1, "内层组合应有 1 个子形状");
assert!(
matches!(&inner.children[0], GroupChild::Sp(s) if s.id == 4 && s.name == "InnerChild")
);
}
other => panic!("第二个子形状应为 Group,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_grad_fill_linear() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:xfrm><a:off x="100" y="200"/><a:ext cx="300" cy="400"/></a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:gradFill flip="none" rotWithShape="1">
<a:gsLst>
<a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
<a:gs pos="100000"><a:srgbClr val="0000FF"/></a:gs>
</a:gsLst>
<a:lin ang="5400000" scaled="1"/>
</a:gradFill>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
match &sp.fill {
Fill::Gradient(grad) => {
assert_eq!(grad.stops.len(), 2, "应有 2 个渐变光轨");
assert_eq!(grad.stops[0].pos, 0);
assert_eq!(grad.stops[1].pos, 100000);
assert!(
matches!(grad.stops[0].color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00)
);
assert!(
matches!(grad.stops[1].color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0x00 && c.2 == 0xFF)
);
assert_eq!(grad.gradient_type, GradientType::Linear(5400000));
assert_eq!(grad.flip.as_deref(), Some("none"));
assert_eq!(grad.rot_with_shape, Some(true));
}
other => panic!("fill 应为 Gradient,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_grad_fill_path() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:gradFill>
<a:gsLst>
<a:gs pos="0"><a:srgbClr val="00FF00"/></a:gs>
<a:gs pos="50000"><a:srgbClr val="FFFF00"/></a:gs>
<a:gs pos="100000"><a:srgbClr val="FF00FF"/></a:gs>
</a:gsLst>
<a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path>
</a:gradFill>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
match &sp.fill {
Fill::Gradient(grad) => {
assert_eq!(grad.stops.len(), 3, "应有 3 个渐变光轨");
assert_eq!(grad.stops[1].pos, 50000);
assert_eq!(grad.gradient_type, GradientType::Path(GradientPath::Circle));
}
other => panic!("fill 应为 Gradient,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_patt_fill() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:pattFill prst="pct5">
<a:fgClr><a:srgbClr val="FF0000"/></a:fgClr>
<a:bgClr><a:srgbClr val="FFFFFF"/></a:bgClr>
</a:pattFill>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
match &sp.fill {
Fill::Pattern(patt) => {
assert_eq!(patt.prst, "pct5", "预置图案应为 pct5");
assert!(
matches!(patt.fg_color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00)
);
assert!(
matches!(patt.bg_color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0xFF)
);
}
other => panic!("fill 应为 Pattern,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_blip_fill_stretch() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:blipFill>
<a:blip r:embed="rId1"/>
<a:stretch><a:fillRect/></a:stretch>
</a:blipFill>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
match &sp.fill {
Fill::Blip { rid, mode } => {
assert_eq!(rid, "rId1", "rid 应为 rId1");
assert!(
matches!(mode, crate::oxml::sppr::BlipFillMode::Stretch),
"mode 应为 Stretch,实际:{:?}",
mode
);
}
other => panic!("fill 应为 Blip,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_blip_fill_tile() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:blipFill>
<a:blip r:embed="rIdImg7"/>
<a:tile tx="100" ty="200" sx="50000" sy="50000" flip="x" algn="ctr"/>
</a:blipFill>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
match &sp.fill {
Fill::Blip { rid, mode } => {
assert_eq!(rid, "rIdImg7", "rid 应为 rIdImg7");
match mode {
crate::oxml::sppr::BlipFillMode::Tile {
tx,
ty,
sx,
sy,
flip,
algn,
} => {
assert_eq!(*tx, Some(100), "tx 应为 100");
assert_eq!(*ty, Some(200), "ty 应为 200");
assert_eq!(*sx, Some(50000), "sx 应为 50000");
assert_eq!(*sy, Some(50000), "sy 应为 50000");
assert_eq!(flip.as_deref(), Some("x"), "flip 应为 x");
assert_eq!(algn.as_deref(), Some("ctr"), "algn 应为 ctr");
}
other => panic!("mode 应为 Tile,实际:{other:?}"),
}
}
other => panic!("fill 应为 Blip,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_blip_fill_self_closing() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:blipFill>
<a:blip r:embed="rId2"/>
<a:stretch/>
</a:blipFill>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
match &sp.fill {
Fill::Blip { rid, mode } => {
assert_eq!(rid, "rId2");
assert!(matches!(mode, crate::oxml::sppr::BlipFillMode::Stretch));
}
other => panic!("fill 应为 Blip,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_grad_fill_self_closing_lin() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:gradFill>
<a:gsLst>
<a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
<a:gs pos="100000"><a:srgbClr val="0000FF"/></a:gs>
</a:gsLst>
<a:lin ang="2700000" scaled="0"/>
</a:gradFill>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
match &sp.fill {
Fill::Gradient(grad) => {
assert_eq!(grad.gradient_type, GradientType::Linear(2700000));
assert_eq!(grad.stops.len(), 2);
}
other => panic!("fill 应为 Gradient,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_ln_with_arrow_heads() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:ln w="12700">
<a:solidFill><a:srgbClr val="000000"/></a:solidFill>
<a:headEnd type="triangle" w="lg" len="med"/>
<a:tailEnd type="stealth" w="sm" len="lg"/>
</a:ln>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
let ln = sp.line.expect("应有 line");
let head = ln.head_end.expect("应有 headEnd");
assert_eq!(head.arrow_type, ArrowType::Triangle);
assert_eq!(head.width, ArrowSize::Large);
assert_eq!(head.length, ArrowSize::Medium);
let tail = ln.tail_end.expect("应有 tailEnd");
assert_eq!(tail.arrow_type, ArrowType::Stealth);
assert_eq!(tail.width, ArrowSize::Small);
assert_eq!(tail.length, ArrowSize::Large);
}
#[test]
fn parse_sppr_ln_with_join() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:round/></a:ln>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
let ln = sp.line.expect("应有 line");
assert_eq!(ln.join, Some(LineJoin::Round));
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:miter lim="600000"/></a:ln>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
let ln = sp.line.expect("应有 line");
assert_eq!(ln.join, Some(LineJoin::Miter(600000)));
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:bevel/></a:ln>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
let ln = sp.line.expect("应有 line");
assert_eq!(ln.join, Some(LineJoin::Bevel));
}
#[test]
fn parse_sppr_ln_with_grad_fill() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:ln w="12700">
<a:gradFill>
<a:gsLst>
<a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
<a:gs pos="100000"><a:srgbClr val="00FF00"/></a:gs>
</a:gsLst>
<a:lin ang="0" scaled="1"/>
</a:gradFill>
</a:ln>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
let ln = sp.line.expect("应有 line");
match &ln.fill {
Fill::Gradient(grad) => {
assert_eq!(grad.stops.len(), 2);
assert_eq!(grad.gradient_type, GradientType::Linear(0));
}
other => panic!("line fill 应为 Gradient,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_ln_with_patt_fill() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:ln w="12700">
<a:pattFill prst="cross">
<a:fgClr><a:srgbClr val="FF0000"/></a:fgClr>
<a:bgClr><a:srgbClr val="FFFFFF"/></a:bgClr>
</a:pattFill>
</a:ln>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
let ln = sp.line.expect("应有 line");
match &ln.fill {
Fill::Pattern(patt) => {
assert_eq!(patt.prst, "cross");
assert!(matches!(patt.fg_color, Color::RGB(c) if c.0 == 0xFF));
assert!(
matches!(patt.bg_color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0xFF)
);
}
other => panic!("line fill 应为 Pattern,实际:{other:?}"),
}
}
#[test]
fn parse_sppr_effect_lst_outer_shadow() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:effectLst>
<a:outerShdw blurRad="40000" dist="38100" dir="2700000" rotWithShape="0">
<a:srgbClr val="000000"/>
</a:outerShdw>
</a:effectLst>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
let effects = sp.effects.expect("应有 effects");
let shdw = effects.outer_shadow.expect("应有 outer_shadow");
assert_eq!(shdw.dir, 2_700_000);
assert_eq!(shdw.dist, 38100);
assert_eq!(shdw.blur_rad, 40000);
assert_eq!(shdw.rot_with_shape, Some(false));
assert!(matches!(shdw.color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0x00 && c.2 == 0x00));
}
#[test]
fn parse_sppr_effect_lst_glow_and_soft_edge() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:effectLst>
<a:glow rad="50000">
<a:srgbClr val="FF00FF"/>
</a:glow>
<a:softEdge rad="25000"/>
</a:effectLst>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
let effects = sp.effects.expect("应有 effects");
let glow = effects.glow.expect("应有 glow");
assert_eq!(glow.rad, 50000);
assert!(matches!(glow.color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0xFF));
let se = effects.soft_edge.expect("应有 soft_edge");
assert_eq!(se.rad, 25000);
}
#[test]
fn parse_sppr_effect_lst_reflection() {
let xml = r#"<p:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:effectLst>
<a:reflection blurRad="50000" stA="52000" stPos="0" endA="30000" endPos="50000" dist="38100" dir="5400000"/>
</a:effectLst>
</p:spPr>"#;
let sp = parse_sppr(xml).expect("parse ok");
let effects = sp.effects.expect("应有 effects");
let refl = effects.reflection.expect("应有 reflection");
assert_eq!(refl.blur_rad, Some(50000));
assert_eq!(refl.st_a, Some(52000));
assert_eq!(refl.st_pos, Some(0));
assert_eq!(refl.end_a, Some(30000));
assert_eq!(refl.end_pos, Some(50000));
assert_eq!(refl.dist, Some(38100));
assert_eq!(refl.dir, Some(5_400_000));
}
#[test]
fn round_trip_cell_merge_attributes() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld><p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:graphicFrame>
<p:nvGraphicFramePr><p:cNvPr id="2" name="MergedTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
<p:xfrm><a:off x="0" y="0"/><a:ext cx="6000000" cy="4000000"/></p:xfrm>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
<a:tbl>
<a:tblPr><a:tableStyleId>{5940675A-B579-460E-94D1-54222C63F5DA}</a:tableStyleId></a:tblPr>
<a:tblGrid>
<a:gridCol w="2000000"/>
<a:gridCol w="2000000"/>
<a:gridCol w="2000000"/>
</a:tblGrid>
<a:tr h="2000000">
<a:tc gridSpan="3" rowSpan="2">
<a:txBody><a:bodyPr/><a:p><a:r><a:t>Merged</a:t></a:r></a:p></a:txBody>
<a:tcPr/>
</a:tc>
</a:tr>
<a:tr h="2000000">
<a:tc hMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
<a:tc hMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
<a:tc vMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
</a:tr>
</a:tbl>
</a:graphicData>
</a:graphic>
</p:graphicFrame>
</p:spTree></p:cSld>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
let frame = sld
.shapes
.iter()
.find_map(|s| {
if let OxmlSlideShape::GraphicFrame(g) = s {
Some(g)
} else {
None
}
})
.expect("应有 graphicFrame");
match &frame.graphic {
crate::oxml::shape::Graphic::Table(tbl) => {
assert_eq!(tbl.rows.len(), 2, "应有 2 行");
assert_eq!(
tbl.table_style
.as_ref()
.expect("应有 table_style")
.style_id(),
"{5940675A-B579-460E-94D1-54222C63F5DA}"
);
let origin = &tbl.rows[0].cells[0];
assert_eq!(origin.grid_span, 3, "gridSpan 应为 3");
assert_eq!(origin.row_span, 2, "rowSpan 应为 2");
assert!(!origin.h_merge, "合并源不应有 hMerge");
assert!(!origin.v_merge, "合并源不应有 vMerge");
let h1 = &tbl.rows[1].cells[0];
assert!(h1.h_merge, "应有 hMerge");
let v1 = &tbl.rows[1].cells[2];
assert!(v1.v_merge, "应有 vMerge");
}
_ => panic!("期望 Table 变体"),
}
}
#[test]
fn round_trip_table_style_id() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld><p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:graphicFrame>
<p:nvGraphicFramePr><p:cNvPr id="2" name="StyledTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
<p:xfrm><a:off x="0" y="0"/><a:ext cx="4000000" cy="2000000"/></p:xfrm>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
<a:tbl>
<a:tblPr><a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId></a:tblPr>
<a:tblGrid><a:gridCol w="2000000"/><a:gridCol w="2000000"/></a:tblGrid>
<a:tr h="2000000">
<a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
<a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
</a:tr>
</a:tbl>
</a:graphicData>
</a:graphic>
</p:graphicFrame>
</p:spTree></p:cSld>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
let frame = sld
.shapes
.iter()
.find_map(|s| {
if let OxmlSlideShape::GraphicFrame(g) = s {
Some(g)
} else {
None
}
})
.expect("应有 graphicFrame");
match &frame.graphic {
crate::oxml::shape::Graphic::Table(tbl) => {
let style = tbl.table_style.as_ref().expect("应有 table_style");
assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
let mut w = crate::oxml::writer::XmlWriter::new();
tbl.write_xml(&mut w);
let out = &w.buf;
assert!(out.contains(
"<a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId>"
));
}
_ => panic!("期望 Table 变体"),
}
}
#[test]
fn parse_table_without_style_id() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld><p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:graphicFrame>
<p:nvGraphicFramePr><p:cNvPr id="2" name="PlainTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
<p:xfrm><a:off x="0" y="0"/><a:ext cx="4000000" cy="2000000"/></p:xfrm>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
<a:tbl>
<a:tblPr/>
<a:tblGrid><a:gridCol w="2000000"/><a:gridCol w="2000000"/></a:tblGrid>
<a:tr h="2000000">
<a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
<a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody><a:tcPr/></a:tc>
</a:tr>
</a:tbl>
</a:graphicData>
</a:graphic>
</p:graphicFrame>
</p:spTree></p:cSld>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
let frame = sld
.shapes
.iter()
.find_map(|s| {
if let OxmlSlideShape::GraphicFrame(g) = s {
Some(g)
} else {
None
}
})
.expect("应有 graphicFrame");
match &frame.graphic {
crate::oxml::shape::Graphic::Table(tbl) => {
assert!(
tbl.table_style.is_none(),
"无 tableStyleId 时 table_style 应为 None"
);
}
_ => panic!("期望 Table 变体"),
}
}
#[test]
fn round_trip_cell_borders_margins_anchor() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld><p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:graphicFrame>
<p:nvGraphicFramePr><p:cNvPr id="2" name="FormattedTable"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
<p:xfrm><a:off x="0" y="0"/><a:ext cx="4000000" cy="2000000"/></p:xfrm>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
<a:tbl>
<a:tblPr><a:tableStyleId>{5940675A-B579-460E-94D1-54222C63F5DA}</a:tableStyleId></a:tblPr>
<a:tblGrid><a:gridCol w="2000000"/><a:gridCol w="2000000"/></a:tblGrid>
<a:tr h="2000000">
<a:tc>
<a:txBody><a:bodyPr/><a:p><a:r><a:t>A1</a:t></a:r></a:p></a:txBody>
<a:tcPr marT="50000" marL="60000" marB="70000" marR="80000" anchor="ctr">
<a:lnL w="9525"><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill></a:lnL>
<a:lnR w="9525"><a:solidFill><a:srgbClr val="00FF00"/></a:solidFill></a:lnR>
<a:lnT w="9525"><a:solidFill><a:srgbClr val="0000FF"/></a:solidFill></a:lnT>
<a:lnB w="9525"><a:noFill/></a:lnB>
</a:tcPr>
</a:tc>
<a:tc>
<a:txBody><a:bodyPr/><a:p><a:r><a:t>B1</a:t></a:r></a:p></a:txBody>
<a:tcPr/>
</a:tc>
</a:tr>
</a:tbl>
</a:graphicData>
</a:graphic>
</p:graphicFrame>
</p:spTree></p:cSld>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
let frame = sld
.shapes
.iter()
.find_map(|s| {
if let OxmlSlideShape::GraphicFrame(g) = s {
Some(g)
} else {
None
}
})
.expect("应有 graphicFrame");
let tbl = match &frame.graphic {
crate::oxml::shape::Graphic::Table(t) => t,
_ => panic!("期望 Table 变体"),
};
let cell = &tbl.rows[0].cells[0];
assert_eq!(cell.margin.0.map(|m| m.value()), Some(50000), "marT");
assert_eq!(cell.margin.1.map(|m| m.value()), Some(60000), "marL");
assert_eq!(cell.margin.2.map(|m| m.value()), Some(70000), "marB");
assert_eq!(cell.margin.3.map(|m| m.value()), Some(80000), "marR");
assert_eq!(
cell.anchor,
crate::oxml::table::VerticalAnchor::Middle,
"anchor=ctr"
);
let bl = cell.border_left.as_ref().expect("应有 lnL");
assert_eq!(bl.width.value(), 9525);
assert!(matches!(bl.color, Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00));
let br = cell.border_right.as_ref().expect("应有 lnR");
assert!(matches!(br.color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0xFF && c.2 == 0x00));
let bt = cell.border_top.as_ref().expect("应有 lnT");
assert!(matches!(bt.color, Color::RGB(c) if c.0 == 0x00 && c.1 == 0x00 && c.2 == 0xFF));
let bb = cell.border_bottom.as_ref().expect("应有 lnB");
assert!(bb.no_fill, "lnB 应为 noFill");
}
#[test]
fn parse_txbody_multi_column() {
let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:bodyPr numCol="3" spcCol="91440" anchor="ctr" wrap="square" lIns="91440" tIns="45720" rIns="91440" bIns="45720"/>
<a:p><a:r><a:t>multi-col text</a:t></a:r></a:p>
</p:txBody>"#;
let tb = parse_txbody(xml).expect("parse ok");
let bp = tb.body_properties.as_ref().expect("应有 body_properties");
assert_eq!(bp.num_cols, Some(3), "numCol");
assert_eq!(bp.col_spacing.map(|v| v.value()), Some(91440), "spcCol");
assert_eq!(bp.anchor, Some(MsoAnchor::Middle), "anchor=ctr");
assert_eq!(bp.wrap, Some(TextWrapping::Square), "wrap=square");
let insets = bp.insets.as_ref().expect("应有 insets");
assert_eq!(insets.left.value(), 91440, "lIns");
assert_eq!(insets.top.value(), 45720, "tIns");
assert_eq!(insets.right.value(), 91440, "rIns");
assert_eq!(insets.bottom.value(), 45720, "bIns");
assert_eq!(tb.paragraphs.len(), 1, "应有一个段落");
assert_eq!(tb.paragraphs[0].runs.len(), 1, "应有一个 run");
assert_eq!(tb.paragraphs[0].runs[0].text, "multi-col text");
}
#[test]
fn parse_txbody_self_closing_body_pr() {
let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:bodyPr numCol="2" spcCol="45720"/>
<a:p/>
</p:txBody>"#;
let tb = parse_txbody(xml).expect("parse ok");
let bp = tb.body_properties.as_ref().expect("应有 body_properties");
assert_eq!(bp.num_cols, Some(2), "numCol");
assert_eq!(bp.col_spacing.map(|v| v.value()), Some(45720), "spcCol");
}
#[test]
fn parse_txbody_with_sp_auto_fit() {
let xml = r#"<p:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<a:bodyPr numCol="1"><a:spAutoFit/></a:bodyPr>
<a:p/>
</p:txBody>"#;
let tb = parse_txbody(xml).expect("parse ok");
let bp = tb.body_properties.as_ref().expect("应有 body_properties");
assert_eq!(bp.num_cols, Some(1), "numCol=1");
assert!(bp.sp_auto_fit, "应启用 spAutoFit");
assert!(!bp.norm_autofit, "不应启用 normAutofit");
}
#[test]
fn parse_paragraph_bullet_styles() {
let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:buChar char="•"/>
</a:pPr>"#;
let ppr = parse_paragraph_properties(xml).expect("parse ok");
assert!(ppr.bullet, "bullet 应为 true");
match &ppr.bullet_style {
Some(BulletStyle::Char { char }) => assert_eq!(char, "•"),
other => panic!("期望 Char,实际: {other:?}"),
}
let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:buAutoNum type="arabicPeriod" startAt="3"/>
</a:pPr>"#;
let ppr = parse_paragraph_properties(xml).expect("parse ok");
assert!(ppr.bullet);
match &ppr.bullet_style {
Some(BulletStyle::AutoNum {
auto_num_type,
start_at,
}) => {
assert_eq!(auto_num_type, "arabicPeriod");
assert_eq!(*start_at, Some(3));
}
other => panic!("期望 AutoNum,实际: {other:?}"),
}
let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:buAutoNum type="alphaLcParenR"/>
</a:pPr>"#;
let ppr = parse_paragraph_properties(xml).expect("parse ok");
match &ppr.bullet_style {
Some(BulletStyle::AutoNum {
auto_num_type,
start_at,
}) => {
assert_eq!(auto_num_type, "alphaLcParenR");
assert_eq!(*start_at, None);
}
other => panic!("期望 AutoNum,实际: {other:?}"),
}
let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:buNone/>
</a:pPr>"#;
let ppr = parse_paragraph_properties(xml).expect("parse ok");
assert!(!ppr.bullet, "bullet 应为 false");
assert!(matches!(ppr.bullet_style, Some(BulletStyle::None)));
}
#[test]
fn parse_paragraph_bullet_styles_self_closing() {
let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:buChar char="▪"/>
</a:pPr>"#;
let ppr = parse_paragraph_properties(xml).expect("parse ok");
match &ppr.bullet_style {
Some(BulletStyle::Char { char }) => assert_eq!(char, "▪"),
other => panic!("期望 Char,实际: {other:?}"),
}
let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:buAutoNum type="romanLcParenBoth" startAt="5"/>
</a:pPr>"#;
let ppr = parse_paragraph_properties(xml).expect("parse ok");
match &ppr.bullet_style {
Some(BulletStyle::AutoNum {
auto_num_type,
start_at,
}) => {
assert_eq!(auto_num_type, "romanLcParenBoth");
assert_eq!(*start_at, Some(5));
}
other => panic!("期望 AutoNum,实际: {other:?}"),
}
}
#[test]
fn parse_paragraph_tab_stops() {
let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:tabLst>
<a:tab pos="914400" algn="l"/>
<a:tab pos="1828800" algn="r"/>
<a:tab pos="2743200" algn="ctr"/>
<a:tab pos="3657600" algn="dec"/>
</a:tabLst>
</a:pPr>"#;
let ppr = parse_paragraph_properties(xml).expect("parse ok");
assert_eq!(ppr.tab_stops.len(), 4, "应有 4 个制表位");
assert_eq!(ppr.tab_stops[0].pos.value(), 914400);
assert_eq!(ppr.tab_stops[0].alignment, TabAlignment::Left);
assert_eq!(ppr.tab_stops[1].pos.value(), 1828800);
assert_eq!(ppr.tab_stops[1].alignment, TabAlignment::Right);
assert_eq!(ppr.tab_stops[2].pos.value(), 2743200);
assert_eq!(ppr.tab_stops[2].alignment, TabAlignment::Center);
assert_eq!(ppr.tab_stops[3].pos.value(), 3657600);
assert_eq!(ppr.tab_stops[3].alignment, TabAlignment::Decimal);
}
#[test]
fn parse_paragraph_empty_tab_lst() {
let xml = r#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:tabLst/>
</a:pPr>"#;
let ppr = parse_paragraph_properties(xml).expect("parse ok");
assert!(ppr.tab_stops.is_empty(), "空 tabLst 应无制表位");
}
#[test]
fn parse_paragraph_field() {
let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:fld id="{12345678-ABCD-EF01-2345-678901234567}" type="slidenum">
<a:rPr lang="en-US"/>
<a:t>1</a:t>
</a:fld>
</a:p>"#;
let p = parse_paragraph(xml).expect("parse ok");
assert_eq!(p.fields.len(), 1, "应有 1 个字段");
let f = &p.fields[0];
assert_eq!(f.id, "{12345678-ABCD-EF01-2345-678901234567}");
assert_eq!(f.field_type, FieldType::SlideNumber);
assert_eq!(f.text, "1");
}
#[test]
fn parse_paragraph_mixed_runs_and_fields() {
let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:r><a:t>Page </a:t></a:r>
<a:fld id="{AAA}" type="slidenum"><a:t>1</a:t></a:fld>
<a:fld id="{BBB}" type="datetime"><a:t>1/1/2024</a:t></a:fld>
</a:p>"#;
let p = parse_paragraph(xml).expect("parse ok");
assert_eq!(p.runs.len(), 1, "应有 1 个 run");
assert_eq!(p.runs[0].text, "Page");
assert_eq!(p.fields.len(), 2, "应有 2 个字段");
assert_eq!(p.fields[0].field_type, FieldType::SlideNumber);
assert_eq!(p.fields[0].text, "1");
assert_eq!(p.fields[1].field_type, FieldType::DateTime);
assert_eq!(p.fields[1].text, "1/1/2024");
}
#[test]
fn parse_paragraph_custom_field_type() {
let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:fld id="{CCC}" type="customField"><a:t>custom</a:t></a:fld>
</a:p>"#;
let p = parse_paragraph(xml).expect("parse ok");
assert_eq!(p.fields.len(), 1);
assert_eq!(
p.fields[0].field_type,
FieldType::Custom("customField".to_string())
);
assert_eq!(p.fields[0].text, "custom");
}
#[test]
fn parse_run_properties_hlink_click_self_closing() {
let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" lang="en-US">
<a:hlinkClick r:id="rId3" tooltip="点击访问"/>
</a:rPr>"#;
let rp = parse_run_properties(xml).expect("parse ok");
let hl = rp.hlink_click.expect("hlink_click 应存在");
assert_eq!(hl.rid.as_deref(), Some("rId3"));
assert_eq!(hl.tooltip.as_deref(), Some("点击访问"));
assert!(hl.action.is_none());
assert!(!hl.invalid);
}
#[test]
fn parse_run_properties_hlink_click_action() {
let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<a:hlinkClick action="ppaction://hlinksldjump"/>
</a:rPr>"#;
let rp = parse_run_properties(xml).expect("parse ok");
let hl = rp.hlink_click.expect("hlink_click 应存在");
assert_eq!(hl.action.as_deref(), Some("ppaction://hlinksldjump"));
assert!(hl.rid.is_none());
}
#[test]
fn parse_run_properties_hlink_hover() {
let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<a:hlinkHover r:id="rId5" tooltip="悬停提示"/>
</a:rPr>"#;
let rp = parse_run_properties(xml).expect("parse ok");
let hl = rp.hlink_hover.expect("hlink_hover 应存在");
assert_eq!(hl.rid.as_deref(), Some("rId5"));
assert_eq!(hl.tooltip.as_deref(), Some("悬停提示"));
}
#[test]
fn parse_run_properties_both_hlinks() {
let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<a:hlinkClick r:id="rId1" tooltip="click"/>
<a:hlinkHover r:id="rId2" tooltip="hover"/>
</a:rPr>"#;
let rp = parse_run_properties(xml).expect("parse ok");
assert_eq!(
rp.hlink_click.as_ref().unwrap().rid.as_deref(),
Some("rId1")
);
assert_eq!(
rp.hlink_hover.as_ref().unwrap().rid.as_deref(),
Some("rId2")
);
}
#[test]
fn parse_run_properties_hlink_click_empty() {
let xml = r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:hlinkClick/>
</a:rPr>"#;
let rp = parse_run_properties(xml).expect("parse ok");
let hl = rp.hlink_click.expect("hlink_click 应存在");
assert!(hl.invalid);
assert!(hl.rid.is_none());
}
#[test]
fn parse_sp_with_locks() {
let xml = r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<p:nvSpPr>
<p:cNvPr id="2" name="Locked"/>
<p:cNvSpPr>
<a:spLocks noGrp="1" noSelect="1" noResize="1"/>
</p:cNvSpPr>
<p:nvPr/>
</p:nvSpPr>
<p:spPr/>
<p:txBody><a:bodyPr/><a:p/></p:txBody>
</p:sp>"#;
let sp = parse_sp(xml).expect("parse ok");
let locks = sp.locks.as_ref().expect("locks 应存在");
assert!(locks.no_grp);
assert!(locks.no_select);
assert!(locks.no_resize);
assert!(!locks.no_move);
assert!(!locks.no_rot);
}
#[test]
fn parse_sp_locks_with_txbox() {
let xml = r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<p:nvSpPr>
<p:cNvPr id="3" name="TB"/>
<p:cNvSpPr txBox="1">
<a:spLocks noChangeAspect="1"/>
</p:cNvSpPr>
<p:nvPr/>
</p:nvSpPr>
<p:spPr/>
<p:txBody><a:bodyPr/><a:p/></p:txBody>
</p:sp>"#;
let sp = parse_sp(xml).expect("parse ok");
assert!(sp.c_nv_sp_pr_tx_box, "txBox 应为 true");
let locks = sp.locks.as_ref().expect("locks 应存在");
assert!(locks.no_change_aspect);
assert!(!locks.no_grp);
}
#[test]
fn parse_sp_with_style() {
let xml = r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<p:nvSpPr>
<p:cNvPr id="4" name="Styled"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr/>
<p:style>
<a:lnRef idx="1"><a:schemeClr val="accent1"/></a:lnRef>
<a:fillRef idx="2"><a:schemeClr val="accent2"/></a:fillRef>
<a:effectRef idx="0"><a:schemeClr val="accent3"/></a:effectRef>
<a:fontRef idx="minor"><a:schemeClr val="tx1"/></a:fontRef>
</p:style>
<p:txBody><a:bodyPr/><a:p/></p:txBody>
</p:sp>"#;
let sp = parse_sp(xml).expect("parse ok");
let style = sp.style.as_ref().expect("style 应存在");
let ln = style.line_ref.as_ref().expect("line_ref 应存在");
assert_eq!(ln.idx.as_deref(), Some("1"));
assert_eq!(ln.scheme_color.as_deref(), Some("accent1"));
let fill = style.fill_ref.as_ref().expect("fill_ref 应存在");
assert_eq!(fill.idx.as_deref(), Some("2"));
assert_eq!(fill.scheme_color.as_deref(), Some("accent2"));
let eff = style.effect_ref.as_ref().expect("effect_ref 应存在");
assert_eq!(eff.idx.as_deref(), Some("0"));
let font = style.font_ref.as_ref().expect("font_ref 应存在");
assert_eq!(font.idx.as_deref(), Some("minor"));
assert_eq!(font.scheme_color.as_deref(), Some("tx1"));
}
#[test]
fn parse_sp_style_self_closing_refs() {
let xml = r#"<p:style xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:lnRef idx="3"/>
<a:fillRef idx="1"/>
</p:style>"#;
let style = parse_shape_style(xml).expect("parse ok");
assert_eq!(style.line_ref.as_ref().unwrap().idx.as_deref(), Some("3"));
assert!(style.line_ref.as_ref().unwrap().scheme_color.is_none());
assert_eq!(style.fill_ref.as_ref().unwrap().idx.as_deref(), Some("1"));
assert!(style.effect_ref.is_none());
assert!(style.font_ref.is_none());
}
#[test]
fn parse_transition_fade() {
let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="slow" advClick="0" advTm="5000">
<p:fade thruBlk="1"/>
</p:transition>"#;
let tr = parse_transition(xml).expect("parse ok");
assert_eq!(tr.speed, TransitionSpeed::Slow);
assert!(!tr.advance_click);
assert_eq!(tr.advance_after_ms, Some(5000));
match &tr.transition_type {
TransitionType::Fade { thru_blk } => assert!(*thru_blk),
other => panic!("expected Fade, got {:?}", other),
}
}
#[test]
fn parse_transition_push() {
let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="fast">
<p:push dir="l"/>
</p:transition>"#;
let tr = parse_transition(xml).expect("parse ok");
assert_eq!(tr.speed, TransitionSpeed::Fast);
assert!(tr.advance_click);
assert!(tr.advance_after_ms.is_none());
match &tr.transition_type {
TransitionType::Push { dir } => assert_eq!(*dir, TransitionDirection::Left),
other => panic!("expected Push, got {:?}", other),
}
}
#[test]
fn parse_transition_morph() {
let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:morph option="byChar"/>
</p:transition>"#;
let tr = parse_transition(xml).expect("parse ok");
match &tr.transition_type {
TransitionType::Morph { option } => assert_eq!(*option, MorphOption::ByChar),
other => panic!("expected Morph, got {:?}", other),
}
}
#[test]
fn parse_transition_split() {
let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:split orient="vert" dir="d"/>
</p:transition>"#;
let tr = parse_transition(xml).expect("parse ok");
match &tr.transition_type {
TransitionType::Split { orient, dir } => {
assert_eq!(*orient, SplitOrientation::Vertical);
assert_eq!(*dir, TransitionDirection::Down);
}
other => panic!("expected Split, got {:?}", other),
}
}
#[test]
fn parse_transition_self_closing() {
let xml = r#"<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med" advClick="1"/>"#;
let tr = parse_transition(xml).expect("parse ok");
assert_eq!(tr.speed, TransitionSpeed::Medium);
assert!(tr.advance_click);
assert_eq!(tr.transition_type, TransitionType::None);
}
#[test]
fn parse_sld_with_transition() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
</p:spTree>
</p:cSld>
<p:clrMapOvr/>
<p:transition spd="slow" advTm="3000">
<p:fade/>
</p:transition>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
let tr = sld.transition.expect("transition should exist");
assert_eq!(tr.speed, TransitionSpeed::Slow);
assert_eq!(tr.advance_after_ms, Some(3000));
match &tr.transition_type {
TransitionType::Fade { thru_blk } => assert!(!*thru_blk),
other => panic!("expected Fade, got {:?}", other),
}
}
#[test]
fn transition_round_trip() {
use crate::oxml::slide::Transition;
use crate::oxml::writer::XmlWriter;
let original = Transition {
speed: TransitionSpeed::Fast,
advance_click: false,
advance_after_ms: Some(8000),
transition_type: TransitionType::Push {
dir: TransitionDirection::Up,
},
};
let mut w = XmlWriter::new();
original.write_xml(&mut w);
let xml = w.buf.clone();
let parsed = parse_transition(&xml).expect("parse ok");
assert_eq!(parsed.speed, original.speed);
assert_eq!(parsed.advance_click, original.advance_click);
assert_eq!(parsed.advance_after_ms, original.advance_after_ms);
assert_eq!(parsed.transition_type, original.transition_type);
}
#[test]
fn parse_custom_geometry_basic() {
let xml = r#"<a:custGeom>
<a:avLst/>
<a:rect l="0" t="0" r="100" b="100"/>
<a:pathLst>
<a:path w="100" h="100" fill="norm" stroke="norm">
<a:moveTo><a:pt x="0" y="0"/></a:moveTo>
<a:lnTo><a:pt x="100" y="0"/></a:lnTo>
<a:lnTo><a:pt x="50" y="100"/></a:lnTo>
<a:close/>
</a:path>
</a:pathLst>
</a:custGeom>"#;
let geom = parse_custom_geometry(xml).expect("parse ok");
assert_eq!(
geom.rect,
Some(GeomRect {
l: "0".to_string(),
t: "0".to_string(),
r: "100".to_string(),
b: "100".to_string(),
})
);
assert_eq!(geom.path_list.len(), 1);
let p = &geom.path_list[0];
assert_eq!(p.width, 100);
assert_eq!(p.height, 100);
assert_eq!(p.fill.as_deref(), Some("norm"));
assert_eq!(p.stroke.as_deref(), Some("norm"));
assert_eq!(p.segments.len(), 4);
match &p.segments[0] {
PathSegment::MoveTo { x, y } => {
assert_eq!(*x, 0);
assert_eq!(*y, 0);
}
other => panic!("expected MoveTo, got {:?}", other),
}
match &p.segments[1] {
PathSegment::LineTo { x, y } => {
assert_eq!(*x, 100);
assert_eq!(*y, 0);
}
other => panic!("expected LineTo, got {:?}", other),
}
match &p.segments[2] {
PathSegment::LineTo { x, y } => {
assert_eq!(*x, 50);
assert_eq!(*y, 100);
}
other => panic!("expected LineTo, got {:?}", other),
}
match &p.segments[3] {
PathSegment::Close => {}
other => panic!("expected Close, got {:?}", other),
}
}
#[test]
fn parse_custom_geometry_with_bez() {
let xml = r#"<a:custGeom>
<a:avLst/>
<a:pathLst>
<a:path w="200" h="200">
<a:moveTo><a:pt x="10" y="10"/></a:moveTo>
<a:cubicBezTo>
<a:pt x="50" y="0"/>
<a:pt x="150" y="0"/>
<a:pt x="190" y="10"/>
</a:cubicBezTo>
<a:quadBezTo>
<a:pt x="190" y="100"/>
<a:pt x="100" y="190"/>
</a:quadBezTo>
</a:path>
</a:pathLst>
</a:custGeom>"#;
let geom = parse_custom_geometry(xml).expect("parse ok");
assert_eq!(geom.path_list.len(), 1);
let p = &geom.path_list[0];
assert_eq!(p.segments.len(), 3);
match &p.segments[1] {
PathSegment::CubicBezTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
assert_eq!(*x1, 50);
assert_eq!(*y1, 0);
assert_eq!(*x2, 150);
assert_eq!(*y2, 0);
assert_eq!(*x3, 190);
assert_eq!(*y3, 10);
}
other => panic!("expected CubicBezTo, got {:?}", other),
}
match &p.segments[2] {
PathSegment::QuadBezTo { x1, y1, x2, y2 } => {
assert_eq!(*x1, 190);
assert_eq!(*y1, 100);
assert_eq!(*x2, 100);
assert_eq!(*y2, 190);
}
other => panic!("expected QuadBezTo, got {:?}", other),
}
}
#[test]
fn parse_custom_geometry_with_arc() {
let xml = r#"<a:custGeom>
<a:avLst/>
<a:pathLst>
<a:path w="100" h="100">
<a:moveTo><a:pt x="0" y="50"/></a:moveTo>
<a:arcTo wR="50" hR="50" stAng="0" swAng="5400000"/>
<a:close/>
</a:path>
</a:pathLst>
</a:custGeom>"#;
let geom = parse_custom_geometry(xml).expect("parse ok");
let p = &geom.path_list[0];
assert_eq!(p.segments.len(), 3);
match &p.segments[1] {
PathSegment::ArcTo {
w_r,
h_r,
st_ang,
sw_ang,
} => {
assert_eq!(*w_r, 50);
assert_eq!(*h_r, 50);
assert_eq!(*st_ang, 0);
assert_eq!(*sw_ang, 5400000);
}
other => panic!("expected ArcTo, got {:?}", other),
}
}
#[test]
fn parse_custom_geometry_fill_stroke() {
let xml = r#"<a:custGeom>
<a:avLst/>
<a:fill>norm</a:fill>
<a:stroke>none</a:stroke>
<a:pathLst>
<a:path w="10" h="10">
<a:moveTo><a:pt x="0" y="0"/></a:moveTo>
</a:path>
</a:pathLst>
</a:custGeom>"#;
let geom = parse_custom_geometry(xml).expect("parse ok");
assert_eq!(geom.fill.as_deref(), Some("norm"));
assert_eq!(geom.stroke.as_deref(), Some("none"));
}
#[test]
fn custom_geometry_round_trip() {
use crate::oxml::writer::XmlWriter;
let original = CustomGeometry {
fill: Some("norm".to_string()),
stroke: None,
rect: Some(GeomRect {
l: "0".to_string(),
t: "0".to_string(),
r: "100".to_string(),
b: "100".to_string(),
}),
path_list: vec![Path {
width: 100,
height: 100,
fill: Some("norm".to_string()),
stroke: None,
segments: vec![
PathSegment::MoveTo { x: 0, y: 0 },
PathSegment::LineTo { x: 100, y: 0 },
PathSegment::LineTo { x: 100, y: 100 },
PathSegment::LineTo { x: 0, y: 100 },
PathSegment::Close,
],
}],
};
let mut w = XmlWriter::new();
original.write_xml(&mut w);
let xml = w.buf.clone();
let parsed = parse_custom_geometry(&xml).expect("parse ok");
assert_eq!(parsed.fill, original.fill);
assert_eq!(parsed.stroke, original.stroke);
assert_eq!(parsed.rect, original.rect);
assert_eq!(parsed.path_list.len(), 1);
let p0 = &parsed.path_list[0];
assert_eq!(p0.width, 100);
assert_eq!(p0.height, 100);
assert_eq!(p0.fill.as_deref(), Some("norm"));
assert_eq!(p0.segments.len(), 5);
match &p0.segments[0] {
PathSegment::MoveTo { x, y } => {
assert_eq!(*x, 0);
assert_eq!(*y, 0);
}
other => panic!("expected MoveTo, got {:?}", other),
}
match &p0.segments[4] {
PathSegment::Close => {}
other => panic!("expected Close, got {:?}", other),
}
}
#[test]
fn parse_sp_with_custgeom() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="10" name="Freeform 1"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr>
<a:xfrm>
<a:off x="100" y="100"/>
<a:ext cx="200" cy="200"/>
</a:xfrm>
<a:custGeom>
<a:avLst/>
<a:pathLst>
<a:path w="200" h="200">
<a:moveTo><a:pt x="0" y="0"/></a:moveTo>
<a:lnTo><a:pt x="200" y="0"/></a:lnTo>
<a:lnTo><a:pt x="100" y="200"/></a:lnTo>
<a:close/>
</a:path>
</a:pathLst>
</a:custGeom>
</p:spPr>
</p:sp>
</p:spTree>
</p:cSld>
<p:clrMapOvr/>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
assert_eq!(sld.shapes.len(), 1);
match &sld.shapes[0] {
OxmlSlideShape::Sp(sp) => {
let geom = sp
.properties
.geometry
.as_ref()
.expect("geometry should exist");
match geom {
Geometry::Custom(cg) => {
assert_eq!(cg.path_list.len(), 1);
let p = &cg.path_list[0];
assert_eq!(p.width, 200);
assert_eq!(p.height, 200);
assert_eq!(p.segments.len(), 4);
match &p.segments[0] {
PathSegment::MoveTo { x, y } => {
assert_eq!(*x, 0);
assert_eq!(*y, 0);
}
other => panic!("expected MoveTo, got {:?}", other),
}
}
other => panic!("expected Custom geometry, got {:?}", other),
}
}
other => panic!("expected Sp, got {:?}", other),
}
}
#[test]
fn geometry_preset_write_xml() {
use crate::oxml::simpletypes::PresetGeometry;
use crate::oxml::writer::XmlWriter;
let geom = Geometry::preset(PresetGeometry::Rectangle);
let mut w = XmlWriter::new();
geom.write_xml(&mut w);
let xml = w.buf.clone();
assert!(xml.contains("<a:prstGeom prst=\"rect\">"), "xml: {}", xml);
assert!(xml.contains("<a:avLst/>"), "xml: {}", xml);
assert!(xml.contains("</a:prstGeom>"), "xml: {}", xml);
}
#[test]
fn geometry_custom_write_xml() {
use crate::oxml::writer::XmlWriter;
let geom = Geometry::Custom(CustomGeometry {
fill: None,
stroke: None,
rect: None,
path_list: vec![Path {
width: 50,
height: 50,
fill: None,
stroke: None,
segments: vec![
PathSegment::MoveTo { x: 0, y: 0 },
PathSegment::LineTo { x: 50, y: 50 },
PathSegment::Close,
],
}],
});
let mut w = XmlWriter::new();
geom.write_xml(&mut w);
let xml = w.buf.clone();
assert!(xml.contains("<a:custGeom>"), "xml: {}", xml);
assert!(xml.contains("<a:avLst/>"), "xml: {}", xml);
assert!(xml.contains("<a:pathLst>"), "xml: {}", xml);
assert!(xml.contains("<a:path w=\"50\" h=\"50\">"), "xml: {}", xml);
assert!(xml.contains("<a:moveTo>"), "xml: {}", xml);
assert!(xml.contains("<a:lnTo>"), "xml: {}", xml);
assert!(xml.contains("<a:close/>"), "xml: {}", xml);
}
#[test]
fn round_trip_prst_geom_with_adjustments() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld><p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="2" name="RoundRect"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
<p:spPr>
<a:xfrm><a:off x="100000" y="100000"/><a:ext cx="500000" cy="300000"/></a:xfrm>
<a:prstGeom prst="roundRect">
<a:avLst>
<a:gd name="adj" fmla="val 16667"/>
</a:avLst>
</a:prstGeom>
</p:spPr>
<p:txBody><a:bodyPr/><a:p/></p:txBody>
</p:sp>
</p:spTree></p:cSld>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
let sp = sld
.shapes
.iter()
.find_map(|s| {
if let OxmlSlideShape::Sp(sp) = s {
Some(sp)
} else {
None
}
})
.expect("应有 Sp");
match &sp.properties.geometry {
Some(Geometry::Preset(prst, adjustments)) => {
assert_eq!(*prst, PresetGeometry::RoundRectangle);
assert_eq!(adjustments.len(), 1, "应有 1 个调整值");
let adj = &adjustments[0];
assert_eq!(adj.name, "adj");
assert_eq!(adj.raw_value, 16667);
assert!((adj.effective_value() - 0.16667).abs() < 1e-6);
}
other => panic!("期望 Preset 几何,得到 {:?}", other),
}
let mut w = crate::oxml::writer::XmlWriter::new();
sp.properties.geometry.as_ref().unwrap().write_xml(&mut w);
let out = &w.buf;
assert!(
out.contains("<a:prstGeom prst=\"roundRect\">"),
"xml: {}",
out
);
assert!(out.contains("<a:avLst>"), "xml: {}", out);
assert!(
out.contains("<a:gd name=\"adj\" fmla=\"val 16667\"/>"),
"xml: {}",
out
);
assert!(out.contains("</a:prstGeom>"), "xml: {}", out);
}
#[test]
fn round_trip_prst_geom_with_multiple_adjustments() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld><p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="3" name="MultiAdj"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
<p:spPr>
<a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm>
<a:prstGeom prst="rect">
<a:avLst>
<a:gd name="adj1" fmla="val 50000"/>
<a:gd name="adj2" fmla="val 25000"/>
</a:avLst>
</a:prstGeom>
</p:spPr>
<p:txBody><a:bodyPr/><a:p/></p:txBody>
</p:sp>
</p:spTree></p:cSld>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
let sp = sld
.shapes
.iter()
.find_map(|s| {
if let OxmlSlideShape::Sp(sp) = s {
Some(sp)
} else {
None
}
})
.expect("应有 Sp");
match &sp.properties.geometry {
Some(Geometry::Preset(_, adjustments)) => {
assert_eq!(adjustments.len(), 2, "应有 2 个调整值");
assert_eq!(adjustments[0].name, "adj1");
assert_eq!(adjustments[0].raw_value, 50000);
assert_eq!(adjustments[1].name, "adj2");
assert_eq!(adjustments[1].raw_value, 25000);
}
other => panic!("期望 Preset 几何,得到 {:?}", other),
}
let mut w = crate::oxml::writer::XmlWriter::new();
sp.properties.geometry.as_ref().unwrap().write_xml(&mut w);
let out = &w.buf;
assert!(
out.contains("<a:gd name=\"adj1\" fmla=\"val 50000\"/>"),
"xml: {}",
out
);
assert!(
out.contains("<a:gd name=\"adj2\" fmla=\"val 25000\"/>"),
"xml: {}",
out
);
}
#[test]
fn parse_prst_geom_with_empty_av_lst() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld><p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="4" name="EmptyAdj"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
<p:spPr>
<a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
</p:spPr>
<p:txBody><a:bodyPr/><a:p/></p:txBody>
</p:sp>
</p:spTree></p:cSld>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
let sp = sld
.shapes
.iter()
.find_map(|s| {
if let OxmlSlideShape::Sp(sp) = s {
Some(sp)
} else {
None
}
})
.expect("应有 Sp");
match &sp.properties.geometry {
Some(Geometry::Preset(prst, adjustments)) => {
assert_eq!(*prst, PresetGeometry::Rectangle);
assert!(adjustments.is_empty(), "空 avLst 应解析为空列表");
}
other => panic!("期望 Preset 几何,得到 {:?}", other),
}
}
#[test]
fn parse_prst_geom_without_av_lst() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld><p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<p:grpSpPr><a:xfrm/></p:grpSpPr>
<p:sp>
<p:nvSpPr><p:cNvPr id="5" name="NoAdj"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
<p:spPr>
<a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm>
<a:prstGeom prst="rect"/>
</p:spPr>
<p:txBody><a:bodyPr/><a:p/></p:txBody>
</p:sp>
</p:spTree></p:cSld>
</p:sld>"#;
let sld = parse_sld(xml).expect("parse ok");
let sp = sld
.shapes
.iter()
.find_map(|s| {
if let OxmlSlideShape::Sp(sp) = s {
Some(sp)
} else {
None
}
})
.expect("应有 Sp");
match &sp.properties.geometry {
Some(Geometry::Preset(prst, adjustments)) => {
assert_eq!(*prst, PresetGeometry::Rectangle);
assert!(adjustments.is_empty(), "无 avLst 应解析为空列表");
}
other => panic!("期望 Preset 几何,得到 {:?}", other),
}
}
#[test]
fn parse_fmla_val_formats() {
assert_eq!(parse_fmla_val("val 16667"), Some(16667));
assert_eq!(parse_fmla_val("val16667"), Some(16667));
assert_eq!(parse_fmla_val(" val 16667 "), Some(16667));
assert_eq!(parse_fmla_val("val -5000"), Some(-5000));
assert_eq!(parse_fmla_val("val abc"), None);
assert_eq!(parse_fmla_val("*/ adj1 100000 50000"), None);
assert_eq!(parse_fmla_val(""), None);
}
#[test]
fn parse_graphic_frame_with_smartart() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:graphicFrame xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram">
<p:nvGraphicFramePr>
<p:cNvPr id="300" name="SmartArt 1"/>
<p:cNvGraphicFramePr/>
<p:nvPr/>
</p:nvGraphicFramePr>
<p:xfrm>
<a:off x="457200" y="1828800"/>
<a:ext cx="8229600" cy="1143000"/>
</p:xfrm>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/diagram">
<dgm:relIds r:dm="rIdDm1" r:lo="rIdLo1" r:qs="rIdQs1" r:cs="rIdCs1"/>
</a:graphicData>
</a:graphic>
</p:graphicFrame>"#;
let frame = parse_graphic_frame(xml).expect("parse ok");
assert_eq!(frame.id, 300);
assert_eq!(frame.name, "SmartArt 1");
match &frame.graphic {
crate::oxml::shape::Graphic::SmartArt(s) => {
assert!(
s.raw_xml.contains("<a:graphicData"),
"raw_xml 应包含 graphicData 外壳,实际: {}",
s.raw_xml
);
assert!(
s.raw_xml.contains(
"uri=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\""
),
"raw_xml 应包含 diagram uri,实际: {}",
s.raw_xml
);
assert_eq!(s.dm_rid.as_deref(), Some("rIdDm1"));
assert_eq!(s.lo_rid.as_deref(), Some("rIdLo1"));
assert_eq!(s.qs_rid.as_deref(), Some("rIdQs1"));
assert_eq!(s.cs_rid.as_deref(), Some("rIdCs1"));
}
other => panic!("期望 SmartArt,得到 {:?}", other),
}
}
#[test]
fn parse_smartart_rel_ids_formats() {
let raw = r#"<a:graphicData uri=".../diagram"><dgm:relIds r:dm="rId1" r:lo="rId2" r:qs="rId3" r:cs="rId4"/></a:graphicData>"#;
let (dm, lo, qs, cs) = parse_smartart_rel_ids(raw);
assert_eq!(dm.as_deref(), Some("rId1"));
assert_eq!(lo.as_deref(), Some("rId2"));
assert_eq!(qs.as_deref(), Some("rId3"));
assert_eq!(cs.as_deref(), Some("rId4"));
let raw_partial =
r#"<a:graphicData uri=".../diagram"><dgm:relIds r:dm="rId1"/></a:graphicData>"#;
let (dm, lo, qs, cs) = parse_smartart_rel_ids(raw_partial);
assert_eq!(dm.as_deref(), Some("rId1"));
assert!(lo.is_none(), "lo 应为 None");
assert!(qs.is_none(), "qs 应为 None");
assert!(cs.is_none(), "cs 应为 None");
let raw_empty = r#"<a:graphicData uri=".../diagram"></a:graphicData>"#;
let (dm, lo, qs, cs) = parse_smartart_rel_ids(raw_empty);
assert!(dm.is_none() && lo.is_none() && qs.is_none() && cs.is_none());
}
}