use drawingml::{
BlipFill, BlipFillMode, Color, EffectList, Fill, Geometry, GeometryAdjustment, GradientFill,
GradientStop, Line, LineCap, LineCompound, LineEnd, LineEndSize, LineEndType, LineJoin,
OuterShadow, PatternFill, PresetLineDash, PresetPattern, PresetShape, Reader, ShapeProperties,
TextAlign, TextAnchor, TextBody, TextBodyProperties, TextCaps, TextParagraph,
TextParagraphProperties, TextRun, TextRunProperties, TextStrike, TextUnderline,
TextVerticalType, TextWrap, Transform2D, read_shape_properties, read_text_body,
write_geometry_fill_line_effects, write_shape_properties, write_text_body,
};
use xml_core::{BytesStart, Event, Writer};
const SPPR_XMLNS_A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
fn roundtrip(properties: &ShapeProperties) -> ShapeProperties {
let mut writer = Writer::new(Vec::new());
let mut root = BytesStart::new("spPr");
root.push_attribute(("xmlns:a", SPPR_XMLNS_A));
writer.write_event(Event::Start(root)).unwrap();
write_shape_properties(&mut writer, properties).unwrap();
writer
.write_event(Event::End(xml_core::BytesEnd::new("spPr")))
.unwrap();
let xml = String::from_utf8(writer.into_inner()).unwrap();
let mut reader = Reader::from_xml_str(&xml);
reader.read_event().unwrap(); read_shape_properties(&mut reader).unwrap()
}
#[test]
fn round_trips_a_transform() {
let properties = ShapeProperties::new().with_transform(
Transform2D::new()
.with_offset(914_400, 457_200)
.with_extent(1_828_800, 914_400)
.with_rotation_degrees(45.0)
.with_flip_horizontal(true),
);
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_transform_with_no_offset_or_extent() {
let properties =
ShapeProperties::new().with_transform(Transform2D::new().with_rotation_degrees(90.0));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_preset_geometry_shape() {
let properties =
ShapeProperties::new().with_geometry(Geometry::Preset(PresetShape::RoundedRectangle));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_preset_geometry_not_in_the_closed_enum() {
let properties = ShapeProperties::new().with_geometry(Geometry::Preset(PresetShape::Other(
"irregularSeal1".to_string(),
)));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_solid_rgb_fill() {
let properties =
ShapeProperties::new().with_fill(Fill::Solid(Color::Rgb("FF9900".to_string())));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_no_fill() {
let properties = ShapeProperties::new().with_fill(Fill::None);
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_an_rgb_percent_color() {
let properties = ShapeProperties::new().with_fill(Fill::Solid(Color::RgbPercent {
red: 50_000,
green: 25_000,
blue: 100_000,
}));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_an_hsl_color() {
let properties = ShapeProperties::new().with_fill(Fill::Solid(Color::Hsl {
hue_60000ths: 10_800_000,
saturation_1000ths_percent: 80_000,
luminance_1000ths_percent: 50_000,
}));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_system_color_with_a_fallback() {
let properties = ShapeProperties::new().with_fill(Fill::Solid(Color::System {
value: "windowText".to_string(),
last_color: Some("000000".to_string()),
}));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_preset_named_color() {
let properties =
ShapeProperties::new().with_fill(Fill::Solid(Color::Preset("tomato".to_string())));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_gradient_fill() {
let gradient = GradientFill::new()
.with_stop(GradientStop::new(0.0, Color::Rgb("FFFFFF".to_string())))
.with_stop(GradientStop::new(100.0, Color::Rgb("000000".to_string())))
.with_angle_degrees(90.0);
let properties = ShapeProperties::new().with_fill(Fill::Gradient(gradient));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_pattern_fill() {
let pattern = PatternFill::new(PresetPattern::LightDiagonalDown)
.with_foreground(Color::Rgb("FF0000".to_string()))
.with_background(Color::Rgb("FFFFFF".to_string()));
let properties = ShapeProperties::new().with_fill(Fill::Pattern(pattern));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_pattern_fill_with_no_explicit_colors() {
let properties =
ShapeProperties::new().with_fill(Fill::Pattern(PatternFill::new(PresetPattern::Weave)));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_line_with_dash_cap_join_and_arrowheads() {
let line = Line::new()
.with_width_emu(25_400)
.with_fill(Fill::Solid(Color::Rgb("2E75B6".to_string())))
.with_dash(PresetLineDash::DashDot)
.with_cap(LineCap::Round)
.with_join(LineJoin::Miter {
limit_1000ths_percent: Some(800_000),
})
.with_head_end(
LineEnd::new()
.with_kind(LineEndType::Triangle)
.with_width(LineEndSize::Medium)
.with_length(LineEndSize::Small),
)
.with_tail_end(LineEnd::new().with_kind(LineEndType::Arrow));
let properties = ShapeProperties::new().with_line(line);
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_bare_line_with_only_a_width() {
let properties = ShapeProperties::new().with_line(Line::new().with_width_emu(9_525));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_preset_geometry_with_a_single_adjustment_value() {
let properties = ShapeProperties::new()
.with_geometry(Geometry::Preset(PresetShape::Chevron))
.with_geometry_adjustments(vec![GeometryAdjustment::new("adj", "val 15000")]);
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_preset_geometry_with_several_adjustment_values() {
let properties = ShapeProperties::new()
.with_geometry(Geometry::Preset(PresetShape::RoundedRectangle))
.with_geometry_adjustments(vec![
GeometryAdjustment::new("adj1", "val 10000"),
GeometryAdjustment::new("adj2", "val 20000"),
]);
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_preset_geometry_with_no_adjustment_values() {
let properties = ShapeProperties::new().with_geometry(Geometry::Preset(PresetShape::Rectangle));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_compound_line_style() {
let properties = ShapeProperties::new().with_line(
Line::new()
.with_width_emu(38_100)
.with_fill(Fill::Solid(Color::Rgb("2E2E2E".to_string())))
.with_compound(LineCompound::ThickThin),
);
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_fully_combined_shape() {
let properties = ShapeProperties::new()
.with_transform(
Transform2D::new()
.with_offset(0, 0)
.with_extent(1_000_000, 500_000),
)
.with_geometry(Geometry::Preset(PresetShape::Ellipse))
.with_fill(Fill::Solid(Color::Rgb("4472C4".to_string())))
.with_line(Line::new().with_width_emu(12_700).with_cap(LineCap::Flat));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_an_image_fill_with_a_relationship_id_and_stretch_mode() {
let properties = ShapeProperties::new().with_fill(Fill::Image(
BlipFill::new("rId3").with_mode(BlipFillMode::Stretch),
));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_an_image_fill_with_tile_mode() {
let properties = ShapeProperties::new().with_fill(Fill::Image(
BlipFill::new("rId7").with_mode(BlipFillMode::Tile),
));
assert_eq!(roundtrip(&properties), properties);
}
#[test]
fn round_trips_a_bare_image_fill_with_no_relationship_or_mode() {
let properties = ShapeProperties::new().with_fill(Fill::Image(BlipFill::default()));
assert_eq!(roundtrip(&properties), properties);
}
const TXBODY_XMLNS_A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
fn roundtrip_text_body(body: &TextBody) -> TextBody {
let mut writer = Writer::new(Vec::new());
let mut root = BytesStart::new("txBody");
root.push_attribute(("xmlns:a", TXBODY_XMLNS_A));
writer.write_event(Event::Start(root)).unwrap();
write_text_body(&mut writer, body).unwrap();
writer
.write_event(Event::End(xml_core::BytesEnd::new("txBody")))
.unwrap();
let xml = String::from_utf8(writer.into_inner()).unwrap();
let mut reader = Reader::from_xml_str(&xml);
reader.read_event().unwrap(); read_text_body(&mut reader).unwrap()
}
#[test]
fn round_trips_an_empty_text_body_with_body_properties() {
let body = TextBody::new().with_properties(
TextBodyProperties::new()
.with_wrap(TextWrap::Square)
.with_anchor(TextAnchor::Center)
.with_insets_emu(91_440, 45_720, 91_440, 45_720),
);
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_a_paragraph_with_alignment_and_margins() {
let body = TextBody::new().with_paragraph(
TextParagraph::new()
.with_properties(
TextParagraphProperties::new()
.with_alignment(TextAlign::Center)
.with_margins_emu(0, 0)
.with_indent_emu(-228_600),
)
.with_run(TextRun::text("Titre centre")),
);
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_a_run_with_full_character_formatting() {
let run_properties = TextRunProperties::new()
.with_bold(true)
.with_italic(true)
.with_underline(TextUnderline::Single)
.with_strike(TextStrike::Double)
.with_fill(Fill::Solid(Color::Rgb("C00000".to_string())))
.with_font_size_points(18.0)
.with_font_family("Calibri");
let body = TextBody::new().with_paragraph(
TextParagraph::new().with_run(TextRun::text_with_properties("Attention", run_properties)),
);
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_a_plain_run_with_no_formatting() {
let body = TextBody::new()
.with_paragraph(TextParagraph::new().with_run(TextRun::text("Texte simple")));
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_a_line_break_with_no_formatting_override() {
let body = TextBody::new().with_paragraph(
TextParagraph::new()
.with_run(TextRun::text("Premiere ligne"))
.with_run(TextRun::line_break())
.with_run(TextRun::text("Deuxieme ligne")),
);
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_a_line_break_with_a_formatting_override() {
let break_properties = TextRunProperties::new().with_bold(true);
let body = TextBody::new().with_paragraph(
TextParagraph::new()
.with_run(TextRun::text("Avant"))
.with_run(TextRun::LineBreak {
properties: Some(break_properties),
}),
);
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_text_caps_and_character_spacing() {
let run_properties = TextRunProperties::new()
.with_text_caps(TextCaps::All)
.with_character_spacing_points(2.5);
let body = TextBody::new().with_paragraph(TextParagraph::new().with_run(
TextRun::text_with_properties("MAJUSCULES ESPACEES", run_properties),
));
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_small_caps_with_no_character_spacing() {
let run_properties = TextRunProperties::new().with_text_caps(TextCaps::Small);
let body = TextBody::new().with_paragraph(TextParagraph::new().with_run(
TextRun::text_with_properties("petites capitales", run_properties),
));
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_negative_character_spacing() {
let run_properties = TextRunProperties::new().with_character_spacing_points(-1.5);
let body = TextBody::new().with_paragraph(
TextParagraph::new().with_run(TextRun::text_with_properties("serre", run_properties)),
);
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_text_body_vertical_direction_and_rotation() {
let body = TextBody::new()
.with_properties(
TextBodyProperties::new()
.with_vertical_direction(TextVerticalType::Vertical270)
.with_rotation_degrees(-15.0),
)
.with_paragraph(TextParagraph::new().with_run(TextRun::text("Texte vertical pivote")));
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_a_dynamic_text_field() {
let body = TextBody::new().with_paragraph(TextParagraph::new().with_run(TextRun::field(
"{5C4A1234-0000-0000-0000-000000000000}",
Some("slidenum".to_string()),
"1",
)));
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_a_dynamic_text_field_with_no_type_and_with_formatting() {
let body = TextBody::new().with_paragraph(TextParagraph::new().with_run(TextRun::Field {
id: "{5C4A1234-1111-1111-1111-111111111111}".to_string(),
field_type: None,
cached_text: "12/07/2026".to_string(),
properties: TextRunProperties::new().with_bold(true),
}));
assert_eq!(roundtrip_text_body(&body), body);
}
#[test]
fn round_trips_multiple_paragraphs_with_a_gradient_filled_run() {
let gradient = GradientFill::new()
.with_stop(GradientStop::new(0.0, Color::Rgb("FFFFFF".to_string())))
.with_stop(GradientStop::new(100.0, Color::Rgb("000000".to_string())));
let run_properties = TextRunProperties::new().with_fill(Fill::Gradient(gradient));
let body = TextBody::new()
.with_paragraph(TextParagraph::new().with_run(TextRun::text("Premier paragraphe")))
.with_paragraph(TextParagraph::new().with_run(TextRun::text_with_properties(
"Second paragraphe, degrade",
run_properties,
)));
assert_eq!(roundtrip_text_body(&body), body);
}
fn roundtrip_geometry_fill_line_effects(
properties: &ShapeProperties,
default_preset: &str,
) -> ShapeProperties {
let mut writer = Writer::new(Vec::new());
let mut root = BytesStart::new("spPr");
root.push_attribute(("xmlns:a", SPPR_XMLNS_A));
writer.write_event(Event::Start(root)).unwrap();
write_geometry_fill_line_effects(&mut writer, properties, default_preset).unwrap();
writer
.write_event(Event::End(xml_core::BytesEnd::new("spPr")))
.unwrap();
let xml = String::from_utf8(writer.into_inner()).unwrap();
let mut reader = Reader::from_xml_str(&xml);
reader.read_event().unwrap(); read_shape_properties(&mut reader).unwrap()
}
#[test]
fn round_trips_geometry_fill_line_effects_with_a_default_geometry_fallback() {
let properties =
ShapeProperties::new().with_fill(Fill::Solid(Color::Rgb("336699".to_string())));
let read_back = roundtrip_geometry_fill_line_effects(&properties, "rect");
assert_eq!(
read_back.geometry,
Some(Geometry::Preset(PresetShape::Rectangle))
);
assert_eq!(
read_back.fill,
Some(Fill::Solid(Color::Rgb("336699".to_string())))
);
}
#[test]
fn round_trips_geometry_fill_line_effects_with_an_explicit_geometry() {
let properties = ShapeProperties::new().with_geometry(Geometry::Preset(PresetShape::Ellipse));
let read_back = roundtrip_geometry_fill_line_effects(&properties, "rect");
assert_eq!(
read_back.geometry,
Some(Geometry::Preset(PresetShape::Ellipse))
);
}
#[test]
fn round_trips_geometry_fill_line_effects_with_effects() {
let effects = EffectList::new().with_outer_shadow(
OuterShadow::new(Color::Rgb("000000".to_string()))
.with_blur_radius_emu(90_000)
.with_distance_emu(50_000),
);
let properties = ShapeProperties::new()
.with_fill(Fill::Solid(Color::Rgb("336699".to_string())))
.with_effects(effects);
let read_back = roundtrip_geometry_fill_line_effects(&properties, "rect");
assert_eq!(
read_back.geometry,
Some(Geometry::Preset(PresetShape::Rectangle))
);
let read_effects = read_back.effects.expect("effects should round-trip");
assert_eq!(
read_effects.outer_shadow.as_ref().map(|s| s.color.clone()),
Some(Color::Rgb("000000".to_string()))
);
}