use std::io::Write;
use xml_core::{BytesEnd, BytesStart, BytesText, Event, Writer};
use crate::error::Result;
use crate::model::{
BlipFill, BlipFillMode, BulletColor, BulletFont, BulletKind, BulletSize, Color, CropRect,
CustomGeometry, EffectList, Fill, Geometry, GeometryAdjustment, GradientFill, GradientStop,
Line, LineEnd, LineJoin, PathCommand, PatternFill, ShapeProperties, TextAutofit, TextBody,
TextBodyProperties, TextParagraph, TextParagraphProperties, TextRun, TextRunProperties,
TextSpacing, Transform2D,
};
fn validate_field_id(id: &str) -> Result<()> {
let bytes = id.as_bytes();
let is_hex_run = |s: &[u8], len: usize| s.len() == len && s.iter().all(u8::is_ascii_hexdigit);
let valid = bytes.len() == 38
&& bytes[0] == b'{'
&& bytes[37] == b'}'
&& bytes[9] == b'-'
&& bytes[14] == b'-'
&& bytes[19] == b'-'
&& bytes[24] == b'-'
&& is_hex_run(&bytes[1..9], 8)
&& is_hex_run(&bytes[10..14], 4)
&& is_hex_run(&bytes[15..19], 4)
&& is_hex_run(&bytes[20..24], 4)
&& is_hex_run(&bytes[25..37], 12)
&& id.chars().all(|c| !c.is_ascii_hexdigit() || c.is_ascii_digit() || c.is_ascii_uppercase());
if valid {
Ok(())
} else {
Err(crate::error::Error::InvalidFieldId(id.to_string()))
}
}
pub fn write_shape_properties<W: Write>(
writer: &mut Writer<W>,
properties: &ShapeProperties,
) -> Result<()> {
if let Some(transform) = &properties.transform {
write_transform(writer, transform)?;
}
if let Some(geometry) = &properties.geometry {
write_geometry(writer, geometry, &properties.geometry_adjustments)?;
}
if let Some(fill) = &properties.fill {
write_fill(writer, fill)?;
}
if let Some(line) = &properties.line {
write_line(writer, line)?;
}
if let Some(effects) = &properties.effects {
write_effect_list(writer, effects)?;
}
Ok(())
}
pub fn write_geometry_fill_line_effects<W: Write>(
writer: &mut Writer<W>,
properties: &ShapeProperties,
default_preset: &str,
) -> Result<()> {
match &properties.geometry {
Some(geometry) => write_geometry(writer, geometry, &properties.geometry_adjustments)?,
None => {
let mut prst_geom = BytesStart::new("a:prstGeom");
prst_geom.push_attribute(("prst", default_preset));
writer.write_event(Event::Start(prst_geom))?;
writer.write_event(Event::Empty(BytesStart::new("a:avLst")))?;
writer.write_event(Event::End(BytesEnd::new("a:prstGeom")))?;
}
}
if let Some(fill) = &properties.fill {
write_fill(writer, fill)?;
}
if let Some(line) = &properties.line {
write_line(writer, line)?;
}
if let Some(effects) = &properties.effects {
write_effect_list(writer, effects)?;
}
Ok(())
}
pub fn write_effect_list<W: Write>(writer: &mut Writer<W>, effects: &EffectList) -> Result<()> {
let has_children = effects.outer_shadow.is_some()
|| effects.glow.is_some()
|| effects.reflection.is_some()
|| effects.soft_edge.is_some();
if !has_children {
writer.write_event(Event::Empty(BytesStart::new("a:effectLst")))?;
return Ok(());
}
writer.write_event(Event::Start(BytesStart::new("a:effectLst")))?;
if let Some(shadow) = &effects.outer_shadow {
let mut start = BytesStart::new("a:outerShdw");
if let Some(blur) = shadow.blur_radius_emu {
start.push_attribute(("blurRad", blur.to_string().as_str()));
}
if let Some(distance) = shadow.distance_emu {
start.push_attribute(("dist", distance.to_string().as_str()));
}
if let Some(direction) = shadow.direction_60000ths {
start.push_attribute(("dir", direction.to_string().as_str()));
}
writer.write_event(Event::Start(start))?;
write_color(writer, &shadow.color)?;
writer.write_event(Event::End(BytesEnd::new("a:outerShdw")))?;
}
if let Some(glow) = &effects.glow {
let mut start = BytesStart::new("a:glow");
start.push_attribute(("rad", glow.radius_emu.to_string().as_str()));
writer.write_event(Event::Start(start))?;
write_color(writer, &glow.color)?;
writer.write_event(Event::End(BytesEnd::new("a:glow")))?;
}
if let Some(reflection) = &effects.reflection {
let mut start = BytesStart::new("a:reflection");
if let Some(blur) = reflection.blur_radius_emu {
start.push_attribute(("blurRad", blur.to_string().as_str()));
}
if let Some(distance) = reflection.distance_emu {
start.push_attribute(("dist", distance.to_string().as_str()));
}
if let Some(direction) = reflection.direction_60000ths {
start.push_attribute(("dir", direction.to_string().as_str()));
}
if let Some(start_alpha) = reflection.start_alpha_1000ths_percent {
start.push_attribute(("stA", start_alpha.to_string().as_str()));
}
if let Some(end_alpha) = reflection.end_alpha_1000ths_percent {
start.push_attribute(("endA", end_alpha.to_string().as_str()));
}
writer.write_event(Event::Empty(start))?;
}
if let Some(soft_edge) = &effects.soft_edge {
let mut start = BytesStart::new("a:softEdge");
start.push_attribute(("rad", soft_edge.radius_emu.to_string().as_str()));
writer.write_event(Event::Empty(start))?;
}
writer.write_event(Event::End(BytesEnd::new("a:effectLst")))?;
Ok(())
}
pub fn write_transform<W: Write>(writer: &mut Writer<W>, transform: &Transform2D) -> Result<()> {
let mut start = BytesStart::new("a:xfrm");
if transform.rotation_60000ths != 0 {
start.push_attribute(("rot", transform.rotation_60000ths.to_string().as_str()));
}
if transform.flip_horizontal {
start.push_attribute(("flipH", "1"));
}
if transform.flip_vertical {
start.push_attribute(("flipV", "1"));
}
if transform.offset.is_none() && transform.extent.is_none() {
writer.write_event(Event::Empty(start))?;
return Ok(());
}
writer.write_event(Event::Start(start))?;
if let Some((x, y)) = transform.offset {
let mut off = BytesStart::new("a:off");
off.push_attribute(("x", x.to_string().as_str()));
off.push_attribute(("y", y.to_string().as_str()));
writer.write_event(Event::Empty(off))?;
}
if let Some((cx, cy)) = transform.extent {
let mut ext = BytesStart::new("a:ext");
ext.push_attribute(("cx", cx.to_string().as_str()));
ext.push_attribute(("cy", cy.to_string().as_str()));
writer.write_event(Event::Empty(ext))?;
}
writer.write_event(Event::End(BytesEnd::new("a:xfrm")))?;
Ok(())
}
pub fn write_geometry<W: Write>(
writer: &mut Writer<W>,
geometry: &Geometry,
adjustments: &[GeometryAdjustment],
) -> Result<()> {
match geometry {
Geometry::Preset(shape) => {
let mut start = BytesStart::new("a:prstGeom");
start.push_attribute(("prst", shape.xml_token()));
if adjustments.is_empty() {
writer.write_event(Event::Empty(start))?;
} else {
writer.write_event(Event::Start(start))?;
writer.write_event(Event::Start(BytesStart::new("a:avLst")))?;
for adjustment in adjustments {
let mut gd = BytesStart::new("a:gd");
gd.push_attribute(("name", adjustment.name.as_str()));
gd.push_attribute(("fmla", adjustment.formula.as_str()));
writer.write_event(Event::Empty(gd))?;
}
writer.write_event(Event::End(BytesEnd::new("a:avLst")))?;
writer.write_event(Event::End(BytesEnd::new("a:prstGeom")))?;
}
}
Geometry::Custom(custom) => write_custom_geometry(writer, custom)?,
}
Ok(())
}
fn write_custom_geometry<W: Write>(writer: &mut Writer<W>, custom: &CustomGeometry) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("a:custGeom")))?;
writer.write_event(Event::Start(BytesStart::new("a:pathLst")))?;
let mut path = BytesStart::new("a:path");
path.push_attribute(("w", custom.width_emu.to_string().as_str()));
path.push_attribute(("h", custom.height_emu.to_string().as_str()));
if custom.commands.is_empty() {
writer.write_event(Event::Empty(path))?;
} else {
writer.write_event(Event::Start(path))?;
for command in &custom.commands {
write_path_command(writer, command)?;
}
writer.write_event(Event::End(BytesEnd::new("a:path")))?;
}
writer.write_event(Event::End(BytesEnd::new("a:pathLst")))?;
writer.write_event(Event::End(BytesEnd::new("a:custGeom")))?;
Ok(())
}
fn write_path_command<W: Write>(writer: &mut Writer<W>, command: &PathCommand) -> Result<()> {
match command {
PathCommand::MoveTo { x, y } => {
writer.write_event(Event::Start(BytesStart::new("a:moveTo")))?;
write_path_point(writer, *x, *y)?;
writer.write_event(Event::End(BytesEnd::new("a:moveTo")))?;
}
PathCommand::LineTo { x, y } => {
writer.write_event(Event::Start(BytesStart::new("a:lnTo")))?;
write_path_point(writer, *x, *y)?;
writer.write_event(Event::End(BytesEnd::new("a:lnTo")))?;
}
PathCommand::CubicBezierTo {
x1,
y1,
x2,
y2,
x,
y,
} => {
writer.write_event(Event::Start(BytesStart::new("a:cubicBezTo")))?;
write_path_point(writer, *x1, *y1)?;
write_path_point(writer, *x2, *y2)?;
write_path_point(writer, *x, *y)?;
writer.write_event(Event::End(BytesEnd::new("a:cubicBezTo")))?;
}
PathCommand::Close => {
writer.write_event(Event::Empty(BytesStart::new("a:close")))?;
}
}
Ok(())
}
fn write_path_point<W: Write>(writer: &mut Writer<W>, x: i64, y: i64) -> Result<()> {
let mut pt = BytesStart::new("a:pt");
pt.push_attribute(("x", x.to_string().as_str()));
pt.push_attribute(("y", y.to_string().as_str()));
writer.write_event(Event::Empty(pt))?;
Ok(())
}
pub fn write_fill<W: Write>(writer: &mut Writer<W>, fill: &Fill) -> Result<()> {
match fill {
Fill::None => writer.write_event(Event::Empty(BytesStart::new("a:noFill")))?,
Fill::Solid(color) => {
writer.write_event(Event::Start(BytesStart::new("a:solidFill")))?;
write_color(writer, color)?;
writer.write_event(Event::End(BytesEnd::new("a:solidFill")))?;
}
Fill::Gradient(gradient) => write_gradient_fill(writer, gradient)?,
Fill::Pattern(pattern) => write_pattern_fill(writer, pattern)?,
Fill::Image(blip_fill) => write_blip_fill(writer, blip_fill)?,
}
Ok(())
}
pub fn write_blip_fill<W: Write>(writer: &mut Writer<W>, blip_fill: &BlipFill) -> Result<()> {
let has_children =
blip_fill.relationship_id.is_some() || blip_fill.crop.is_some() || blip_fill.mode.is_some();
if !has_children {
writer.write_event(Event::Empty(BytesStart::new("a:blipFill")))?;
return Ok(());
}
writer.write_event(Event::Start(BytesStart::new("a:blipFill")))?;
if let Some(relationship_id) = &blip_fill.relationship_id {
let mut blip = BytesStart::new("a:blip");
blip.push_attribute(("r:embed", relationship_id.as_str()));
writer.write_event(Event::Empty(blip))?;
}
if let Some(crop) = &blip_fill.crop {
write_crop_rect(writer, crop)?;
}
if let Some(mode) = &blip_fill.mode {
let element_name = match mode {
BlipFillMode::Stretch => "a:stretch",
BlipFillMode::Tile => "a:tile",
};
writer.write_event(Event::Empty(BytesStart::new(element_name)))?;
}
writer.write_event(Event::End(BytesEnd::new("a:blipFill")))?;
Ok(())
}
fn write_crop_rect<W: Write>(writer: &mut Writer<W>, crop: &CropRect) -> Result<()> {
let mut start = BytesStart::new("a:srcRect");
if let Some(left) = crop.left_1000ths_percent {
start.push_attribute(("l", left.to_string().as_str()));
}
if let Some(top) = crop.top_1000ths_percent {
start.push_attribute(("t", top.to_string().as_str()));
}
if let Some(right) = crop.right_1000ths_percent {
start.push_attribute(("r", right.to_string().as_str()));
}
if let Some(bottom) = crop.bottom_1000ths_percent {
start.push_attribute(("b", bottom.to_string().as_str()));
}
writer.write_event(Event::Empty(start))?;
Ok(())
}
fn write_gradient_fill<W: Write>(writer: &mut Writer<W>, gradient: &GradientFill) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("a:gradFill")))?;
if !gradient.stops.is_empty() {
writer.write_event(Event::Start(BytesStart::new("a:gsLst")))?;
for stop in &gradient.stops {
write_gradient_stop(writer, stop)?;
}
writer.write_event(Event::End(BytesEnd::new("a:gsLst")))?;
}
if let Some(angle) = gradient.angle_60000ths {
let mut lin = BytesStart::new("a:lin");
lin.push_attribute(("ang", angle.to_string().as_str()));
writer.write_event(Event::Empty(lin))?;
}
writer.write_event(Event::End(BytesEnd::new("a:gradFill")))?;
Ok(())
}
fn write_gradient_stop<W: Write>(writer: &mut Writer<W>, stop: &GradientStop) -> Result<()> {
let mut start = BytesStart::new("a:gs");
start.push_attribute(("pos", stop.position_1000ths_percent.to_string().as_str()));
writer.write_event(Event::Start(start))?;
write_color(writer, &stop.color)?;
writer.write_event(Event::End(BytesEnd::new("a:gs")))?;
Ok(())
}
fn write_pattern_fill<W: Write>(writer: &mut Writer<W>, pattern: &PatternFill) -> Result<()> {
let mut start = BytesStart::new("a:pattFill");
start.push_attribute(("prst", pattern.preset.xml_token()));
if pattern.foreground.is_none() && pattern.background.is_none() {
writer.write_event(Event::Empty(start))?;
return Ok(());
}
writer.write_event(Event::Start(start))?;
if let Some(color) = &pattern.foreground {
writer.write_event(Event::Start(BytesStart::new("a:fgClr")))?;
write_color(writer, color)?;
writer.write_event(Event::End(BytesEnd::new("a:fgClr")))?;
}
if let Some(color) = &pattern.background {
writer.write_event(Event::Start(BytesStart::new("a:bgClr")))?;
write_color(writer, color)?;
writer.write_event(Event::End(BytesEnd::new("a:bgClr")))?;
}
writer.write_event(Event::End(BytesEnd::new("a:pattFill")))?;
Ok(())
}
pub fn write_color<W: Write>(writer: &mut Writer<W>, color: &Color) -> Result<()> {
match color {
Color::Rgb(hex) => {
let mut start = BytesStart::new("a:srgbClr");
start.push_attribute(("val", hex.as_str()));
writer.write_event(Event::Empty(start))?;
}
Color::RgbPercent { red, green, blue } => {
let mut start = BytesStart::new("a:scrgbClr");
start.push_attribute(("r", red.to_string().as_str()));
start.push_attribute(("g", green.to_string().as_str()));
start.push_attribute(("b", blue.to_string().as_str()));
writer.write_event(Event::Empty(start))?;
}
Color::Hsl {
hue_60000ths,
saturation_1000ths_percent,
luminance_1000ths_percent,
} => {
let mut start = BytesStart::new("a:hslClr");
start.push_attribute(("hue", hue_60000ths.to_string().as_str()));
start.push_attribute(("sat", saturation_1000ths_percent.to_string().as_str()));
start.push_attribute(("lum", luminance_1000ths_percent.to_string().as_str()));
writer.write_event(Event::Empty(start))?;
}
Color::System { value, last_color } => {
let mut start = BytesStart::new("a:sysClr");
start.push_attribute(("val", value.as_str()));
if let Some(last_color) = last_color {
start.push_attribute(("lastClr", last_color.as_str()));
}
writer.write_event(Event::Empty(start))?;
}
Color::Preset(name) => {
let mut start = BytesStart::new("a:prstClr");
start.push_attribute(("val", name.as_str()));
writer.write_event(Event::Empty(start))?;
}
}
Ok(())
}
pub fn write_line<W: Write>(writer: &mut Writer<W>, line: &Line) -> Result<()> {
write_line_named(writer, "a:ln", line)
}
pub fn write_line_named<W: Write>(
writer: &mut Writer<W>,
element_name: &str,
line: &Line,
) -> Result<()> {
let mut start = BytesStart::new(element_name);
if let Some(width) = line.width_emu {
start.push_attribute(("w", width.to_string().as_str()));
}
if let Some(cap) = &line.cap {
start.push_attribute(("cap", cap.xml_token()));
}
if let Some(compound) = &line.compound {
start.push_attribute(("cmpd", compound.xml_token()));
}
let has_children = line.fill.is_some()
|| line.dash.is_some()
|| line.join.is_some()
|| line.head_end.is_some()
|| line.tail_end.is_some();
if !has_children {
writer.write_event(Event::Empty(start))?;
return Ok(());
}
writer.write_event(Event::Start(start))?;
if let Some(fill) = &line.fill {
write_fill(writer, fill)?;
}
if let Some(dash) = &line.dash {
let mut prst_dash = BytesStart::new("a:prstDash");
prst_dash.push_attribute(("val", dash.xml_token()));
writer.write_event(Event::Empty(prst_dash))?;
}
if let Some(join) = &line.join {
write_line_join(writer, join)?;
}
if let Some(head_end) = &line.head_end {
write_line_end(writer, "a:headEnd", head_end)?;
}
if let Some(tail_end) = &line.tail_end {
write_line_end(writer, "a:tailEnd", tail_end)?;
}
writer.write_event(Event::End(BytesEnd::new(element_name)))?;
Ok(())
}
fn write_line_join<W: Write>(writer: &mut Writer<W>, join: &LineJoin) -> Result<()> {
match join {
LineJoin::Round => writer.write_event(Event::Empty(BytesStart::new("a:round")))?,
LineJoin::Bevel => writer.write_event(Event::Empty(BytesStart::new("a:bevel")))?,
LineJoin::Miter {
limit_1000ths_percent,
} => {
let mut start = BytesStart::new("a:miter");
if let Some(limit) = limit_1000ths_percent {
start.push_attribute(("lim", limit.to_string().as_str()));
}
writer.write_event(Event::Empty(start))?;
}
}
Ok(())
}
fn write_line_end<W: Write>(
writer: &mut Writer<W>,
element_name: &str,
end: &LineEnd,
) -> Result<()> {
let mut start = BytesStart::new(element_name);
if let Some(kind) = &end.kind {
start.push_attribute(("type", kind.xml_token()));
}
if let Some(width) = &end.width {
start.push_attribute(("w", width.xml_token()));
}
if let Some(length) = &end.length {
start.push_attribute(("len", length.xml_token()));
}
writer.write_event(Event::Empty(start))?;
Ok(())
}
pub fn write_text_body<W: Write>(writer: &mut Writer<W>, body: &TextBody) -> Result<()> {
write_text_body_properties(writer, &body.properties)?;
for paragraph in &body.paragraphs {
write_text_paragraph(writer, paragraph)?;
}
Ok(())
}
pub fn write_text_body_properties<W: Write>(
writer: &mut Writer<W>,
properties: &TextBodyProperties,
) -> Result<()> {
let mut start = BytesStart::new("a:bodyPr");
if properties.rotation_60000ths != 0 {
start.push_attribute(("rot", properties.rotation_60000ths.to_string().as_str()));
}
if let Some(vertical_direction) = &properties.vertical_direction {
start.push_attribute(("vert", vertical_direction.xml_token()));
}
if let Some(wrap) = &properties.wrap {
start.push_attribute(("wrap", wrap.xml_token()));
}
if let Some(anchor) = &properties.anchor {
start.push_attribute(("anchor", anchor.xml_token()));
}
if properties.anchor_center {
start.push_attribute(("anchorCtr", "1"));
}
if let Some(inset) = properties.inset_left_emu {
start.push_attribute(("lIns", inset.to_string().as_str()));
}
if let Some(inset) = properties.inset_top_emu {
start.push_attribute(("tIns", inset.to_string().as_str()));
}
if let Some(inset) = properties.inset_right_emu {
start.push_attribute(("rIns", inset.to_string().as_str()));
}
if let Some(inset) = properties.inset_bottom_emu {
start.push_attribute(("bIns", inset.to_string().as_str()));
}
let Some(autofit) = &properties.autofit else {
writer.write_event(Event::Empty(start))?;
return Ok(());
};
writer.write_event(Event::Start(start))?;
match autofit {
TextAutofit::None => writer.write_event(Event::Empty(BytesStart::new("a:noAutofit")))?,
TextAutofit::Shape => writer.write_event(Event::Empty(BytesStart::new("a:spAutoFit")))?,
TextAutofit::Normal {
font_scale_1000ths_percent,
line_spacing_reduction_1000ths_percent,
} => {
let mut element = BytesStart::new("a:normAutofit");
if let Some(scale) = font_scale_1000ths_percent {
element.push_attribute(("fontScale", scale.to_string().as_str()));
}
if let Some(reduction) = line_spacing_reduction_1000ths_percent {
element.push_attribute(("lnSpcReduction", reduction.to_string().as_str()));
}
writer.write_event(Event::Empty(element))?;
}
}
writer.write_event(Event::End(BytesEnd::new("a:bodyPr")))?;
Ok(())
}
pub fn write_text_paragraph<W: Write>(
writer: &mut Writer<W>,
paragraph: &TextParagraph,
) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("a:p")))?;
if let Some(properties) = ¶graph.properties {
write_text_paragraph_properties(writer, properties)?;
}
for run in ¶graph.runs {
write_text_run(writer, run)?;
}
writer.write_event(Event::End(BytesEnd::new("a:p")))?;
Ok(())
}
pub fn write_text_paragraph_properties<W: Write>(
writer: &mut Writer<W>,
properties: &TextParagraphProperties,
) -> Result<()> {
let mut start = BytesStart::new("a:pPr");
if let Some(margin) = properties.margin_left_emu {
start.push_attribute(("marL", margin.to_string().as_str()));
}
if let Some(margin) = properties.margin_right_emu {
start.push_attribute(("marR", margin.to_string().as_str()));
}
if let Some(level) = properties.level {
start.push_attribute(("lvl", level.to_string().as_str()));
}
if let Some(indent) = properties.indent_emu {
start.push_attribute(("indent", indent.to_string().as_str()));
}
if let Some(alignment) = &properties.alignment {
start.push_attribute(("algn", alignment.xml_token()));
}
let has_children = properties.line_spacing.is_some()
|| properties.space_before.is_some()
|| properties.space_after.is_some()
|| properties.bullet_color.is_some()
|| properties.bullet_size.is_some()
|| properties.bullet_font.is_some()
|| properties.bullet.is_some()
|| !properties.tab_stops.is_empty()
|| properties.default_run_properties.is_some();
if !has_children {
writer.write_event(Event::Empty(start))?;
return Ok(());
}
writer.write_event(Event::Start(start))?;
if let Some(spacing) = &properties.line_spacing {
write_text_spacing(writer, "a:lnSpc", spacing)?;
}
if let Some(spacing) = &properties.space_before {
write_text_spacing(writer, "a:spcBef", spacing)?;
}
if let Some(spacing) = &properties.space_after {
write_text_spacing(writer, "a:spcAft", spacing)?;
}
if let Some(color) = &properties.bullet_color {
match color {
BulletColor::FollowText => {
writer.write_event(Event::Empty(BytesStart::new("a:buClrTx")))?
}
BulletColor::Explicit(color) => {
writer.write_event(Event::Start(BytesStart::new("a:buClr")))?;
write_color(writer, color)?;
writer.write_event(Event::End(BytesEnd::new("a:buClr")))?;
}
}
}
if let Some(size) = &properties.bullet_size {
match size {
BulletSize::FollowText => {
writer.write_event(Event::Empty(BytesStart::new("a:buSzTx")))?
}
BulletSize::Percent(value) => {
let mut element = BytesStart::new("a:buSzPct");
element.push_attribute(("val", value.to_string().as_str()));
writer.write_event(Event::Empty(element))?;
}
BulletSize::Points(value) => {
let mut element = BytesStart::new("a:buSzPts");
element.push_attribute(("val", value.to_string().as_str()));
writer.write_event(Event::Empty(element))?;
}
}
}
if let Some(font) = &properties.bullet_font {
match font {
BulletFont::FollowText => {
writer.write_event(Event::Empty(BytesStart::new("a:buFontTx")))?
}
BulletFont::Typeface(typeface) => {
let mut element = BytesStart::new("a:buFont");
element.push_attribute(("typeface", typeface.as_str()));
writer.write_event(Event::Empty(element))?;
}
}
}
if let Some(bullet) = &properties.bullet {
match bullet {
BulletKind::None => writer.write_event(Event::Empty(BytesStart::new("a:buNone")))?,
BulletKind::Character(character) => {
let mut element = BytesStart::new("a:buChar");
element.push_attribute(("char", character.as_str()));
writer.write_event(Event::Empty(element))?;
}
BulletKind::AutoNumber { scheme, start_at } => {
let mut element = BytesStart::new("a:buAutoNum");
element.push_attribute(("type", scheme.as_str()));
if let Some(start_at) = start_at {
element.push_attribute(("startAt", start_at.to_string().as_str()));
}
writer.write_event(Event::Empty(element))?;
}
}
}
if !properties.tab_stops.is_empty() {
writer.write_event(Event::Start(BytesStart::new("a:tabLst")))?;
for tab_stop in &properties.tab_stops {
let mut tab = BytesStart::new("a:tab");
tab.push_attribute(("pos", tab_stop.position_emu.to_string().as_str()));
if let Some(alignment) = &tab_stop.alignment {
tab.push_attribute(("algn", alignment.xml_token()));
}
writer.write_event(Event::Empty(tab))?;
}
writer.write_event(Event::End(BytesEnd::new("a:tabLst")))?;
}
if properties.default_run_properties.is_some() {
write_text_run_properties(
writer,
"a:defRPr",
properties.default_run_properties.as_deref(),
)?;
}
writer.write_event(Event::End(BytesEnd::new("a:pPr")))?;
Ok(())
}
fn write_text_spacing<W: Write>(
writer: &mut Writer<W>,
element_name: &str,
spacing: &TextSpacing,
) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new(element_name)))?;
let (child_name, value) = match spacing {
TextSpacing::Percent(value) => ("a:spcPct", *value),
TextSpacing::Points(value) => ("a:spcPts", *value),
};
let mut child = BytesStart::new(child_name);
child.push_attribute(("val", value.to_string().as_str()));
writer.write_event(Event::Empty(child))?;
writer.write_event(Event::End(BytesEnd::new(element_name)))?;
Ok(())
}
pub fn write_text_run<W: Write>(writer: &mut Writer<W>, run: &TextRun) -> Result<()> {
match run {
TextRun::Regular { text, properties } => {
writer.write_event(Event::Start(BytesStart::new("a:r")))?;
write_text_run_properties(writer, "a:rPr", Some(properties))?;
writer.write_event(Event::Start(BytesStart::new("a:t")))?;
writer.write_event(Event::Text(BytesText::new(text)))?;
writer.write_event(Event::End(BytesEnd::new("a:t")))?;
writer.write_event(Event::End(BytesEnd::new("a:r")))?;
}
TextRun::LineBreak { properties } => {
if properties.is_none() {
writer.write_event(Event::Empty(BytesStart::new("a:br")))?;
} else {
writer.write_event(Event::Start(BytesStart::new("a:br")))?;
write_text_run_properties(writer, "a:rPr", properties.as_ref())?;
writer.write_event(Event::End(BytesEnd::new("a:br")))?;
}
}
TextRun::Field {
id,
field_type,
cached_text,
properties,
} => {
validate_field_id(id)?;
let mut start = BytesStart::new("a:fld");
start.push_attribute(("id", id.as_str()));
if let Some(field_type) = field_type {
start.push_attribute(("type", field_type.as_str()));
}
writer.write_event(Event::Start(start))?;
write_text_run_properties(writer, "a:rPr", Some(properties))?;
writer.write_event(Event::Start(BytesStart::new("a:t")))?;
writer.write_event(Event::Text(BytesText::new(cached_text)))?;
writer.write_event(Event::End(BytesEnd::new("a:t")))?;
writer.write_event(Event::End(BytesEnd::new("a:fld")))?;
}
}
Ok(())
}
fn write_text_run_properties<W: Write>(
writer: &mut Writer<W>,
element_name: &str,
properties: Option<&TextRunProperties>,
) -> Result<()> {
let Some(properties) = properties else {
return Ok(());
};
let mut start = BytesStart::new(element_name);
if properties.bold {
start.push_attribute(("b", "1"));
}
if properties.italic {
start.push_attribute(("i", "1"));
}
if let Some(underline) = &properties.underline {
start.push_attribute(("u", underline.xml_token()));
}
if let Some(strike) = &properties.strike {
start.push_attribute(("strike", strike.xml_token()));
}
if let Some(size) = properties.font_size_100ths_point {
start.push_attribute(("sz", size.to_string().as_str()));
}
if let Some(caps) = &properties.text_caps {
start.push_attribute(("cap", caps.xml_token()));
}
if let Some(spacing) = properties.character_spacing_100ths_point {
start.push_attribute(("spc", spacing.to_string().as_str()));
}
if let Some(baseline) = properties.baseline_1000ths_percent {
start.push_attribute(("baseline", baseline.to_string().as_str()));
}
let has_children = properties.fill.is_some()
|| properties.highlight.is_some()
|| properties.font_family.is_some()
|| properties.hyperlink.is_some();
if !has_children {
writer.write_event(Event::Empty(start))?;
return Ok(());
}
writer.write_event(Event::Start(start))?;
if let Some(fill) = &properties.fill {
write_fill(writer, fill)?;
}
if let Some(highlight) = &properties.highlight {
writer.write_event(Event::Start(BytesStart::new("a:highlight")))?;
write_color(writer, highlight)?;
writer.write_event(Event::End(BytesEnd::new("a:highlight")))?;
}
if let Some(font_family) = &properties.font_family {
let mut latin = BytesStart::new("a:latin");
latin.push_attribute(("typeface", font_family.as_str()));
writer.write_event(Event::Empty(latin))?;
}
if let Some(hyperlink) = &properties.hyperlink {
write_hyperlink(writer, "a:hlinkClick", hyperlink)?;
}
writer.write_event(Event::End(BytesEnd::new(element_name)))?;
Ok(())
}
fn write_hyperlink<W: Write>(
writer: &mut Writer<W>,
element_name: &str,
hyperlink: &crate::model::Hyperlink,
) -> Result<()> {
let mut start = BytesStart::new(element_name);
if let Some(relationship_id) = &hyperlink.relationship_id {
start.push_attribute(("r:id", relationship_id.as_str()));
}
if let Some(tooltip) = &hyperlink.tooltip {
start.push_attribute(("tooltip", tooltip.as_str()));
}
writer.write_event(Event::Empty(start))?;
Ok(())
}