use std::str::FromStr;
use crate::oxml::color::Color;
use crate::oxml::simpletypes::PresetGeometry;
use crate::units::Emu;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Transform {
pub off_x: Option<Emu>,
pub off_y: Option<Emu>,
pub ext_cx: Option<Emu>,
pub ext_cy: Option<Emu>,
pub rot: Option<i32>,
pub flip_h: bool,
pub flip_v: bool,
}
impl Transform {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
if self.is_empty() {
return;
}
let rot_s = self.rot.map(|v| v.to_string());
let off_x_s = self.off_x.map(|v| v.value().to_string());
let off_y_s = self.off_y.map(|v| v.value().to_string());
let ext_cx_s = self.ext_cx.map(|v| v.value().to_string());
let ext_cy_s = self.ext_cy.map(|v| v.value().to_string());
let mut attrs: Vec<(&str, &str)> = Vec::new();
if self.flip_h {
attrs.push(("flipH", "1"));
}
if self.flip_v {
attrs.push(("flipV", "1"));
}
if let Some(s) = &rot_s {
attrs.push(("rot", s.as_str()));
}
w.open_with("a:xfrm", &attrs);
if let (Some(xs), Some(ys)) = (off_x_s.as_ref(), off_y_s.as_ref()) {
w.empty_with("a:off", &[("x", xs.as_str()), ("y", ys.as_str())]);
}
if let (Some(xs), Some(ys)) = (ext_cx_s.as_ref(), ext_cy_s.as_ref()) {
w.empty_with("a:ext", &[("cx", xs.as_str()), ("cy", ys.as_str())]);
}
w.close("a:xfrm");
}
pub fn is_empty(&self) -> bool {
self.off_x.is_none()
&& self.off_y.is_none()
&& self.ext_cx.is_none()
&& self.ext_cy.is_none()
&& self.rot.is_none()
&& !self.flip_h
&& !self.flip_v
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum GradientType {
Linear(i32),
Path(GradientPath),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum GradientPath {
Circle,
Rect,
Shape,
}
impl GradientPath {
pub fn as_str(self) -> &'static str {
match self {
GradientPath::Circle => "circle",
GradientPath::Rect => "rect",
GradientPath::Shape => "shape",
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct GradientStop {
pub pos: u32,
pub color: Color,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GradientFill {
pub stops: Vec<GradientStop>,
pub gradient_type: GradientType,
pub flip: Option<String>,
pub rot_with_shape: Option<bool>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PatternFill {
pub prst: String,
pub fg_color: Color,
pub bg_color: Color,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum BlipFillMode {
#[default]
Stretch,
Tile {
tx: Option<i64>,
ty: Option<i64>,
sx: Option<i32>,
sy: Option<i32>,
flip: Option<String>,
algn: Option<String>,
},
None,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum Fill {
None,
Solid(Color),
Blip {
rid: String,
mode: BlipFillMode,
},
Gradient(GradientFill),
Pattern(PatternFill),
#[default]
Inherit,
}
impl BlipFillMode {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
match self {
BlipFillMode::Stretch => {
w.open("a:stretch");
w.empty("a:fillRect");
w.close("a:stretch");
}
BlipFillMode::Tile {
tx,
ty,
sx,
sy,
flip,
algn,
} => {
let tx_s = tx.map(|v| v.to_string());
let ty_s = ty.map(|v| v.to_string());
let sx_s = sx.map(|v| v.to_string());
let sy_s = sy.map(|v| v.to_string());
let mut attrs: Vec<(&str, &str)> = Vec::new();
if let Some(s) = &tx_s {
attrs.push(("tx", s));
}
if let Some(s) = &ty_s {
attrs.push(("ty", s));
}
if let Some(s) = &sx_s {
attrs.push(("sx", s));
}
if let Some(s) = &sy_s {
attrs.push(("sy", s));
}
if let Some(f) = flip {
attrs.push(("flip", f));
}
if let Some(a) = algn {
attrs.push(("algn", a));
}
w.empty_with("a:tile", &attrs);
}
BlipFillMode::None => {
}
}
}
}
impl Fill {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
match self {
Fill::None => {
w.empty("a:noFill");
}
Fill::Solid(c) => c.write_solid_fill(w),
Fill::Blip { rid, mode } => {
w.open_with(
"a:blipFill",
&[("xmlns:r", crate::oxml::ns::NS_DRAWING_RELS)],
);
w.empty_with("a:blip", &[("r:embed", rid.as_str())]);
mode.write_xml(w);
w.close("a:blipFill");
}
Fill::Gradient(g) => {
let flip_s = g.flip.as_deref();
let rws_s = g.rot_with_shape.map(|b| if b { "1" } else { "0" });
let mut attrs: Vec<(&str, &str)> = Vec::new();
if let Some(f) = flip_s {
attrs.push(("flip", f));
}
if let Some(r) = rws_s {
attrs.push(("rotWithShape", r));
}
if attrs.is_empty() {
w.open("a:gradFill");
} else {
w.open_with("a:gradFill", &attrs);
}
w.open("a:gsLst");
for stop in &g.stops {
let pos_s = stop.pos.to_string();
w.open_with("a:gs", &[("pos", pos_s.as_str())]);
stop.color.write_solid_fill(w);
w.close("a:gs");
}
w.close("a:gsLst");
match &g.gradient_type {
GradientType::Linear(ang) => {
let ang_s = ang.to_string();
w.empty_with("a:lin", &[("ang", ang_s.as_str()), ("scaled", "1")]);
}
GradientType::Path(p) => {
w.empty_with("a:path", &[("path", p.as_str())]);
}
}
w.close("a:gradFill");
}
Fill::Pattern(p) => {
w.open_with("a:pattFill", &[("prst", p.prst.as_str())]);
w.open("a:fgClr");
p.fg_color.write_solid_fill(w);
w.close("a:fgClr");
w.open("a:bgClr");
p.bg_color.write_solid_fill(w);
w.close("a:bgClr");
w.close("a:pattFill");
}
Fill::Inherit => { }
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum ArrowType {
#[default]
None,
Triangle,
Stealth,
Diamond,
Oval,
Arrow,
}
impl ArrowType {
pub fn as_str(self) -> &'static str {
match self {
ArrowType::None => "none",
ArrowType::Triangle => "triangle",
ArrowType::Stealth => "stealth",
ArrowType::Diamond => "diamond",
ArrowType::Oval => "oval",
ArrowType::Arrow => "arrow",
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum ArrowSize {
Small,
#[default]
Medium,
Large,
}
impl ArrowSize {
pub fn as_str(self) -> &'static str {
match self {
ArrowSize::Small => "sm",
ArrowSize::Medium => "med",
ArrowSize::Large => "lg",
}
}
}
#[derive(Copy, Clone, Debug, Default)]
pub struct ArrowHead {
pub arrow_type: ArrowType,
pub width: ArrowSize,
pub length: ArrowSize,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum LineJoin {
#[default]
Round,
Miter(i32),
Bevel,
}
#[derive(Clone, Debug, Default)]
pub struct Line {
pub width: Option<Emu>, pub color: Color,
pub dash: Option<Dash>,
pub cap: Option<String>,
pub compound: Option<String>,
pub no_fill: bool,
pub head_end: Option<ArrowHead>,
pub tail_end: Option<ArrowHead>,
pub join: Option<LineJoin>,
pub fill: Fill,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Dash {
Solid,
Dash,
DashDot,
Dot,
LgDash,
LgDashDot,
LgDashDotDot,
SysDash,
SysDashDot,
SysDashDotDot,
SysDot,
}
impl Dash {
pub fn as_str(self) -> &'static str {
match self {
Dash::Solid => "solid",
Dash::Dash => "dash",
Dash::DashDot => "dashDot",
Dash::Dot => "dot",
Dash::LgDash => "lgDash",
Dash::LgDashDot => "lgDashDot",
Dash::LgDashDotDot => "lgDashDotDot",
Dash::SysDash => "sysDash",
Dash::SysDashDot => "sysDashDot",
Dash::SysDashDotDot => "sysDashDotDot",
Dash::SysDot => "sysDot",
}
}
}
impl FromStr for Dash {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"solid" => Dash::Solid,
"dash" => Dash::Dash,
"dashDot" => Dash::DashDot,
"dot" => Dash::Dot,
"lgDash" => Dash::LgDash,
"lgDashDot" => Dash::LgDashDot,
"lgDashDotDot" => Dash::LgDashDotDot,
"sysDash" => Dash::SysDash,
"sysDashDot" => Dash::SysDashDot,
"sysDashDotDot" => Dash::SysDashDotDot,
"sysDot" => Dash::SysDot,
_ => return Err(()),
})
}
}
impl Line {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let w_s = self.width.map(|v| v.value().to_string());
let mut attrs: Vec<(&str, &str)> = Vec::new();
if let Some(s) = &w_s {
attrs.push(("w", s.as_str()));
}
if let Some(c) = &self.cap {
attrs.push(("cap", c.as_str()));
}
if let Some(c) = &self.compound {
attrs.push(("cmpd", c.as_str()));
}
w.open_with("a:ln", &attrs);
if self.no_fill {
w.empty("a:noFill");
} else {
match &self.fill {
Fill::Gradient(_) | Fill::Pattern(_) => self.fill.write_xml(w),
_ => self.color.write_solid_fill(w),
}
}
if let Some(d) = &self.dash {
w.open("a:prstDash");
w.empty_with("a:prst", &[("val", d.as_str())]);
w.close("a:prstDash");
}
if let Some(h) = &self.head_end {
w.empty_with(
"a:headEnd",
&[
("type", h.arrow_type.as_str()),
("w", h.width.as_str()),
("len", h.length.as_str()),
],
);
}
if let Some(t) = &self.tail_end {
w.empty_with(
"a:tailEnd",
&[
("type", t.arrow_type.as_str()),
("w", t.width.as_str()),
("len", t.length.as_str()),
],
);
}
if let Some(j) = &self.join {
match j {
LineJoin::Round => {
w.empty("a:round");
}
LineJoin::Miter(lim) => {
let lim_s = lim.to_string();
w.empty_with("a:miter", &[("lim", lim_s.as_str())]);
}
LineJoin::Bevel => {
w.empty("a:bevel");
}
}
}
w.close("a:ln");
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ShadowEffect {
pub dir: i32,
pub dist: i64,
pub blur_rad: i64,
pub color: Color,
pub rot_with_shape: Option<bool>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct GlowEffect {
pub rad: i64,
pub color: Color,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SoftEdgeEffect {
pub rad: i64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ReflectionEffect {
pub blur_rad: Option<i64>,
pub st_a: Option<i32>,
pub st_pos: Option<i32>,
pub end_a: Option<i32>,
pub end_pos: Option<i32>,
pub dist: Option<i64>,
pub dir: Option<i32>,
pub rot_with_shape: Option<bool>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EffectList {
pub outer_shadow: Option<ShadowEffect>,
pub inner_shadow: Option<ShadowEffect>,
pub glow: Option<GlowEffect>,
pub soft_edge: Option<SoftEdgeEffect>,
pub reflection: Option<ReflectionEffect>,
}
impl EffectList {
pub fn is_empty(&self) -> bool {
self.outer_shadow.is_none()
&& self.inner_shadow.is_none()
&& self.glow.is_none()
&& self.soft_edge.is_none()
&& self.reflection.is_none()
}
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
if self.is_empty() {
return;
}
w.open("a:effectLst");
if let Some(g) = &self.glow {
let rad_s = g.rad.to_string();
w.open_with("a:glow", &[("rad", rad_s.as_str())]);
g.color.write_solid_fill(w);
w.close("a:glow");
}
if let Some(s) = &self.inner_shadow {
let dir_s = s.dir.to_string();
let dist_s = s.dist.to_string();
let blur_s = s.blur_rad.to_string();
w.open_with(
"a:innerShdw",
&[
("blurRad", blur_s.as_str()),
("dist", dist_s.as_str()),
("dir", dir_s.as_str()),
],
);
s.color.write_solid_fill(w);
w.close("a:innerShdw");
}
if let Some(s) = &self.outer_shadow {
let dir_s = s.dir.to_string();
let dist_s = s.dist.to_string();
let blur_s = s.blur_rad.to_string();
let mut attrs: Vec<(&str, &str)> = vec![
("blurRad", blur_s.as_str()),
("dist", dist_s.as_str()),
("dir", dir_s.as_str()),
];
let rot_s;
if let Some(r) = s.rot_with_shape {
rot_s = if r { "1".to_string() } else { "0".to_string() };
attrs.push(("rotWithShape", rot_s.as_str()));
}
w.open_with("a:outerShdw", &attrs);
s.color.write_solid_fill(w);
w.close("a:outerShdw");
}
if let Some(r) = &self.reflection {
let mut attrs: Vec<(String, String)> = Vec::new();
if let Some(v) = r.blur_rad {
attrs.push(("blurRad".to_string(), v.to_string()));
}
if let Some(v) = r.st_a {
attrs.push(("stA".to_string(), v.to_string()));
}
if let Some(v) = r.st_pos {
attrs.push(("stPos".to_string(), v.to_string()));
}
if let Some(v) = r.end_a {
attrs.push(("endA".to_string(), v.to_string()));
}
if let Some(v) = r.end_pos {
attrs.push(("endPos".to_string(), v.to_string()));
}
if let Some(v) = r.dist {
attrs.push(("dist".to_string(), v.to_string()));
}
if let Some(v) = r.dir {
attrs.push(("dir".to_string(), v.to_string()));
}
if let Some(v) = r.rot_with_shape {
attrs.push((
"rotWithShape".to_string(),
if v { "1".to_string() } else { "0".to_string() },
));
}
let refs: Vec<(&str, &str)> = attrs
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
w.empty_with("a:reflection", &refs);
}
if let Some(s) = &self.soft_edge {
let rad_s = s.rad.to_string();
w.empty_with("a:softEdge", &[("rad", rad_s.as_str())]);
}
w.close("a:effectLst");
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct GeomRect {
pub l: String,
pub t: String,
pub r: String,
pub b: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PathSegment {
MoveTo { x: i64, y: i64 },
LineTo { x: i64, y: i64 },
CubicBezTo {
x1: i64,
y1: i64,
x2: i64,
y2: i64,
x3: i64,
y3: i64,
},
QuadBezTo { x1: i64, y1: i64, x2: i64, y2: i64 },
ArcTo {
w_r: i64,
h_r: i64,
st_ang: i32,
sw_ang: i32,
},
Close,
}
impl PathSegment {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
match self {
PathSegment::MoveTo { x, y } => {
let xs = x.to_string();
let ys = y.to_string();
w.open("a:moveTo");
w.empty_with("a:pt", &[("x", xs.as_str()), ("y", ys.as_str())]);
w.close("a:moveTo");
}
PathSegment::LineTo { x, y } => {
let xs = x.to_string();
let ys = y.to_string();
w.open("a:lnTo");
w.empty_with("a:pt", &[("x", xs.as_str()), ("y", ys.as_str())]);
w.close("a:lnTo");
}
PathSegment::CubicBezTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
let x1s = x1.to_string();
let y1s = y1.to_string();
let x2s = x2.to_string();
let y2s = y2.to_string();
let x3s = x3.to_string();
let y3s = y3.to_string();
w.open("a:cubicBezTo");
w.empty_with("a:pt", &[("x", x1s.as_str()), ("y", y1s.as_str())]);
w.empty_with("a:pt", &[("x", x2s.as_str()), ("y", y2s.as_str())]);
w.empty_with("a:pt", &[("x", x3s.as_str()), ("y", y3s.as_str())]);
w.close("a:cubicBezTo");
}
PathSegment::QuadBezTo { x1, y1, x2, y2 } => {
let x1s = x1.to_string();
let y1s = y1.to_string();
let x2s = x2.to_string();
let y2s = y2.to_string();
w.open("a:quadBezTo");
w.empty_with("a:pt", &[("x", x1s.as_str()), ("y", y1s.as_str())]);
w.empty_with("a:pt", &[("x", x2s.as_str()), ("y", y2s.as_str())]);
w.close("a:quadBezTo");
}
PathSegment::ArcTo {
w_r,
h_r,
st_ang,
sw_ang,
} => {
let wr_s = w_r.to_string();
let hr_s = h_r.to_string();
let st_s = st_ang.to_string();
let sw_s = sw_ang.to_string();
w.empty_with(
"a:arcTo",
&[
("wR", wr_s.as_str()),
("hR", hr_s.as_str()),
("stAng", st_s.as_str()),
("swAng", sw_s.as_str()),
],
);
}
PathSegment::Close => {
w.empty("a:close");
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Path {
pub width: i64,
pub height: i64,
pub fill: Option<String>,
pub stroke: Option<String>,
pub segments: Vec<PathSegment>,
}
impl Path {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let w_s = self.width.to_string();
let h_s = self.height.to_string();
let mut attrs: Vec<(&str, &str)> = Vec::new();
attrs.push(("w", w_s.as_str()));
attrs.push(("h", h_s.as_str()));
if let Some(f) = &self.fill {
attrs.push(("fill", f.as_str()));
}
if let Some(s) = &self.stroke {
attrs.push(("stroke", s.as_str()));
}
w.open_with("a:path", &attrs);
for seg in &self.segments {
seg.write_xml(w);
}
w.close("a:path");
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CustomGeometry {
pub fill: Option<String>,
pub stroke: Option<String>,
pub rect: Option<GeomRect>,
pub path_list: Vec<Path>,
}
impl CustomGeometry {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("a:custGeom");
w.empty("a:avLst");
if let Some(f) = &self.fill {
w.leaf("a:fill", f.as_str());
}
if let Some(s) = &self.stroke {
w.leaf("a:stroke", s.as_str());
}
if let Some(r) = &self.rect {
w.empty_with(
"a:rect",
&[
("l", r.l.as_str()),
("t", r.t.as_str()),
("r", r.r.as_str()),
("b", r.b.as_str()),
],
);
}
w.open("a:pathLst");
for p in &self.path_list {
p.write_xml(w);
}
w.close("a:pathLst");
w.close("a:custGeom");
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AdjustmentValue {
pub name: String,
pub raw_value: i64,
}
impl AdjustmentValue {
pub fn new(name: impl Into<String>, raw_value: i64) -> Self {
Self {
name: name.into(),
raw_value,
}
}
pub fn from_normalized(name: impl Into<String>, value: f64) -> Self {
Self {
name: name.into(),
raw_value: (value * 100000.0).round() as i64,
}
}
pub fn effective_value(&self) -> f64 {
self.raw_value as f64 / 100000.0
}
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let val_s = self.raw_value.to_string();
w.empty_with(
"a:gd",
&[
("name", self.name.as_str()),
("fmla", &format!("val {}", val_s)),
],
);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Geometry {
Preset(PresetGeometry, Vec<AdjustmentValue>),
Custom(CustomGeometry),
}
impl Default for Geometry {
fn default() -> Self {
Geometry::Preset(PresetGeometry::Rectangle, Vec::new())
}
}
impl Geometry {
pub fn preset(prst: PresetGeometry) -> Self {
Geometry::Preset(prst, Vec::new())
}
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
match self {
Geometry::Preset(p, adjustments) => {
w.open_with("a:prstGeom", &[("prst", p.as_str())]);
if adjustments.is_empty() {
w.empty("a:avLst");
} else {
w.open("a:avLst");
for adj in adjustments {
adj.write_xml(w);
}
w.close("a:avLst");
}
w.close("a:prstGeom");
}
Geometry::Custom(c) => {
c.write_xml(w);
}
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ShapeProperties {
pub xfrm: Transform,
pub geometry: Option<Geometry>,
pub fill: Fill,
pub line: Option<Line>,
pub effects: Option<EffectList>,
pub scene3d: Option<Scene3d>,
pub sp3d: Option<Sp3d>,
pub rot_deg: Option<f64>,
}
impl ShapeProperties {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
w.open(tag);
if !self.xfrm.is_empty() {
self.xfrm.write_xml(w);
}
let geom = self.geometry.clone().unwrap_or_default();
geom.write_xml(w);
self.fill.write_xml(w);
if let Some(ln) = &self.line {
ln.write_xml(w);
}
if let Some(effects) = &self.effects {
effects.write_xml(w);
}
if let Some(scene) = &self.scene3d {
scene.write_xml(w);
}
if let Some(sp3d) = &self.sp3d {
sp3d.write_xml(w);
}
w.close(tag);
}
pub fn write_xfrm_only(&self, w: &mut super::writer::XmlWriter, tag: &str) {
if self.xfrm.is_empty() {
w.empty(tag);
return;
}
w.open(tag);
self.xfrm.write_xml(w);
w.close(tag);
}
pub fn effects(&self) -> Option<&EffectList> {
self.effects.as_ref()
}
pub fn effects_mut(&mut self) -> &mut EffectList {
self.effects.get_or_insert_with(EffectList::default)
}
pub fn set_outer_shadow(&mut self, shadow: ShadowEffect) {
self.effects_mut().outer_shadow = Some(shadow);
}
pub fn set_inner_shadow(&mut self, shadow: ShadowEffect) {
self.effects_mut().inner_shadow = Some(shadow);
}
pub fn set_glow(&mut self, glow: GlowEffect) {
self.effects_mut().glow = Some(glow);
}
pub fn set_soft_edge(&mut self, rad: i64) {
self.effects_mut().soft_edge = Some(SoftEdgeEffect { rad });
}
pub fn set_reflection(&mut self, reflection: ReflectionEffect) {
self.effects_mut().reflection = Some(reflection);
}
pub fn clear_outer_shadow(&mut self) {
if let Some(e) = self.effects.as_mut() {
e.outer_shadow = None;
}
}
pub fn clear_inner_shadow(&mut self) {
if let Some(e) = self.effects.as_mut() {
e.inner_shadow = None;
}
}
pub fn clear_glow(&mut self) {
if let Some(e) = self.effects.as_mut() {
e.glow = None;
}
}
pub fn clear_soft_edge(&mut self) {
if let Some(e) = self.effects.as_mut() {
e.soft_edge = None;
}
}
pub fn clear_reflection(&mut self) {
if let Some(e) = self.effects.as_mut() {
e.reflection = None;
}
}
pub fn clear_effects(&mut self) {
self.effects = None;
}
}
use crate::oxml::color::ColorFormat;
#[derive(Debug)]
pub struct FillFormat<'a> {
fill: &'a mut Fill,
}
impl<'a> FillFormat<'a> {
pub fn new(fill: &'a mut Fill) -> Self {
FillFormat { fill }
}
pub fn fill(&self) -> &Fill {
self.fill
}
pub fn fill_mut(&mut self) -> &mut Fill {
self.fill
}
pub fn fill_type(&self) -> super::simpletypes::MsoFillType {
match self.fill {
Fill::None => super::simpletypes::MsoFillType::Background,
Fill::Solid(_) => super::simpletypes::MsoFillType::Solid,
Fill::Blip { .. } => super::simpletypes::MsoFillType::Picture,
Fill::Gradient(_) => super::simpletypes::MsoFillType::Gradient,
Fill::Pattern(_) => super::simpletypes::MsoFillType::Pattern,
Fill::Inherit => super::simpletypes::MsoFillType::Inherit,
}
}
pub fn solid(&mut self) -> ColorFormat<'_> {
if !matches!(self.fill, Fill::Solid(_)) {
*self.fill = Fill::Solid(Color::None);
}
match self.fill {
Fill::Solid(c) => ColorFormat::new(c),
_ => unreachable!("just set to Solid"),
}
}
pub fn set_none(&mut self) {
*self.fill = Fill::None;
}
pub fn set_picture(&mut self, rid: impl Into<String>, mode: BlipFillMode) {
*self.fill = Fill::Blip {
rid: rid.into(),
mode,
};
}
pub fn clear(&mut self) {
*self.fill = Fill::Inherit;
}
pub fn set_solid_rgb(&mut self, c: impl Into<crate::units::RGBColor>) {
*self.fill = Fill::Solid(Color::RGB(c.into()));
}
pub fn set_solid_theme(&mut self, t: super::simpletypes::MsoThemeColorIndex) {
if let Some(s) = t.as_str() {
if let Ok(sc) = s.parse::<crate::oxml::color::SchemeColor>() {
*self.fill = Fill::Solid(Color::Scheme(sc));
}
}
}
pub fn gradient(&mut self) -> &mut GradientFill {
if !matches!(self.fill, Fill::Gradient(_)) {
*self.fill = Fill::Gradient(GradientFill {
stops: Vec::new(),
gradient_type: GradientType::Linear(0),
flip: None,
rot_with_shape: None,
});
}
match &mut self.fill {
Fill::Gradient(g) => g,
_ => unreachable!("just set to Gradient"),
}
}
pub fn set_gradient_linear(&mut self, stops: Vec<GradientStop>, angle: i32) {
*self.fill = Fill::Gradient(GradientFill {
stops,
gradient_type: GradientType::Linear(angle),
flip: None,
rot_with_shape: None,
});
}
pub fn set_gradient_path(&mut self, stops: Vec<GradientStop>, path: GradientPath) {
*self.fill = Fill::Gradient(GradientFill {
stops,
gradient_type: GradientType::Path(path),
flip: None,
rot_with_shape: None,
});
}
pub fn pattern(&mut self) -> &mut PatternFill {
if !matches!(self.fill, Fill::Pattern(_)) {
*self.fill = Fill::Pattern(PatternFill {
prst: String::new(),
fg_color: Color::None,
bg_color: Color::None,
});
}
match &mut self.fill {
Fill::Pattern(p) => p,
_ => unreachable!("just set to Pattern"),
}
}
pub fn set_pattern(&mut self, prst: impl Into<String>, fg: Color, bg: Color) {
*self.fill = Fill::Pattern(PatternFill {
prst: prst.into(),
fg_color: fg,
bg_color: bg,
});
}
}
impl<'a> From<&'a mut Fill> for FillFormat<'a> {
fn from(f: &'a mut Fill) -> Self {
FillFormat::new(f)
}
}
#[derive(Debug)]
pub struct LineFormat<'a> {
line: &'a mut Line,
}
impl<'a> LineFormat<'a> {
pub fn new(line: &'a mut Line) -> Self {
LineFormat { line }
}
pub fn line(&self) -> &Line {
self.line
}
pub fn line_mut(&mut self) -> &mut Line {
self.line
}
pub fn color(&mut self) -> ColorFormat<'_> {
ColorFormat::new(&mut self.line.color)
}
pub fn width(&self) -> Option<crate::units::Emu> {
self.line.width
}
pub fn set_width(&mut self, w: crate::units::Emu) {
self.line.width = Some(w);
}
pub fn dash_style(&self) -> Option<Dash> {
self.line.dash
}
pub fn set_dash_style(&mut self, s: super::simpletypes::MsoLineDashStyle) {
self.line.dash = Some(s.into());
}
pub fn set_no_fill(&mut self) {
self.line.no_fill = true;
self.line.color = crate::oxml::color::Color::None;
}
pub fn set_width_pt(&mut self, pt: crate::units::Pt) {
self.line.width = Some(crate::units::Emu((pt.value() * 12_700.0) as i64));
}
}
impl<'a> From<&'a mut Line> for LineFormat<'a> {
fn from(l: &'a mut Line) -> Self {
LineFormat::new(l)
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Rotation3d {
pub lat: i32,
pub lon: i32,
pub rev: i32,
}
impl Rotation3d {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let lat = self.lat.to_string();
let lon = self.lon.to_string();
let rev = self.rev.to_string();
w.empty_with(
"a:rot",
&[
("lat", lat.as_str()),
("lon", lon.as_str()),
("rev", rev.as_str()),
],
);
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum CameraPreset {
#[default]
OrthographicFront,
IsometricOffAxis1,
IsometricOffAxis2,
IsometricOffAxis3,
IsometricOffAxis4,
IsometricLeftDown,
IsometricLeftUp,
IsometricRightDown,
IsometricRightUp,
PerspectiveFront,
PerspectiveLeft,
PerspectiveRight,
Other(String),
}
impl CameraPreset {
pub fn as_str(&self) -> &str {
match self {
CameraPreset::OrthographicFront => "orthographicFront",
CameraPreset::IsometricOffAxis1 => "isometricOffAxis1",
CameraPreset::IsometricOffAxis2 => "isometricOffAxis2",
CameraPreset::IsometricOffAxis3 => "isometricOffAxis3",
CameraPreset::IsometricOffAxis4 => "isometricOffAxis4",
CameraPreset::IsometricLeftDown => "isometricLeftDown",
CameraPreset::IsometricLeftUp => "isometricLeftUp",
CameraPreset::IsometricRightDown => "isometricRightDown",
CameraPreset::IsometricRightUp => "isometricRightUp",
CameraPreset::PerspectiveFront => "perspectiveFront",
CameraPreset::PerspectiveLeft => "perspectiveLeft",
CameraPreset::PerspectiveRight => "perspectiveRight",
CameraPreset::Other(s) => s.as_str(),
}
}
pub fn parse(s: &str) -> Self {
match s {
"orthographicFront" => CameraPreset::OrthographicFront,
"isometricOffAxis1" => CameraPreset::IsometricOffAxis1,
"isometricOffAxis2" => CameraPreset::IsometricOffAxis2,
"isometricOffAxis3" => CameraPreset::IsometricOffAxis3,
"isometricOffAxis4" => CameraPreset::IsometricOffAxis4,
"isometricLeftDown" => CameraPreset::IsometricLeftDown,
"isometricLeftUp" => CameraPreset::IsometricLeftUp,
"isometricRightDown" => CameraPreset::IsometricRightDown,
"isometricRightUp" => CameraPreset::IsometricRightUp,
"perspectiveFront" => CameraPreset::PerspectiveFront,
"perspectiveLeft" => CameraPreset::PerspectiveLeft,
"perspectiveRight" => CameraPreset::PerspectiveRight,
other => CameraPreset::Other(other.to_string()),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Camera {
pub preset: CameraPreset,
pub fov: i32,
pub zoom: i32,
pub rotation: Option<Rotation3d>,
}
impl Camera {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let prst = self.preset.as_str();
let mut attrs: Vec<(&str, &str)> = vec![("prst", prst)];
let fov_s;
if self.fov != 0 {
fov_s = self.fov.to_string();
attrs.push(("fov", fov_s.as_str()));
}
let zoom_s;
if self.zoom != 0 && self.zoom != 100000 {
zoom_s = self.zoom.to_string();
attrs.push(("zoom", zoom_s.as_str()));
}
if let Some(rot) = &self.rotation {
w.open_with("a:camera", &attrs);
rot.write_xml(w);
w.close("a:camera");
} else {
w.empty_with("a:camera", &attrs);
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum LightRigType {
#[default]
Balanced,
Bright,
Chilly,
Contrasting,
Flat,
Harsh,
Morning,
Soft,
Sunrise,
Sunset,
ThreePt,
TwoPt,
Other(String),
}
impl LightRigType {
pub fn as_str(&self) -> &str {
match self {
LightRigType::Balanced => "balanced",
LightRigType::Bright => "bright",
LightRigType::Chilly => "chilly",
LightRigType::Contrasting => "contrasting",
LightRigType::Flat => "flat",
LightRigType::Harsh => "harsh",
LightRigType::Morning => "morning",
LightRigType::Soft => "soft",
LightRigType::Sunrise => "sunrise",
LightRigType::Sunset => "sunset",
LightRigType::ThreePt => "threePt",
LightRigType::TwoPt => "twoPt",
LightRigType::Other(s) => s.as_str(),
}
}
pub fn parse(s: &str) -> Self {
match s {
"balanced" => LightRigType::Balanced,
"bright" => LightRigType::Bright,
"chilly" => LightRigType::Chilly,
"contrasting" => LightRigType::Contrasting,
"flat" => LightRigType::Flat,
"harsh" => LightRigType::Harsh,
"morning" => LightRigType::Morning,
"soft" => LightRigType::Soft,
"sunrise" => LightRigType::Sunrise,
"sunset" => LightRigType::Sunset,
"threePt" => LightRigType::ThreePt,
"twoPt" => LightRigType::TwoPt,
other => LightRigType::Other(other.to_string()),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum LightRigDirection {
TopLeft,
#[default]
Top,
TopRight,
Left,
Right,
BottomLeft,
Bottom,
BottomRight,
}
impl LightRigDirection {
pub fn as_str(&self) -> &str {
match self {
LightRigDirection::TopLeft => "tl",
LightRigDirection::Top => "t",
LightRigDirection::TopRight => "tr",
LightRigDirection::Left => "l",
LightRigDirection::Right => "r",
LightRigDirection::BottomLeft => "bl",
LightRigDirection::Bottom => "b",
LightRigDirection::BottomRight => "br",
}
}
pub fn parse(s: &str) -> Self {
match s {
"tl" => LightRigDirection::TopLeft,
"t" => LightRigDirection::Top,
"tr" => LightRigDirection::TopRight,
"l" => LightRigDirection::Left,
"r" => LightRigDirection::Right,
"bl" => LightRigDirection::BottomLeft,
"b" => LightRigDirection::Bottom,
"br" => LightRigDirection::BottomRight,
_ => LightRigDirection::Top,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LightRig {
pub rig: LightRigType,
pub dir: LightRigDirection,
pub rotation: Option<Rotation3d>,
}
impl LightRig {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let rig = self.rig.as_str();
let dir = self.dir.as_str();
if let Some(rot) = &self.rotation {
w.open_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
rot.write_xml(w);
w.close("a:lightRig");
} else {
w.empty_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Scene3d {
pub camera: Camera,
pub light_rig: LightRig,
pub backdrop: Option<Backdrop>,
}
impl Scene3d {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("a:scene3d");
self.camera.write_xml(w);
self.light_rig.write_xml(w);
if let Some(b) = &self.backdrop {
b.write_xml(w);
}
w.close("a:scene3d");
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Point3d {
pub x: i32,
pub y: i32,
pub z: i32,
}
impl Point3d {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let xs = self.x.to_string();
let ys = self.y.to_string();
let zs = self.z.to_string();
w.empty_with(
"a:anchor",
&[("x", xs.as_str()), ("y", ys.as_str()), ("z", zs.as_str())],
);
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Backdrop {
pub anchor: Option<Point3d>,
pub floor: bool,
pub wall: bool,
pub left: bool,
pub right: bool,
pub top: bool,
pub bottom: bool,
}
impl Backdrop {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("a:backdrop");
if let Some(a) = &self.anchor {
a.write_xml(w);
}
if self.floor {
w.empty("a:floor");
}
if self.wall {
w.empty("a:wall");
}
if self.left {
w.empty("a:l");
}
if self.right {
w.empty("a:r");
}
if self.top {
w.empty("a:t");
}
if self.bottom {
w.empty("a:b");
}
w.close("a:backdrop");
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Bevel {
pub w: i32,
pub h: i32,
}
impl Bevel {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
let w_s = self.w.to_string();
let h_s = self.h.to_string();
w.empty_with(tag, &[("w", w_s.as_str()), ("h", h_s.as_str())]);
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum MaterialPreset {
#[default]
WarmMatte,
Clear,
DarkEdge,
Flat,
LegacyMatte,
LegacyMetallic,
LegacyPlastic,
LegacyWireframe,
Matte,
Metallic,
Plastic,
Powder,
SoftEdge,
SoftMetal,
Other(String),
}
impl MaterialPreset {
pub fn as_str(&self) -> &str {
match self {
MaterialPreset::WarmMatte => "warmMatte",
MaterialPreset::Clear => "clear",
MaterialPreset::DarkEdge => "dkEdge",
MaterialPreset::Flat => "flat",
MaterialPreset::LegacyMatte => "legacyMatte",
MaterialPreset::LegacyMetallic => "legacyMetallic",
MaterialPreset::LegacyPlastic => "legacyPlastic",
MaterialPreset::LegacyWireframe => "legacyWireframe",
MaterialPreset::Matte => "matte",
MaterialPreset::Metallic => "metallic",
MaterialPreset::Plastic => "plastic",
MaterialPreset::Powder => "powder",
MaterialPreset::SoftEdge => "softEdge",
MaterialPreset::SoftMetal => "softmetal",
MaterialPreset::Other(s) => s.as_str(),
}
}
pub fn parse(s: &str) -> Self {
match s {
"warmMatte" => MaterialPreset::WarmMatte,
"clear" => MaterialPreset::Clear,
"dkEdge" => MaterialPreset::DarkEdge,
"flat" => MaterialPreset::Flat,
"legacyMatte" => MaterialPreset::LegacyMatte,
"legacyMetallic" => MaterialPreset::LegacyMetallic,
"legacyPlastic" => MaterialPreset::LegacyPlastic,
"legacyWireframe" => MaterialPreset::LegacyWireframe,
"matte" => MaterialPreset::Matte,
"metallic" => MaterialPreset::Metallic,
"plastic" => MaterialPreset::Plastic,
"powder" => MaterialPreset::Powder,
"softEdge" => MaterialPreset::SoftEdge,
"softmetal" => MaterialPreset::SoftMetal,
other => MaterialPreset::Other(other.to_string()),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Sp3d {
pub extrusion_h: i32,
pub contour_w: i32,
pub prst_material: MaterialPreset,
pub bevel_top: Option<Bevel>,
pub bevel_bottom: Option<Bevel>,
pub extrusion_color: Option<Color>,
pub contour_color: Option<Color>,
}
impl Sp3d {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let mut attrs: Vec<(&str, &str)> = Vec::new();
let eh_s;
if self.extrusion_h != 0 {
eh_s = self.extrusion_h.to_string();
attrs.push(("extrusionH", eh_s.as_str()));
}
let cw_s;
if self.contour_w != 0 {
cw_s = self.contour_w.to_string();
attrs.push(("contourW", cw_s.as_str()));
}
let pm_s;
if !matches!(self.prst_material, MaterialPreset::WarmMatte) {
pm_s = self.prst_material.as_str().to_string();
attrs.push(("prstMaterial", pm_s.as_str()));
}
let has_children = self.bevel_top.is_some()
|| self.bevel_bottom.is_some()
|| self.extrusion_color.is_some()
|| self.contour_color.is_some();
if !has_children && attrs.is_empty() {
w.empty("a:sp3d");
return;
}
if !has_children {
w.empty_with("a:sp3d", &attrs);
return;
}
w.open_with("a:sp3d", &attrs);
if let Some(b) = &self.bevel_top {
b.write_xml(w, "a:bevelT");
}
if let Some(b) = &self.bevel_bottom {
b.write_xml(w, "a:bevelB");
}
if let Some(c) = &self.extrusion_color {
w.open("a:extrusionClr");
c.write_solid_fill(w);
w.close("a:extrusionClr");
}
if let Some(c) = &self.contour_color {
w.open("a:contourClr");
c.write_solid_fill(w);
w.close("a:contourClr");
}
w.close("a:sp3d");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oxml::color::Color;
use crate::oxml::writer::XmlWriter;
use crate::units::RGBColor;
#[test]
fn adjustment_value_new() {
let av = AdjustmentValue::new("adj", 16667);
assert_eq!(av.name, "adj");
assert_eq!(av.raw_value, 16667);
}
#[test]
fn adjustment_value_from_normalized() {
let av = AdjustmentValue::from_normalized("adj", 0.5);
assert_eq!(av.raw_value, 50000);
let av2 = AdjustmentValue::from_normalized("adj1", 0.25);
assert_eq!(av2.raw_value, 25000);
}
#[test]
fn adjustment_value_effective_value() {
let av = AdjustmentValue::new("adj", 16667);
assert!((av.effective_value() - 0.16667).abs() < 0.0001);
}
#[test]
fn adjustment_value_write_xml() {
let av = AdjustmentValue::new("adj", 16667);
let mut w = XmlWriter::new();
av.write_xml(&mut w);
let xml = &w.buf;
assert!(xml.contains("<a:gd"), "xml: {}", xml);
assert!(xml.contains(r#"name="adj""#), "xml: {}", xml);
assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
}
#[test]
fn geometry_preset_with_adjustments_write_xml() {
let geom = Geometry::Preset(
PresetGeometry::RoundRectangle,
vec![AdjustmentValue::new("adj", 16667)],
);
let mut w = XmlWriter::new();
geom.write_xml(&mut w);
let xml = &w.buf;
assert!(xml.contains(r#"prst="roundRect""#), "xml: {}", xml);
assert!(xml.contains("<a:avLst>"), "xml: {}", xml);
assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
assert!(xml.contains("</a:avLst>"), "xml: {}", xml);
}
#[test]
fn geometry_preset_no_adjustments_write_xml() {
let geom = Geometry::preset(PresetGeometry::Rectangle);
let mut w = XmlWriter::new();
geom.write_xml(&mut w);
assert!(w.buf.contains("<a:avLst/>"), "xml: {}", w.buf);
}
#[test]
fn geometry_preset_helper() {
let geom = Geometry::preset(PresetGeometry::Ellipse);
match geom {
Geometry::Preset(p, adj) => {
assert_eq!(p, PresetGeometry::Ellipse);
assert!(adj.is_empty());
}
_ => panic!("应为 Preset 变体"),
}
}
#[test]
fn fill_format_gradient_switches_mode() {
let mut fill = Fill::Inherit;
let mut fmt = FillFormat::new(&mut fill);
let g = fmt.gradient();
g.stops.push(GradientStop {
pos: 0,
color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
});
g.stops.push(GradientStop {
pos: 100_000,
color: Color::RGB(RGBColor(0x00, 0x00, 0xFF)),
});
g.gradient_type = GradientType::Linear(2_700_000);
assert!(matches!(fill, Fill::Gradient(_)));
if let Fill::Gradient(g) = &fill {
assert_eq!(g.stops.len(), 2);
assert_eq!(g.stops[0].pos, 0);
assert_eq!(g.stops[1].pos, 100_000);
assert!(matches!(g.gradient_type, GradientType::Linear(2_700_000)));
}
}
#[test]
fn fill_format_set_gradient_linear() {
let mut fill = Fill::Inherit;
let mut fmt = FillFormat::new(&mut fill);
let stops = vec![
GradientStop {
pos: 0,
color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
},
GradientStop {
pos: 100_000,
color: Color::RGB(RGBColor(0x00, 0xFF, 0x00)),
},
];
fmt.set_gradient_linear(stops, 5_400_000);
assert!(matches!(fill, Fill::Gradient(_)));
if let Fill::Gradient(g) = &fill {
assert_eq!(g.stops.len(), 2);
assert!(matches!(g.gradient_type, GradientType::Linear(5_400_000)));
}
}
#[test]
fn fill_format_set_gradient_path() {
let mut fill = Fill::Inherit;
let mut fmt = FillFormat::new(&mut fill);
let stops = vec![GradientStop {
pos: 50_000,
color: Color::RGB(RGBColor(0x80, 0x80, 0x80)),
}];
fmt.set_gradient_path(stops, GradientPath::Circle);
if let Fill::Gradient(g) = &fill {
assert!(matches!(
g.gradient_type,
GradientType::Path(GradientPath::Circle)
));
}
}
#[test]
fn fill_format_pattern_switches_mode() {
let mut fill = Fill::Inherit;
let mut fmt = FillFormat::new(&mut fill);
let p = fmt.pattern();
p.prst = "horz".to_string();
p.fg_color = Color::RGB(RGBColor(0xFF, 0x00, 0x00));
p.bg_color = Color::RGB(RGBColor(0xFF, 0xFF, 0xFF));
assert!(matches!(fill, Fill::Pattern(_)));
if let Fill::Pattern(p) = &fill {
assert_eq!(p.prst, "horz");
}
}
#[test]
fn fill_format_set_pattern() {
let mut fill = Fill::Inherit;
let mut fmt = FillFormat::new(&mut fill);
fmt.set_pattern(
"cross",
Color::RGB(RGBColor(0x00, 0x00, 0x00)),
Color::RGB(RGBColor(0xFF, 0xFF, 0xFF)),
);
if let Fill::Pattern(p) = &fill {
assert_eq!(p.prst, "cross");
assert!(matches!(p.fg_color, Color::RGB(_)));
assert!(matches!(p.bg_color, Color::RGB(_)));
} else {
panic!("应为 Pattern 变体");
}
}
#[test]
fn fill_format_fill_type_for_gradient_and_pattern() {
let mut fill = Fill::Gradient(GradientFill {
stops: vec![],
gradient_type: GradientType::Linear(0),
flip: None,
rot_with_shape: None,
});
let fmt = FillFormat::new(&mut fill);
assert_eq!(
fmt.fill_type(),
crate::oxml::simpletypes::MsoFillType::Gradient
);
let mut fill = Fill::Pattern(PatternFill::default());
let fmt = FillFormat::new(&mut fill);
assert_eq!(
fmt.fill_type(),
crate::oxml::simpletypes::MsoFillType::Pattern
);
}
#[test]
fn effect_list_outer_shadow_serialization() {
let mut effects = EffectList::default();
assert!(effects.is_empty());
effects.outer_shadow = Some(ShadowEffect {
dir: 2_700_000,
dist: 38100,
blur_rad: 40000,
color: Color::RGB(RGBColor(0x00, 0x00, 0x00)),
rot_with_shape: Some(false),
});
assert!(!effects.is_empty());
let mut w = XmlWriter::new();
effects.write_xml(&mut w);
let buf = &w.buf;
assert!(buf.contains("<a:effectLst>"));
assert!(buf.contains("<a:outerShdw"));
assert!(buf.contains("dir=\"2700000\""));
assert!(buf.contains("dist=\"38100\""));
assert!(buf.contains("blurRad=\"40000\""));
assert!(buf.contains("rotWithShape=\"0\""));
assert!(buf.contains("<a:srgbClr val=\"000000\""));
assert!(buf.contains("</a:effectLst>"));
}
#[test]
fn effect_list_glow_and_soft_edge() {
let effects = EffectList {
glow: Some(GlowEffect {
rad: 50000,
color: Color::RGB(RGBColor(0xFF, 0x00, 0xFF)),
}),
soft_edge: Some(SoftEdgeEffect { rad: 25000 }),
..Default::default()
};
let mut w = XmlWriter::new();
effects.write_xml(&mut w);
let buf = &w.buf;
assert!(buf.contains("<a:glow rad=\"50000\">"));
assert!(buf.contains("<a:srgbClr val=\"FF00FF\""));
assert!(buf.contains("<a:softEdge rad=\"25000\"/>"));
}
#[test]
fn effect_list_reflection_serialization() {
let effects = EffectList {
reflection: Some(ReflectionEffect {
blur_rad: Some(50000),
st_a: Some(52000),
st_pos: Some(0),
end_a: Some(30000),
end_pos: Some(50000),
dist: Some(38100),
dir: Some(5_400_000),
rot_with_shape: None,
}),
..Default::default()
};
let mut w = XmlWriter::new();
effects.write_xml(&mut w);
let buf = &w.buf;
assert!(buf.contains("<a:reflection"));
assert!(buf.contains("blurRad=\"50000\""));
assert!(buf.contains("stA=\"52000\""));
assert!(buf.contains("endPos=\"50000\""));
assert!(buf.contains("dist=\"38100\""));
assert!(buf.contains("/>")); }
#[test]
fn effect_list_empty_writes_nothing() {
let effects = EffectList::default();
let mut w = XmlWriter::new();
effects.write_xml(&mut w);
assert!(w.buf.is_empty());
}
#[test]
fn blip_fill_mode_stretch_serialize() {
let mode = BlipFillMode::Stretch;
let mut w = XmlWriter::new();
mode.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
assert!(s.contains("<a:fillRect/>"), "应输出 a:fillRect,实际: {s}");
assert!(s.contains("</a:stretch>"), "应关闭 a:stretch,实际: {s}");
}
#[test]
fn blip_fill_mode_tile_serialize_full() {
let mode = BlipFillMode::Tile {
tx: Some(914400),
ty: Some(457200),
sx: Some(100_000),
sy: Some(50_000),
flip: Some("x".to_string()),
algn: Some("tl".to_string()),
};
let mut w = XmlWriter::new();
mode.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
assert!(s.contains("tx=\"914400\""), "应包含 tx,实际: {s}");
assert!(s.contains("ty=\"457200\""), "应包含 ty,实际: {s}");
assert!(s.contains("sx=\"100000\""), "应包含 sx,实际: {s}");
assert!(s.contains("sy=\"50000\""), "应包含 sy,实际: {s}");
assert!(s.contains("flip=\"x\""), "应包含 flip,实际: {s}");
assert!(s.contains("algn=\"tl\""), "应包含 algn,实际: {s}");
assert!(s.contains("/>"), "应为自闭合标签,实际: {s}");
}
#[test]
fn blip_fill_mode_tile_serialize_partial() {
let mode = BlipFillMode::Tile {
tx: None,
ty: None,
sx: Some(200_000),
sy: None,
flip: None,
algn: Some("ctr".to_string()),
};
let mut w = XmlWriter::new();
mode.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
assert!(!s.contains("tx="), "不应包含 tx,实际: {s}");
assert!(!s.contains("ty="), "不应包含 ty,实际: {s}");
assert!(s.contains("sx=\"200000\""), "应包含 sx,实际: {s}");
assert!(!s.contains("sy="), "不应包含 sy,实际: {s}");
assert!(s.contains("algn=\"ctr\""), "应包含 algn,实际: {s}");
}
#[test]
fn blip_fill_mode_none_serialize() {
let mode = BlipFillMode::None;
let mut w = XmlWriter::new();
mode.write_xml(&mut w);
assert!(w.buf.is_empty(), "None 模式不应写出任何 XML");
}
#[test]
fn fill_blip_with_stretch_serialize() {
let fill = Fill::Blip {
rid: "rIdImg1".to_string(),
mode: BlipFillMode::Stretch,
};
let mut w = XmlWriter::new();
fill.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
assert!(
s.contains("r:embed=\"rIdImg1\""),
"应包含 r:embed,实际: {s}"
);
assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
let blip_count = s.matches("<a:blip ").count()
+ s.matches("<a:blip/>").count()
+ s.matches("<a:blip>").count();
assert_eq!(
blip_count, 1,
"应仅输出 1 个 <a:blip 标签,实际 {} 次(可能存在双重标签 bug): {s}",
blip_count
);
assert!(
!s.contains("<a:blip><a:blip"),
"检测到嵌套的双重 <a:blip> 标签(BUG-001 回归): {s}"
);
}
#[test]
fn fill_blip_with_tile_serialize() {
let fill = Fill::Blip {
rid: "rIdImg2".to_string(),
mode: BlipFillMode::Tile {
tx: Some(100),
ty: None,
sx: None,
sy: None,
flip: Some("xy".to_string()),
algn: None,
},
};
let mut w = XmlWriter::new();
fill.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
assert!(
s.contains("r:embed=\"rIdImg2\""),
"应包含 r:embed,实际: {s}"
);
assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
assert!(s.contains("tx=\"100\""), "应包含 tx,实际: {s}");
assert!(s.contains("flip=\"xy\""), "应包含 flip,实际: {s}");
}
#[test]
fn blip_fill_mode_default_is_stretch() {
let mode = BlipFillMode::default();
assert!(matches!(mode, BlipFillMode::Stretch));
}
#[test]
fn rotation_3d_serialize() {
let rot = Rotation3d {
lat: 0,
lon: 0,
rev: 1200000,
};
let mut w = XmlWriter::new();
rot.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:rot"), "应输出 a:rot,实际: {s}");
assert!(s.contains("lat=\"0\""), "应包含 lat,实际: {s}");
assert!(s.contains("lon=\"0\""), "应包含 lon,实际: {s}");
assert!(s.contains("rev=\"1200000\""), "应包含 rev,实际: {s}");
assert!(s.contains("/>"), "应为自闭合,实际: {s}");
}
#[test]
fn camera_default_serialize() {
let camera = Camera::default();
let mut w = XmlWriter::new();
camera.write_xml(&mut w);
let s = &w.buf;
assert!(
s.contains("<a:camera prst=\"orthographicFront\""),
"应包含默认 prst,实际: {s}"
);
assert!(!s.contains("fov="), "默认 fov 不应输出,实际: {s}");
assert!(!s.contains("zoom="), "默认 zoom 不应输出,实际: {s}");
}
#[test]
fn camera_with_rotation_serialize() {
let camera = Camera {
preset: CameraPreset::PerspectiveFront,
fov: 3600000,
zoom: 200000,
rotation: Some(Rotation3d {
lat: 30,
lon: 45,
rev: 0,
}),
};
let mut w = XmlWriter::new();
camera.write_xml(&mut w);
let s = &w.buf;
assert!(
s.contains("<a:camera prst=\"perspectiveFront\""),
"应包含 perspectiveFront,实际: {s}"
);
assert!(s.contains("fov=\"3600000\""), "应包含 fov,实际: {s}");
assert!(s.contains("zoom=\"200000\""), "应包含 zoom,实际: {s}");
assert!(s.contains("<a:rot"), "应包含 a:rot 子元素,实际: {s}");
assert!(s.contains("</a:camera>"), "应关闭 a:camera,实际: {s}");
}
#[test]
fn scene3d_full_serialize() {
let scene = Scene3d {
camera: Camera {
preset: CameraPreset::OrthographicFront,
fov: 0,
zoom: 0,
rotation: Some(Rotation3d {
lat: 0,
lon: 0,
rev: 0,
}),
},
light_rig: LightRig {
rig: LightRigType::ThreePt,
dir: LightRigDirection::Top,
rotation: Some(Rotation3d {
lat: 0,
lon: 0,
rev: 1200000,
}),
},
backdrop: None,
};
let mut w = XmlWriter::new();
scene.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:scene3d>"), "应输出 a:scene3d,实际: {s}");
assert!(
s.contains("<a:camera prst=\"orthographicFront\">"),
"应包含 camera 子元素,实际: {s}"
);
assert!(
s.contains("<a:lightRig rig=\"threePt\" dir=\"t\">"),
"应包含 lightRig 子元素,实际: {s}"
);
assert!(s.contains("</a:scene3d>"), "应关闭 a:scene3d,实际: {s}");
}
#[test]
fn sp3d_default_serialize() {
let sp3d = Sp3d::default();
let mut w = XmlWriter::new();
sp3d.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:sp3d"), "应输出 a:sp3d,实际: {s}");
assert!(
!s.contains("prstMaterial="),
"默认 warmMatte 不应输出,实际: {s}"
);
}
#[test]
fn sp3d_with_bevel_and_colors_serialize() {
let sp3d = Sp3d {
extrusion_h: 38100,
contour_w: 12700,
prst_material: MaterialPreset::Metallic,
bevel_top: Some(Bevel { w: 63500, h: 25400 }),
bevel_bottom: None,
extrusion_color: Some(Color::RGB(crate::units::RGBColor::BLACK)),
contour_color: Some(Color::RGB(crate::units::RGBColor::WHITE)),
};
let mut w = XmlWriter::new();
sp3d.write_xml(&mut w);
let s = &w.buf;
assert!(
s.contains("extrusionH=\"38100\""),
"应包含 extrusionH,实际: {s}"
);
assert!(
s.contains("contourW=\"12700\""),
"应包含 contourW,实际: {s}"
);
assert!(
s.contains("prstMaterial=\"metallic\""),
"应包含 prstMaterial,实际: {s}"
);
assert!(
s.contains("<a:bevelT w=\"63500\" h=\"25400\"/>"),
"应包含 bevelT,实际: {s}"
);
assert!(
s.contains("<a:extrusionClr>"),
"应包含 extrusionClr,实际: {s}"
);
assert!(s.contains("<a:contourClr>"), "应包含 contourClr,实际: {s}");
assert!(s.contains("</a:sp3d>"), "应关闭 a:sp3d,实际: {s}");
}
#[test]
fn shape_properties_with_3d_serialize() {
let sp = ShapeProperties {
scene3d: Some(Scene3d::default()),
sp3d: Some(Sp3d::default()),
..Default::default()
};
let mut w = XmlWriter::new();
sp.write_xml(&mut w, "p:spPr");
let s = &w.buf;
assert!(s.contains("<a:scene3d>"), "应输出 scene3d,实际: {s}");
assert!(s.contains("<a:sp3d"), "应输出 sp3d,实际: {s}");
let scene_pos = s.find("<a:scene3d>").expect("scene3d 应存在");
let sp3d_pos = s.find("<a:sp3d").expect("sp3d 应存在");
assert!(
scene_pos < sp3d_pos,
"scene3d 应在 sp3d 之前,实际 scene3d@{scene_pos} sp3d@{sp3d_pos}"
);
}
#[test]
fn camera_preset_from_str() {
assert!(matches!(
CameraPreset::parse("orthographicFront"),
CameraPreset::OrthographicFront
));
assert!(matches!(
CameraPreset::parse("perspectiveFront"),
CameraPreset::PerspectiveFront
));
match CameraPreset::parse("customUnknown") {
CameraPreset::Other(s) => assert_eq!(s, "customUnknown"),
other => panic!("未知预设应归入 Other,实际: {other:?}"),
}
}
#[test]
fn material_preset_from_str() {
assert!(matches!(
MaterialPreset::parse("warmMatte"),
MaterialPreset::WarmMatte
));
assert!(matches!(
MaterialPreset::parse("metallic"),
MaterialPreset::Metallic
));
match MaterialPreset::parse("futureMaterial") {
MaterialPreset::Other(s) => assert_eq!(s, "futureMaterial"),
other => panic!("未知材质应归入 Other,实际: {other:?}"),
}
}
#[test]
fn backdrop_default_serialize() {
let bd = Backdrop::default();
let mut w = XmlWriter::new();
bd.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:backdrop>"), "应输出 a:backdrop,实际: {s}");
assert!(s.contains("</a:backdrop>"), "应关闭 a:backdrop,实际: {s}");
assert!(!s.contains("<a:floor/>"), "默认不应输出 floor: {s}");
assert!(!s.contains("<a:wall/>"), "默认不应输出 wall: {s}");
assert!(!s.contains("<a:l/>"), "默认不应输出 l: {s}");
assert!(!s.contains("<a:r/>"), "默认不应输出 r: {s}");
assert!(!s.contains("<a:t/>"), "默认不应输出 t: {s}");
assert!(!s.contains("<a:b/>"), "默认不应输出 b: {s}");
}
#[test]
fn backdrop_with_anchor_and_all_planes_serialize() {
let bd = Backdrop {
anchor: Some(Point3d {
x: 100000,
y: 200000,
z: 300000,
}),
floor: true,
wall: true,
left: true,
right: true,
top: true,
bottom: true,
};
let mut w = XmlWriter::new();
bd.write_xml(&mut w);
let s = &w.buf;
assert!(
s.contains("<a:anchor x=\"100000\" y=\"200000\" z=\"300000\"/>"),
"应包含 anchor,实际: {s}"
);
assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
assert!(s.contains("<a:l/>"), "应包含 l: {s}");
assert!(s.contains("<a:r/>"), "应包含 r: {s}");
assert!(s.contains("<a:t/>"), "应包含 t: {s}");
assert!(s.contains("<a:b/>"), "应包含 b: {s}");
let anchor_pos = s.find("<a:anchor").expect("anchor 应存在");
let floor_pos = s.find("<a:floor/>").expect("floor 应存在");
let wall_pos = s.find("<a:wall/>").expect("wall 应存在");
let l_pos = s.find("<a:l/>").expect("l 应存在");
let r_pos = s.find("<a:r/>").expect("r 应存在");
let t_pos = s.find("<a:t/>").expect("t 应存在");
let b_pos = s.find("<a:b/>").expect("b 应存在");
assert!(anchor_pos < floor_pos, "anchor 应在 floor 之前");
assert!(floor_pos < wall_pos, "floor 应在 wall 之前");
assert!(wall_pos < l_pos, "wall 应在 l 之前");
assert!(l_pos < r_pos, "l 应在 r 之前");
assert!(r_pos < t_pos, "r 应在 t 之前");
assert!(t_pos < b_pos, "t 应在 b 之前");
}
#[test]
fn backdrop_partial_planes_serialize() {
let bd = Backdrop {
floor: true,
wall: true,
..Default::default()
};
let mut w = XmlWriter::new();
bd.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
assert!(!s.contains("<a:l/>"), "不应包含 l: {s}");
assert!(!s.contains("<a:r/>"), "不应包含 r: {s}");
assert!(!s.contains("<a:t/>"), "不应包含 t: {s}");
assert!(!s.contains("<a:b/>"), "不应包含 b: {s}");
}
#[test]
fn scene3d_with_backdrop_serialize() {
let scene = Scene3d {
camera: Camera::default(),
light_rig: LightRig::default(),
backdrop: Some(Backdrop {
floor: true,
wall: true,
..Default::default()
}),
};
let mut w = XmlWriter::new();
scene.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
assert!(s.contains("<a:backdrop>"), "应包含 backdrop: {s}");
assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
let cam_pos = s.find("<a:camera").expect("camera 应存在");
let rig_pos = s.find("<a:lightRig").expect("lightRig 应存在");
let bd_pos = s.find("<a:backdrop>").expect("backdrop 应存在");
assert!(cam_pos < rig_pos, "camera 应在 lightRig 之前");
assert!(rig_pos < bd_pos, "lightRig 应在 backdrop 之前");
}
#[test]
fn scene3d_without_backdrop_no_element() {
let scene = Scene3d::default();
let mut w = XmlWriter::new();
scene.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
assert!(!s.contains("<a:backdrop>"), "无 backdrop 时不应输出: {s}");
}
#[test]
fn point3d_serialize() {
let p = Point3d {
x: -100000,
y: 0,
z: 500000,
};
let mut w = XmlWriter::new();
p.write_xml(&mut w);
let s = &w.buf;
assert!(s.contains("x=\"-100000\""), "应包含 x: {s}");
assert!(s.contains("y=\"0\""), "应包含 y: {s}");
assert!(s.contains("z=\"500000\""), "应包含 z: {s}");
}
}