1use std::io::Write;
12
13use xml_core::{BytesEnd, BytesStart, BytesText, Event, Writer};
14
15use crate::error::Result;
16use crate::model::{
17 BlipFill, BlipFillMode, BulletColor, BulletFont, BulletKind, BulletSize, Color, CropRect,
18 CustomGeometry, EffectList, Fill, Geometry, GeometryAdjustment, GradientFill, GradientStop,
19 Line, LineEnd, LineJoin, PathCommand, PatternFill, ShapeProperties, TextAutofit, TextBody,
20 TextBodyProperties, TextParagraph, TextParagraphProperties, TextRun, TextRunProperties,
21 TextSpacing, Transform2D,
22};
23
24fn validate_field_id(id: &str) -> Result<()> {
31 let bytes = id.as_bytes();
32 let is_hex_run = |s: &[u8], len: usize| s.len() == len && s.iter().all(u8::is_ascii_hexdigit);
33 let valid = bytes.len() == 38
34 && bytes[0] == b'{'
35 && bytes[37] == b'}'
36 && bytes[9] == b'-'
37 && bytes[14] == b'-'
38 && bytes[19] == b'-'
39 && bytes[24] == b'-'
40 && is_hex_run(&bytes[1..9], 8)
41 && is_hex_run(&bytes[10..14], 4)
42 && is_hex_run(&bytes[15..19], 4)
43 && is_hex_run(&bytes[20..24], 4)
44 && is_hex_run(&bytes[25..37], 12)
45 && id.chars().all(|c| !c.is_ascii_hexdigit() || c.is_ascii_digit() || c.is_ascii_uppercase());
48 if valid {
49 Ok(())
50 } else {
51 Err(crate::error::Error::InvalidFieldId(id.to_string()))
52 }
53}
54
55pub fn write_shape_properties<W: Write>(
58 writer: &mut Writer<W>,
59 properties: &ShapeProperties,
60) -> Result<()> {
61 if let Some(transform) = &properties.transform {
62 write_transform(writer, transform)?;
63 }
64 if let Some(geometry) = &properties.geometry {
65 write_geometry(writer, geometry, &properties.geometry_adjustments)?;
66 }
67 if let Some(fill) = &properties.fill {
68 write_fill(writer, fill)?;
69 }
70 if let Some(line) = &properties.line {
71 write_line(writer, line)?;
72 }
73 if let Some(effects) = &properties.effects {
74 write_effect_list(writer, effects)?;
75 }
76 Ok(())
77}
78
79pub fn write_geometry_fill_line_effects<W: Write>(
98 writer: &mut Writer<W>,
99 properties: &ShapeProperties,
100 default_preset: &str,
101) -> Result<()> {
102 match &properties.geometry {
103 Some(geometry) => write_geometry(writer, geometry, &properties.geometry_adjustments)?,
104 None => {
105 let mut prst_geom = BytesStart::new("a:prstGeom");
106 prst_geom.push_attribute(("prst", default_preset));
107 writer.write_event(Event::Start(prst_geom))?;
108 writer.write_event(Event::Empty(BytesStart::new("a:avLst")))?;
109 writer.write_event(Event::End(BytesEnd::new("a:prstGeom")))?;
110 }
111 }
112 if let Some(fill) = &properties.fill {
113 write_fill(writer, fill)?;
114 }
115 if let Some(line) = &properties.line {
116 write_line(writer, line)?;
117 }
118 if let Some(effects) = &properties.effects {
119 write_effect_list(writer, effects)?;
120 }
121 Ok(())
122}
123
124pub fn write_effect_list<W: Write>(writer: &mut Writer<W>, effects: &EffectList) -> Result<()> {
129 let has_children = effects.outer_shadow.is_some()
130 || effects.glow.is_some()
131 || effects.reflection.is_some()
132 || effects.soft_edge.is_some();
133 if !has_children {
134 writer.write_event(Event::Empty(BytesStart::new("a:effectLst")))?;
135 return Ok(());
136 }
137
138 writer.write_event(Event::Start(BytesStart::new("a:effectLst")))?;
139 if let Some(shadow) = &effects.outer_shadow {
140 let mut start = BytesStart::new("a:outerShdw");
141 if let Some(blur) = shadow.blur_radius_emu {
142 start.push_attribute(("blurRad", blur.to_string().as_str()));
143 }
144 if let Some(distance) = shadow.distance_emu {
145 start.push_attribute(("dist", distance.to_string().as_str()));
146 }
147 if let Some(direction) = shadow.direction_60000ths {
148 start.push_attribute(("dir", direction.to_string().as_str()));
149 }
150 writer.write_event(Event::Start(start))?;
151 write_color(writer, &shadow.color)?;
152 writer.write_event(Event::End(BytesEnd::new("a:outerShdw")))?;
153 }
154 if let Some(glow) = &effects.glow {
155 let mut start = BytesStart::new("a:glow");
156 start.push_attribute(("rad", glow.radius_emu.to_string().as_str()));
157 writer.write_event(Event::Start(start))?;
158 write_color(writer, &glow.color)?;
159 writer.write_event(Event::End(BytesEnd::new("a:glow")))?;
160 }
161 if let Some(reflection) = &effects.reflection {
162 let mut start = BytesStart::new("a:reflection");
163 if let Some(blur) = reflection.blur_radius_emu {
164 start.push_attribute(("blurRad", blur.to_string().as_str()));
165 }
166 if let Some(distance) = reflection.distance_emu {
167 start.push_attribute(("dist", distance.to_string().as_str()));
168 }
169 if let Some(direction) = reflection.direction_60000ths {
170 start.push_attribute(("dir", direction.to_string().as_str()));
171 }
172 if let Some(start_alpha) = reflection.start_alpha_1000ths_percent {
173 start.push_attribute(("stA", start_alpha.to_string().as_str()));
174 }
175 if let Some(end_alpha) = reflection.end_alpha_1000ths_percent {
176 start.push_attribute(("endA", end_alpha.to_string().as_str()));
177 }
178 writer.write_event(Event::Empty(start))?;
179 }
180 if let Some(soft_edge) = &effects.soft_edge {
181 let mut start = BytesStart::new("a:softEdge");
182 start.push_attribute(("rad", soft_edge.radius_emu.to_string().as_str()));
183 writer.write_event(Event::Empty(start))?;
184 }
185 writer.write_event(Event::End(BytesEnd::new("a:effectLst")))?;
186 Ok(())
187}
188
189pub fn write_transform<W: Write>(writer: &mut Writer<W>, transform: &Transform2D) -> Result<()> {
191 let mut start = BytesStart::new("a:xfrm");
192 if transform.rotation_60000ths != 0 {
193 start.push_attribute(("rot", transform.rotation_60000ths.to_string().as_str()));
194 }
195 if transform.flip_horizontal {
196 start.push_attribute(("flipH", "1"));
197 }
198 if transform.flip_vertical {
199 start.push_attribute(("flipV", "1"));
200 }
201
202 if transform.offset.is_none() && transform.extent.is_none() {
203 writer.write_event(Event::Empty(start))?;
204 return Ok(());
205 }
206
207 writer.write_event(Event::Start(start))?;
208 if let Some((x, y)) = transform.offset {
209 let mut off = BytesStart::new("a:off");
210 off.push_attribute(("x", x.to_string().as_str()));
211 off.push_attribute(("y", y.to_string().as_str()));
212 writer.write_event(Event::Empty(off))?;
213 }
214 if let Some((cx, cy)) = transform.extent {
215 let mut ext = BytesStart::new("a:ext");
216 ext.push_attribute(("cx", cx.to_string().as_str()));
217 ext.push_attribute(("cy", cy.to_string().as_str()));
218 writer.write_event(Event::Empty(ext))?;
219 }
220 writer.write_event(Event::End(BytesEnd::new("a:xfrm")))?;
221 Ok(())
222}
223
224pub fn write_geometry<W: Write>(
234 writer: &mut Writer<W>,
235 geometry: &Geometry,
236 adjustments: &[GeometryAdjustment],
237) -> Result<()> {
238 match geometry {
239 Geometry::Preset(shape) => {
240 let mut start = BytesStart::new("a:prstGeom");
241 start.push_attribute(("prst", shape.xml_token()));
242 if adjustments.is_empty() {
243 writer.write_event(Event::Empty(start))?;
244 } else {
245 writer.write_event(Event::Start(start))?;
246 writer.write_event(Event::Start(BytesStart::new("a:avLst")))?;
247 for adjustment in adjustments {
248 let mut gd = BytesStart::new("a:gd");
249 gd.push_attribute(("name", adjustment.name.as_str()));
250 gd.push_attribute(("fmla", adjustment.formula.as_str()));
251 writer.write_event(Event::Empty(gd))?;
252 }
253 writer.write_event(Event::End(BytesEnd::new("a:avLst")))?;
254 writer.write_event(Event::End(BytesEnd::new("a:prstGeom")))?;
255 }
256 }
257 Geometry::Custom(custom) => write_custom_geometry(writer, custom)?,
258 }
259 Ok(())
260}
261
262fn write_custom_geometry<W: Write>(writer: &mut Writer<W>, custom: &CustomGeometry) -> Result<()> {
267 writer.write_event(Event::Start(BytesStart::new("a:custGeom")))?;
268 writer.write_event(Event::Start(BytesStart::new("a:pathLst")))?;
269
270 let mut path = BytesStart::new("a:path");
271 path.push_attribute(("w", custom.width_emu.to_string().as_str()));
272 path.push_attribute(("h", custom.height_emu.to_string().as_str()));
273 if custom.commands.is_empty() {
274 writer.write_event(Event::Empty(path))?;
275 } else {
276 writer.write_event(Event::Start(path))?;
277 for command in &custom.commands {
278 write_path_command(writer, command)?;
279 }
280 writer.write_event(Event::End(BytesEnd::new("a:path")))?;
281 }
282
283 writer.write_event(Event::End(BytesEnd::new("a:pathLst")))?;
284 writer.write_event(Event::End(BytesEnd::new("a:custGeom")))?;
285 Ok(())
286}
287
288fn write_path_command<W: Write>(writer: &mut Writer<W>, command: &PathCommand) -> Result<()> {
289 match command {
290 PathCommand::MoveTo { x, y } => {
291 writer.write_event(Event::Start(BytesStart::new("a:moveTo")))?;
292 write_path_point(writer, *x, *y)?;
293 writer.write_event(Event::End(BytesEnd::new("a:moveTo")))?;
294 }
295 PathCommand::LineTo { x, y } => {
296 writer.write_event(Event::Start(BytesStart::new("a:lnTo")))?;
297 write_path_point(writer, *x, *y)?;
298 writer.write_event(Event::End(BytesEnd::new("a:lnTo")))?;
299 }
300 PathCommand::CubicBezierTo {
301 x1,
302 y1,
303 x2,
304 y2,
305 x,
306 y,
307 } => {
308 writer.write_event(Event::Start(BytesStart::new("a:cubicBezTo")))?;
309 write_path_point(writer, *x1, *y1)?;
310 write_path_point(writer, *x2, *y2)?;
311 write_path_point(writer, *x, *y)?;
312 writer.write_event(Event::End(BytesEnd::new("a:cubicBezTo")))?;
313 }
314 PathCommand::Close => {
315 writer.write_event(Event::Empty(BytesStart::new("a:close")))?;
316 }
317 }
318 Ok(())
319}
320
321fn write_path_point<W: Write>(writer: &mut Writer<W>, x: i64, y: i64) -> Result<()> {
322 let mut pt = BytesStart::new("a:pt");
323 pt.push_attribute(("x", x.to_string().as_str()));
324 pt.push_attribute(("y", y.to_string().as_str()));
325 writer.write_event(Event::Empty(pt))?;
326 Ok(())
327}
328
329pub fn write_fill<W: Write>(writer: &mut Writer<W>, fill: &Fill) -> Result<()> {
332 match fill {
333 Fill::None => writer.write_event(Event::Empty(BytesStart::new("a:noFill")))?,
334 Fill::Solid(color) => {
335 writer.write_event(Event::Start(BytesStart::new("a:solidFill")))?;
336 write_color(writer, color)?;
337 writer.write_event(Event::End(BytesEnd::new("a:solidFill")))?;
338 }
339 Fill::Gradient(gradient) => write_gradient_fill(writer, gradient)?,
340 Fill::Pattern(pattern) => write_pattern_fill(writer, pattern)?,
341 Fill::Image(blip_fill) => write_blip_fill(writer, blip_fill)?,
342 }
343 Ok(())
344}
345
346pub fn write_blip_fill<W: Write>(writer: &mut Writer<W>, blip_fill: &BlipFill) -> Result<()> {
349 let has_children =
350 blip_fill.relationship_id.is_some() || blip_fill.crop.is_some() || blip_fill.mode.is_some();
351 if !has_children {
352 writer.write_event(Event::Empty(BytesStart::new("a:blipFill")))?;
353 return Ok(());
354 }
355
356 writer.write_event(Event::Start(BytesStart::new("a:blipFill")))?;
357 if let Some(relationship_id) = &blip_fill.relationship_id {
358 let mut blip = BytesStart::new("a:blip");
359 blip.push_attribute(("r:embed", relationship_id.as_str()));
360 writer.write_event(Event::Empty(blip))?;
361 }
362 if let Some(crop) = &blip_fill.crop {
363 write_crop_rect(writer, crop)?;
364 }
365 if let Some(mode) = &blip_fill.mode {
366 let element_name = match mode {
367 BlipFillMode::Stretch => "a:stretch",
368 BlipFillMode::Tile => "a:tile",
369 };
370 writer.write_event(Event::Empty(BytesStart::new(element_name)))?;
371 }
372 writer.write_event(Event::End(BytesEnd::new("a:blipFill")))?;
373 Ok(())
374}
375
376fn write_crop_rect<W: Write>(writer: &mut Writer<W>, crop: &CropRect) -> Result<()> {
378 let mut start = BytesStart::new("a:srcRect");
379 if let Some(left) = crop.left_1000ths_percent {
380 start.push_attribute(("l", left.to_string().as_str()));
381 }
382 if let Some(top) = crop.top_1000ths_percent {
383 start.push_attribute(("t", top.to_string().as_str()));
384 }
385 if let Some(right) = crop.right_1000ths_percent {
386 start.push_attribute(("r", right.to_string().as_str()));
387 }
388 if let Some(bottom) = crop.bottom_1000ths_percent {
389 start.push_attribute(("b", bottom.to_string().as_str()));
390 }
391 writer.write_event(Event::Empty(start))?;
392 Ok(())
393}
394
395fn write_gradient_fill<W: Write>(writer: &mut Writer<W>, gradient: &GradientFill) -> Result<()> {
396 writer.write_event(Event::Start(BytesStart::new("a:gradFill")))?;
397
398 if !gradient.stops.is_empty() {
399 writer.write_event(Event::Start(BytesStart::new("a:gsLst")))?;
400 for stop in &gradient.stops {
401 write_gradient_stop(writer, stop)?;
402 }
403 writer.write_event(Event::End(BytesEnd::new("a:gsLst")))?;
404 }
405
406 if let Some(angle) = gradient.angle_60000ths {
407 let mut lin = BytesStart::new("a:lin");
408 lin.push_attribute(("ang", angle.to_string().as_str()));
409 writer.write_event(Event::Empty(lin))?;
410 }
411
412 writer.write_event(Event::End(BytesEnd::new("a:gradFill")))?;
413 Ok(())
414}
415
416fn write_gradient_stop<W: Write>(writer: &mut Writer<W>, stop: &GradientStop) -> Result<()> {
417 let mut start = BytesStart::new("a:gs");
418 start.push_attribute(("pos", stop.position_1000ths_percent.to_string().as_str()));
419 writer.write_event(Event::Start(start))?;
420 write_color(writer, &stop.color)?;
421 writer.write_event(Event::End(BytesEnd::new("a:gs")))?;
422 Ok(())
423}
424
425fn write_pattern_fill<W: Write>(writer: &mut Writer<W>, pattern: &PatternFill) -> Result<()> {
426 let mut start = BytesStart::new("a:pattFill");
427 start.push_attribute(("prst", pattern.preset.xml_token()));
428
429 if pattern.foreground.is_none() && pattern.background.is_none() {
430 writer.write_event(Event::Empty(start))?;
431 return Ok(());
432 }
433
434 writer.write_event(Event::Start(start))?;
435 if let Some(color) = &pattern.foreground {
436 writer.write_event(Event::Start(BytesStart::new("a:fgClr")))?;
437 write_color(writer, color)?;
438 writer.write_event(Event::End(BytesEnd::new("a:fgClr")))?;
439 }
440 if let Some(color) = &pattern.background {
441 writer.write_event(Event::Start(BytesStart::new("a:bgClr")))?;
442 write_color(writer, color)?;
443 writer.write_event(Event::End(BytesEnd::new("a:bgClr")))?;
444 }
445 writer.write_event(Event::End(BytesEnd::new("a:pattFill")))?;
446 Ok(())
447}
448
449pub fn write_color<W: Write>(writer: &mut Writer<W>, color: &Color) -> Result<()> {
453 match color {
454 Color::Rgb(hex) => {
455 let mut start = BytesStart::new("a:srgbClr");
456 start.push_attribute(("val", hex.as_str()));
457 writer.write_event(Event::Empty(start))?;
458 }
459 Color::RgbPercent { red, green, blue } => {
460 let mut start = BytesStart::new("a:scrgbClr");
461 start.push_attribute(("r", red.to_string().as_str()));
462 start.push_attribute(("g", green.to_string().as_str()));
463 start.push_attribute(("b", blue.to_string().as_str()));
464 writer.write_event(Event::Empty(start))?;
465 }
466 Color::Hsl {
467 hue_60000ths,
468 saturation_1000ths_percent,
469 luminance_1000ths_percent,
470 } => {
471 let mut start = BytesStart::new("a:hslClr");
472 start.push_attribute(("hue", hue_60000ths.to_string().as_str()));
473 start.push_attribute(("sat", saturation_1000ths_percent.to_string().as_str()));
474 start.push_attribute(("lum", luminance_1000ths_percent.to_string().as_str()));
475 writer.write_event(Event::Empty(start))?;
476 }
477 Color::System { value, last_color } => {
478 let mut start = BytesStart::new("a:sysClr");
479 start.push_attribute(("val", value.as_str()));
480 if let Some(last_color) = last_color {
481 start.push_attribute(("lastClr", last_color.as_str()));
482 }
483 writer.write_event(Event::Empty(start))?;
484 }
485 Color::Preset(name) => {
486 let mut start = BytesStart::new("a:prstClr");
487 start.push_attribute(("val", name.as_str()));
488 writer.write_event(Event::Empty(start))?;
489 }
490 }
491 Ok(())
492}
493
494pub fn write_line<W: Write>(writer: &mut Writer<W>, line: &Line) -> Result<()> {
497 write_line_named(writer, "a:ln", line)
498}
499
500pub fn write_line_named<W: Write>(
506 writer: &mut Writer<W>,
507 element_name: &str,
508 line: &Line,
509) -> Result<()> {
510 let mut start = BytesStart::new(element_name);
511 if let Some(width) = line.width_emu {
512 start.push_attribute(("w", width.to_string().as_str()));
513 }
514 if let Some(cap) = &line.cap {
515 start.push_attribute(("cap", cap.xml_token()));
516 }
517 if let Some(compound) = &line.compound {
518 start.push_attribute(("cmpd", compound.xml_token()));
519 }
520
521 let has_children = line.fill.is_some()
522 || line.dash.is_some()
523 || line.join.is_some()
524 || line.head_end.is_some()
525 || line.tail_end.is_some();
526 if !has_children {
527 writer.write_event(Event::Empty(start))?;
528 return Ok(());
529 }
530
531 writer.write_event(Event::Start(start))?;
532 if let Some(fill) = &line.fill {
533 write_fill(writer, fill)?;
534 }
535 if let Some(dash) = &line.dash {
536 let mut prst_dash = BytesStart::new("a:prstDash");
537 prst_dash.push_attribute(("val", dash.xml_token()));
538 writer.write_event(Event::Empty(prst_dash))?;
539 }
540 if let Some(join) = &line.join {
541 write_line_join(writer, join)?;
542 }
543 if let Some(head_end) = &line.head_end {
544 write_line_end(writer, "a:headEnd", head_end)?;
545 }
546 if let Some(tail_end) = &line.tail_end {
547 write_line_end(writer, "a:tailEnd", tail_end)?;
548 }
549 writer.write_event(Event::End(BytesEnd::new(element_name)))?;
550 Ok(())
551}
552
553fn write_line_join<W: Write>(writer: &mut Writer<W>, join: &LineJoin) -> Result<()> {
554 match join {
555 LineJoin::Round => writer.write_event(Event::Empty(BytesStart::new("a:round")))?,
556 LineJoin::Bevel => writer.write_event(Event::Empty(BytesStart::new("a:bevel")))?,
557 LineJoin::Miter {
558 limit_1000ths_percent,
559 } => {
560 let mut start = BytesStart::new("a:miter");
561 if let Some(limit) = limit_1000ths_percent {
562 start.push_attribute(("lim", limit.to_string().as_str()));
563 }
564 writer.write_event(Event::Empty(start))?;
565 }
566 }
567 Ok(())
568}
569
570fn write_line_end<W: Write>(
571 writer: &mut Writer<W>,
572 element_name: &str,
573 end: &LineEnd,
574) -> Result<()> {
575 let mut start = BytesStart::new(element_name);
576 if let Some(kind) = &end.kind {
577 start.push_attribute(("type", kind.xml_token()));
578 }
579 if let Some(width) = &end.width {
580 start.push_attribute(("w", width.xml_token()));
581 }
582 if let Some(length) = &end.length {
583 start.push_attribute(("len", length.xml_token()));
584 }
585 writer.write_event(Event::Empty(start))?;
586 Ok(())
587}
588
589pub fn write_text_body<W: Write>(writer: &mut Writer<W>, body: &TextBody) -> Result<()> {
598 write_text_body_properties(writer, &body.properties)?;
599 for paragraph in &body.paragraphs {
600 write_text_paragraph(writer, paragraph)?;
601 }
602 Ok(())
603}
604
605pub fn write_text_body_properties<W: Write>(
609 writer: &mut Writer<W>,
610 properties: &TextBodyProperties,
611) -> Result<()> {
612 let mut start = BytesStart::new("a:bodyPr");
613 if properties.rotation_60000ths != 0 {
614 start.push_attribute(("rot", properties.rotation_60000ths.to_string().as_str()));
615 }
616 if let Some(vertical_direction) = &properties.vertical_direction {
617 start.push_attribute(("vert", vertical_direction.xml_token()));
618 }
619 if let Some(wrap) = &properties.wrap {
620 start.push_attribute(("wrap", wrap.xml_token()));
621 }
622 if let Some(anchor) = &properties.anchor {
623 start.push_attribute(("anchor", anchor.xml_token()));
624 }
625 if properties.anchor_center {
626 start.push_attribute(("anchorCtr", "1"));
627 }
628 if let Some(inset) = properties.inset_left_emu {
629 start.push_attribute(("lIns", inset.to_string().as_str()));
630 }
631 if let Some(inset) = properties.inset_top_emu {
632 start.push_attribute(("tIns", inset.to_string().as_str()));
633 }
634 if let Some(inset) = properties.inset_right_emu {
635 start.push_attribute(("rIns", inset.to_string().as_str()));
636 }
637 if let Some(inset) = properties.inset_bottom_emu {
638 start.push_attribute(("bIns", inset.to_string().as_str()));
639 }
640
641 let Some(autofit) = &properties.autofit else {
642 writer.write_event(Event::Empty(start))?;
643 return Ok(());
644 };
645
646 writer.write_event(Event::Start(start))?;
647 match autofit {
648 TextAutofit::None => writer.write_event(Event::Empty(BytesStart::new("a:noAutofit")))?,
649 TextAutofit::Shape => writer.write_event(Event::Empty(BytesStart::new("a:spAutoFit")))?,
650 TextAutofit::Normal {
651 font_scale_1000ths_percent,
652 line_spacing_reduction_1000ths_percent,
653 } => {
654 let mut element = BytesStart::new("a:normAutofit");
655 if let Some(scale) = font_scale_1000ths_percent {
656 element.push_attribute(("fontScale", scale.to_string().as_str()));
657 }
658 if let Some(reduction) = line_spacing_reduction_1000ths_percent {
659 element.push_attribute(("lnSpcReduction", reduction.to_string().as_str()));
660 }
661 writer.write_event(Event::Empty(element))?;
662 }
663 }
664 writer.write_event(Event::End(BytesEnd::new("a:bodyPr")))?;
665 Ok(())
666}
667
668pub fn write_text_paragraph<W: Write>(
670 writer: &mut Writer<W>,
671 paragraph: &TextParagraph,
672) -> Result<()> {
673 writer.write_event(Event::Start(BytesStart::new("a:p")))?;
674 if let Some(properties) = ¶graph.properties {
675 write_text_paragraph_properties(writer, properties)?;
676 }
677 for run in ¶graph.runs {
678 write_text_run(writer, run)?;
679 }
680 writer.write_event(Event::End(BytesEnd::new("a:p")))?;
681 Ok(())
682}
683
684pub fn write_text_paragraph_properties<W: Write>(
690 writer: &mut Writer<W>,
691 properties: &TextParagraphProperties,
692) -> Result<()> {
693 let mut start = BytesStart::new("a:pPr");
694 if let Some(margin) = properties.margin_left_emu {
695 start.push_attribute(("marL", margin.to_string().as_str()));
696 }
697 if let Some(margin) = properties.margin_right_emu {
698 start.push_attribute(("marR", margin.to_string().as_str()));
699 }
700 if let Some(level) = properties.level {
701 start.push_attribute(("lvl", level.to_string().as_str()));
702 }
703 if let Some(indent) = properties.indent_emu {
704 start.push_attribute(("indent", indent.to_string().as_str()));
705 }
706 if let Some(alignment) = &properties.alignment {
707 start.push_attribute(("algn", alignment.xml_token()));
708 }
709
710 let has_children = properties.line_spacing.is_some()
711 || properties.space_before.is_some()
712 || properties.space_after.is_some()
713 || properties.bullet_color.is_some()
714 || properties.bullet_size.is_some()
715 || properties.bullet_font.is_some()
716 || properties.bullet.is_some()
717 || !properties.tab_stops.is_empty()
718 || properties.default_run_properties.is_some();
719 if !has_children {
720 writer.write_event(Event::Empty(start))?;
721 return Ok(());
722 }
723
724 writer.write_event(Event::Start(start))?;
725 if let Some(spacing) = &properties.line_spacing {
726 write_text_spacing(writer, "a:lnSpc", spacing)?;
727 }
728 if let Some(spacing) = &properties.space_before {
729 write_text_spacing(writer, "a:spcBef", spacing)?;
730 }
731 if let Some(spacing) = &properties.space_after {
732 write_text_spacing(writer, "a:spcAft", spacing)?;
733 }
734 if let Some(color) = &properties.bullet_color {
735 match color {
736 BulletColor::FollowText => {
737 writer.write_event(Event::Empty(BytesStart::new("a:buClrTx")))?
738 }
739 BulletColor::Explicit(color) => {
740 writer.write_event(Event::Start(BytesStart::new("a:buClr")))?;
741 write_color(writer, color)?;
742 writer.write_event(Event::End(BytesEnd::new("a:buClr")))?;
743 }
744 }
745 }
746 if let Some(size) = &properties.bullet_size {
747 match size {
748 BulletSize::FollowText => {
749 writer.write_event(Event::Empty(BytesStart::new("a:buSzTx")))?
750 }
751 BulletSize::Percent(value) => {
752 let mut element = BytesStart::new("a:buSzPct");
753 element.push_attribute(("val", value.to_string().as_str()));
754 writer.write_event(Event::Empty(element))?;
755 }
756 BulletSize::Points(value) => {
757 let mut element = BytesStart::new("a:buSzPts");
758 element.push_attribute(("val", value.to_string().as_str()));
759 writer.write_event(Event::Empty(element))?;
760 }
761 }
762 }
763 if let Some(font) = &properties.bullet_font {
764 match font {
765 BulletFont::FollowText => {
766 writer.write_event(Event::Empty(BytesStart::new("a:buFontTx")))?
767 }
768 BulletFont::Typeface(typeface) => {
769 let mut element = BytesStart::new("a:buFont");
770 element.push_attribute(("typeface", typeface.as_str()));
771 writer.write_event(Event::Empty(element))?;
772 }
773 }
774 }
775 if let Some(bullet) = &properties.bullet {
776 match bullet {
777 BulletKind::None => writer.write_event(Event::Empty(BytesStart::new("a:buNone")))?,
778 BulletKind::Character(character) => {
779 let mut element = BytesStart::new("a:buChar");
780 element.push_attribute(("char", character.as_str()));
781 writer.write_event(Event::Empty(element))?;
782 }
783 BulletKind::AutoNumber { scheme, start_at } => {
784 let mut element = BytesStart::new("a:buAutoNum");
785 element.push_attribute(("type", scheme.as_str()));
786 if let Some(start_at) = start_at {
787 element.push_attribute(("startAt", start_at.to_string().as_str()));
788 }
789 writer.write_event(Event::Empty(element))?;
790 }
791 }
792 }
793 if !properties.tab_stops.is_empty() {
794 writer.write_event(Event::Start(BytesStart::new("a:tabLst")))?;
795 for tab_stop in &properties.tab_stops {
796 let mut tab = BytesStart::new("a:tab");
797 tab.push_attribute(("pos", tab_stop.position_emu.to_string().as_str()));
798 if let Some(alignment) = &tab_stop.alignment {
799 tab.push_attribute(("algn", alignment.xml_token()));
800 }
801 writer.write_event(Event::Empty(tab))?;
802 }
803 writer.write_event(Event::End(BytesEnd::new("a:tabLst")))?;
804 }
805 if properties.default_run_properties.is_some() {
806 write_text_run_properties(
807 writer,
808 "a:defRPr",
809 properties.default_run_properties.as_deref(),
810 )?;
811 }
812 writer.write_event(Event::End(BytesEnd::new("a:pPr")))?;
813 Ok(())
814}
815
816fn write_text_spacing<W: Write>(
819 writer: &mut Writer<W>,
820 element_name: &str,
821 spacing: &TextSpacing,
822) -> Result<()> {
823 writer.write_event(Event::Start(BytesStart::new(element_name)))?;
824 let (child_name, value) = match spacing {
825 TextSpacing::Percent(value) => ("a:spcPct", *value),
826 TextSpacing::Points(value) => ("a:spcPts", *value),
827 };
828 let mut child = BytesStart::new(child_name);
829 child.push_attribute(("val", value.to_string().as_str()));
830 writer.write_event(Event::Empty(child))?;
831 writer.write_event(Event::End(BytesEnd::new(element_name)))?;
832 Ok(())
833}
834
835pub fn write_text_run<W: Write>(writer: &mut Writer<W>, run: &TextRun) -> Result<()> {
838 match run {
839 TextRun::Regular { text, properties } => {
840 writer.write_event(Event::Start(BytesStart::new("a:r")))?;
841 write_text_run_properties(writer, "a:rPr", Some(properties))?;
842 writer.write_event(Event::Start(BytesStart::new("a:t")))?;
843 writer.write_event(Event::Text(BytesText::new(text)))?;
844 writer.write_event(Event::End(BytesEnd::new("a:t")))?;
845 writer.write_event(Event::End(BytesEnd::new("a:r")))?;
846 }
847 TextRun::LineBreak { properties } => {
848 if properties.is_none() {
849 writer.write_event(Event::Empty(BytesStart::new("a:br")))?;
850 } else {
851 writer.write_event(Event::Start(BytesStart::new("a:br")))?;
852 write_text_run_properties(writer, "a:rPr", properties.as_ref())?;
853 writer.write_event(Event::End(BytesEnd::new("a:br")))?;
854 }
855 }
856 TextRun::Field {
857 id,
858 field_type,
859 cached_text,
860 properties,
861 } => {
862 validate_field_id(id)?;
863 let mut start = BytesStart::new("a:fld");
864 start.push_attribute(("id", id.as_str()));
865 if let Some(field_type) = field_type {
866 start.push_attribute(("type", field_type.as_str()));
867 }
868 writer.write_event(Event::Start(start))?;
869 write_text_run_properties(writer, "a:rPr", Some(properties))?;
870 writer.write_event(Event::Start(BytesStart::new("a:t")))?;
871 writer.write_event(Event::Text(BytesText::new(cached_text)))?;
872 writer.write_event(Event::End(BytesEnd::new("a:t")))?;
873 writer.write_event(Event::End(BytesEnd::new("a:fld")))?;
874 }
875 }
876 Ok(())
877}
878
879fn write_text_run_properties<W: Write>(
885 writer: &mut Writer<W>,
886 element_name: &str,
887 properties: Option<&TextRunProperties>,
888) -> Result<()> {
889 let Some(properties) = properties else {
890 return Ok(());
891 };
892
893 let mut start = BytesStart::new(element_name);
894 if properties.bold {
895 start.push_attribute(("b", "1"));
896 }
897 if properties.italic {
898 start.push_attribute(("i", "1"));
899 }
900 if let Some(underline) = &properties.underline {
901 start.push_attribute(("u", underline.xml_token()));
902 }
903 if let Some(strike) = &properties.strike {
904 start.push_attribute(("strike", strike.xml_token()));
905 }
906 if let Some(size) = properties.font_size_100ths_point {
907 start.push_attribute(("sz", size.to_string().as_str()));
908 }
909 if let Some(caps) = &properties.text_caps {
910 start.push_attribute(("cap", caps.xml_token()));
911 }
912 if let Some(spacing) = properties.character_spacing_100ths_point {
913 start.push_attribute(("spc", spacing.to_string().as_str()));
914 }
915 if let Some(baseline) = properties.baseline_1000ths_percent {
916 start.push_attribute(("baseline", baseline.to_string().as_str()));
917 }
918
919 let has_children = properties.fill.is_some()
920 || properties.highlight.is_some()
921 || properties.font_family.is_some()
922 || properties.hyperlink.is_some();
923 if !has_children {
924 writer.write_event(Event::Empty(start))?;
925 return Ok(());
926 }
927
928 writer.write_event(Event::Start(start))?;
929 if let Some(fill) = &properties.fill {
930 write_fill(writer, fill)?;
931 }
932 if let Some(highlight) = &properties.highlight {
933 writer.write_event(Event::Start(BytesStart::new("a:highlight")))?;
934 write_color(writer, highlight)?;
935 writer.write_event(Event::End(BytesEnd::new("a:highlight")))?;
936 }
937 if let Some(font_family) = &properties.font_family {
938 let mut latin = BytesStart::new("a:latin");
939 latin.push_attribute(("typeface", font_family.as_str()));
940 writer.write_event(Event::Empty(latin))?;
941 }
942 if let Some(hyperlink) = &properties.hyperlink {
943 write_hyperlink(writer, "a:hlinkClick", hyperlink)?;
944 }
945 writer.write_event(Event::End(BytesEnd::new(element_name)))?;
946 Ok(())
947}
948
949fn write_hyperlink<W: Write>(
953 writer: &mut Writer<W>,
954 element_name: &str,
955 hyperlink: &crate::model::Hyperlink,
956) -> Result<()> {
957 let mut start = BytesStart::new(element_name);
958 if let Some(relationship_id) = &hyperlink.relationship_id {
959 start.push_attribute(("r:id", relationship_id.as_str()));
960 }
961 if let Some(tooltip) = &hyperlink.tooltip {
962 start.push_attribute(("tooltip", tooltip.as_str()));
963 }
964 writer.write_event(Event::Empty(start))?;
965 Ok(())
966}