1use std::fmt;
2use std::io::Write;
3
4use oxml_core::OxmlError;
5use oxml_core::raw_xml::{capture_element, capture_empty_element};
6use oxml_core::xml::{get_attr, local_name, matches_local_name};
7use quick_xml::events::{BytesEnd, BytesStart, Event};
8use quick_xml::{Reader, Writer, XmlVersion};
9
10use crate::fill::{Fill, FillError};
11use crate::order::OrderedRawChildren;
12
13const MAX_LINE_WIDTH_EMU: u32 = 20_116_800;
14
15#[derive(Debug)]
17pub enum LineError {
18 Xml(OxmlError),
19 Fill(FillError),
20 UnexpectedElement(String),
21 MissingAttribute {
22 element: String,
23 attribute: String,
24 },
25 InvalidAttribute {
26 element: String,
27 attribute: String,
28 value: String,
29 },
30 LineWidthOutOfRange(u32),
31}
32
33impl fmt::Display for LineError {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::Xml(error) => error.fmt(formatter),
37 Self::Fill(error) => error.fmt(formatter),
38 Self::UnexpectedElement(element) => {
39 write!(formatter, "unexpected DrawingML line element: {element}")
40 }
41 Self::MissingAttribute { element, attribute } => {
42 write!(formatter, "DrawingML {element} requires @{attribute}")
43 }
44 Self::InvalidAttribute {
45 element,
46 attribute,
47 value,
48 } => write!(
49 formatter,
50 "DrawingML {element} has invalid @{attribute}: {value}"
51 ),
52 Self::LineWidthOutOfRange(value) => write!(
53 formatter,
54 "DrawingML line width is outside 0 to {MAX_LINE_WIDTH_EMU}: {value}"
55 ),
56 }
57 }
58}
59
60impl std::error::Error for LineError {
61 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
62 match self {
63 Self::Xml(error) => Some(error),
64 Self::Fill(error) => Some(error),
65 _ => None,
66 }
67 }
68}
69
70impl From<OxmlError> for LineError {
71 fn from(error: OxmlError) -> Self {
72 Self::Xml(error)
73 }
74}
75
76impl From<FillError> for LineError {
77 fn from(error: FillError) -> Self {
78 Self::Fill(error)
79 }
80}
81
82pub type Result<T> = std::result::Result<T, LineError>;
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum LineCap {
87 Round,
88 Square,
89 Flat,
90}
91
92impl LineCap {
93 fn parse(value: &str) -> Option<Self> {
94 match value {
95 "rnd" => Some(Self::Round),
96 "sq" => Some(Self::Square),
97 "flat" => Some(Self::Flat),
98 _ => None,
99 }
100 }
101
102 const fn as_str(self) -> &'static str {
103 match self {
104 Self::Round => "rnd",
105 Self::Square => "sq",
106 Self::Flat => "flat",
107 }
108 }
109}
110
111#[allow(non_camel_case_types)]
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum ST_PresetLineDashVal {
115 Solid,
116 Dot,
117 SystemDot,
118 Dash,
119 SystemDash,
120 LargeDash,
121 DashDot,
122 SystemDashDot,
123 LargeDashDot,
124 LargeDashDotDot,
125 SystemDashDotDot,
126}
127
128impl ST_PresetLineDashVal {
129 pub const ALL: [Self; 11] = [
130 Self::Solid,
131 Self::Dot,
132 Self::SystemDot,
133 Self::Dash,
134 Self::SystemDash,
135 Self::LargeDash,
136 Self::DashDot,
137 Self::SystemDashDot,
138 Self::LargeDashDot,
139 Self::LargeDashDotDot,
140 Self::SystemDashDotDot,
141 ];
142
143 fn parse(value: &str) -> Option<Self> {
144 match value {
145 "solid" => Some(Self::Solid),
146 "dot" => Some(Self::Dot),
147 "sysDot" => Some(Self::SystemDot),
148 "dash" => Some(Self::Dash),
149 "sysDash" => Some(Self::SystemDash),
150 "lgDash" => Some(Self::LargeDash),
151 "dashDot" => Some(Self::DashDot),
152 "sysDashDot" => Some(Self::SystemDashDot),
153 "lgDashDot" => Some(Self::LargeDashDot),
154 "lgDashDotDot" => Some(Self::LargeDashDotDot),
155 "sysDashDotDot" => Some(Self::SystemDashDotDot),
156 _ => None,
157 }
158 }
159
160 pub const fn as_str(self) -> &'static str {
161 match self {
162 Self::Solid => "solid",
163 Self::Dot => "dot",
164 Self::SystemDot => "sysDot",
165 Self::Dash => "dash",
166 Self::SystemDash => "sysDash",
167 Self::LargeDash => "lgDash",
168 Self::DashDot => "dashDot",
169 Self::SystemDashDot => "sysDashDot",
170 Self::LargeDashDot => "lgDashDot",
171 Self::LargeDashDotDot => "lgDashDotDot",
172 Self::SystemDashDotDot => "sysDashDotDot",
173 }
174 }
175
176 pub const fn dash_array(self) -> &'static [u16] {
178 match self {
179 Self::Solid => &[],
180 Self::Dot | Self::SystemDot => &[1, 1],
181 Self::Dash => &[4, 3],
182 Self::SystemDash => &[3, 1],
183 Self::LargeDash => &[8, 3],
184 Self::DashDot => &[4, 3, 1, 3],
185 Self::SystemDashDot => &[3, 1, 1, 1],
186 Self::LargeDashDot => &[8, 3, 1, 3],
187 Self::LargeDashDotDot => &[8, 3, 1, 3, 1, 3],
188 Self::SystemDashDotDot => &[3, 1, 1, 1, 1, 1],
189 }
190 }
191}
192
193#[derive(Clone, Debug, Eq, PartialEq)]
195pub struct DashStop {
196 pub dash: i32,
197 pub space: i32,
198 raw_children: OrderedRawChildren,
199}
200
201impl DashStop {
202 pub fn new(dash: i32, space: i32) -> Result<Self> {
203 validate_positive_percentage("ds", "d", dash)?;
204 validate_positive_percentage("ds", "sp", space)?;
205 Ok(Self {
206 dash,
207 space,
208 raw_children: OrderedRawChildren::default(),
209 })
210 }
211}
212
213#[derive(Clone, Debug, Eq, PartialEq)]
215pub struct PresetDash {
216 pub value: ST_PresetLineDashVal,
217 raw_children: OrderedRawChildren,
218}
219
220impl PresetDash {
221 pub fn new(value: ST_PresetLineDashVal) -> Self {
222 Self {
223 value,
224 raw_children: OrderedRawChildren::default(),
225 }
226 }
227}
228
229#[derive(Clone, Debug, Default, Eq, PartialEq)]
231pub struct CustomDash {
232 pub stops: Vec<DashStop>,
233 raw_children: OrderedRawChildren,
234}
235
236#[derive(Clone, Debug, Eq, PartialEq)]
238pub enum LineDash {
239 Preset(PresetDash),
240 Custom(CustomDash),
241}
242
243#[derive(Clone, Debug, Eq, PartialEq)]
245pub enum LineJoin {
246 Round {
247 raw_children: OrderedRawChildren,
248 },
249 Bevel {
250 raw_children: OrderedRawChildren,
251 },
252 Miter {
253 limit: Option<i32>,
254 raw_children: OrderedRawChildren,
255 },
256}
257
258impl LineJoin {
259 pub fn round() -> Self {
260 Self::Round {
261 raw_children: OrderedRawChildren::default(),
262 }
263 }
264
265 pub fn bevel() -> Self {
266 Self::Bevel {
267 raw_children: OrderedRawChildren::default(),
268 }
269 }
270
271 pub fn miter(limit: Option<i32>) -> Result<Self> {
272 if let Some(limit) = limit {
273 validate_positive_percentage("miter", "lim", limit)?;
274 }
275 Ok(Self::Miter {
276 limit,
277 raw_children: OrderedRawChildren::default(),
278 })
279 }
280}
281
282#[derive(Clone, Copy, Debug, Eq, PartialEq)]
284pub enum LineEndType {
285 None,
286 Triangle,
287 Stealth,
288 Diamond,
289 Oval,
290 Arrow,
291}
292
293impl LineEndType {
294 fn parse(value: &str) -> Option<Self> {
295 match value {
296 "none" => Some(Self::None),
297 "triangle" => Some(Self::Triangle),
298 "stealth" => Some(Self::Stealth),
299 "diamond" => Some(Self::Diamond),
300 "oval" => Some(Self::Oval),
301 "arrow" => Some(Self::Arrow),
302 _ => None,
303 }
304 }
305
306 const fn as_str(self) -> &'static str {
307 match self {
308 Self::None => "none",
309 Self::Triangle => "triangle",
310 Self::Stealth => "stealth",
311 Self::Diamond => "diamond",
312 Self::Oval => "oval",
313 Self::Arrow => "arrow",
314 }
315 }
316}
317
318#[derive(Clone, Copy, Debug, Eq, PartialEq)]
320pub enum LineEndSize {
321 Small,
322 Medium,
323 Large,
324}
325
326impl LineEndSize {
327 fn parse(value: &str) -> Option<Self> {
328 match value {
329 "sm" => Some(Self::Small),
330 "med" => Some(Self::Medium),
331 "lg" => Some(Self::Large),
332 _ => None,
333 }
334 }
335
336 const fn as_str(self) -> &'static str {
337 match self {
338 Self::Small => "sm",
339 Self::Medium => "med",
340 Self::Large => "lg",
341 }
342 }
343}
344
345#[derive(Clone, Debug, Default, Eq, PartialEq)]
347pub struct LineEnd {
348 pub kind: Option<LineEndType>,
349 pub width: Option<LineEndSize>,
350 pub length: Option<LineEndSize>,
351 raw_children: OrderedRawChildren,
352}
353
354#[allow(non_camel_case_types)]
356#[derive(Clone, Debug, Default, Eq, PartialEq)]
357pub struct CT_LineProperties {
358 pub width: Option<u32>,
359 pub cap: Option<LineCap>,
360 pub fill: Option<Fill>,
361 pub dash: Option<LineDash>,
362 pub join: Option<LineJoin>,
363 pub head_end: Option<LineEnd>,
364 pub tail_end: Option<LineEnd>,
365 raw_attributes: Vec<(String, String)>,
366 raw_children: OrderedRawChildren,
367}
368
369impl CT_LineProperties {
370 pub fn from_xml(xml: &[u8]) -> Result<Self> {
372 let mut reader = Reader::from_reader(xml);
373 let mut buffer = Vec::new();
374 loop {
375 match reader
376 .read_event_into(&mut buffer)
377 .map_err(OxmlError::from)?
378 {
379 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ln") => {
380 return Self::from_element(&mut reader, &element);
381 }
382 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ln") => {
383 return Self::from_start(&element);
384 }
385 Event::Start(element) | Event::Empty(element) => {
386 return Err(unexpected(&element));
387 }
388 Event::Eof => {
389 return Err(LineError::Xml(OxmlError::MissingElement(
390 "DrawingML line properties".to_owned(),
391 )));
392 }
393 _ => {}
394 }
395 buffer.clear();
396 }
397 }
398
399 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
400 let width = optional_exact_parse(start, b"w")?;
401 if let Some(width) = width {
402 validate_line_width(width)?;
403 }
404 Ok(Self {
405 width,
406 cap: optional_exact_enum(start, b"cap", LineCap::parse)?,
407 raw_attributes: capture_raw_attributes(start, &[b"w", b"cap"])?,
408 ..Self::default()
409 })
410 }
411
412 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
413 let mut line = Self::from_start(start)?;
414 let mut boundary = 0;
415 let mut buffer = Vec::new();
416 loop {
417 match reader
418 .read_event_into(&mut buffer)
419 .map_err(OxmlError::from)?
420 {
421 Event::Start(element) if is_fill(element.name().as_ref()) => {
422 line.fill = Some(Fill::from_element(reader, &element)?);
423 boundary = 1;
424 }
425 Event::Empty(element) if is_fill(element.name().as_ref()) => {
426 line.fill = Some(Fill::from_empty_element(&element)?);
427 boundary = 1;
428 }
429 Event::Start(element)
430 if matches_local_name(element.name().as_ref(), b"prstDash") =>
431 {
432 line.dash = Some(LineDash::Preset(parse_preset_dash(reader, &element)?));
433 boundary = 2;
434 }
435 Event::Empty(element)
436 if matches_local_name(element.name().as_ref(), b"prstDash") =>
437 {
438 line.dash = Some(LineDash::Preset(parse_empty_preset_dash(&element)?));
439 boundary = 2;
440 }
441 Event::Start(element)
442 if matches_local_name(element.name().as_ref(), b"custDash") =>
443 {
444 line.dash = Some(LineDash::Custom(parse_custom_dash(reader)?));
445 boundary = 2;
446 }
447 Event::Empty(element)
448 if matches_local_name(element.name().as_ref(), b"custDash") =>
449 {
450 line.dash = Some(LineDash::Custom(CustomDash::default()));
451 boundary = 2;
452 }
453 Event::Start(element) if is_join(element.name().as_ref()) => {
454 line.join = Some(parse_join(reader, &element)?);
455 boundary = 3;
456 }
457 Event::Empty(element) if is_join(element.name().as_ref()) => {
458 line.join = Some(parse_empty_join(&element)?);
459 boundary = 3;
460 }
461 Event::Start(element)
462 if matches_local_name(element.name().as_ref(), b"headEnd") =>
463 {
464 line.head_end = Some(parse_line_end(reader, &element)?);
465 boundary = 4;
466 }
467 Event::Empty(element)
468 if matches_local_name(element.name().as_ref(), b"headEnd") =>
469 {
470 line.head_end = Some(parse_empty_line_end(&element)?);
471 boundary = 4;
472 }
473 Event::Start(element)
474 if matches_local_name(element.name().as_ref(), b"tailEnd") =>
475 {
476 line.tail_end = Some(parse_line_end(reader, &element)?);
477 boundary = 5;
478 }
479 Event::Empty(element)
480 if matches_local_name(element.name().as_ref(), b"tailEnd") =>
481 {
482 line.tail_end = Some(parse_empty_line_end(&element)?);
483 boundary = 5;
484 }
485 Event::Start(element) => line
486 .raw_children
487 .push(boundary, capture_element(reader, &element)?),
488 Event::Empty(element) => line
489 .raw_children
490 .push(boundary, capture_empty_element(&element)?),
491 Event::End(element) if matches_local_name(element.name().as_ref(), b"ln") => break,
492 Event::Eof => return Err(missing_end("ln")),
493 _ => {}
494 }
495 buffer.clear();
496 }
497 Ok(line)
498 }
499
500 pub fn to_xml(&self) -> Result<Vec<u8>> {
502 let mut writer = Writer::new(Vec::new());
503 self.write_xml(&mut writer)?;
504 Ok(writer.into_inner())
505 }
506
507 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
509 if let Some(width) = self.width {
510 validate_line_width(width)?;
511 }
512 if matches!(self.fill.as_ref(), Some(Fill::Blip(_))) {
513 return Err(LineError::UnexpectedElement("a:blipFill".to_owned()));
514 }
515 let mut start = BytesStart::new("a:ln");
516 let width = self.width.map(|value| value.to_string());
517 if let Some(width) = width.as_deref() {
518 start.push_attribute(("w", width));
519 }
520 if let Some(cap) = self.cap {
521 start.push_attribute(("cap", cap.as_str()));
522 }
523 push_raw_attributes(&mut start, &self.raw_attributes);
524 if self.fill.is_none()
525 && self.dash.is_none()
526 && self.join.is_none()
527 && self.head_end.is_none()
528 && self.tail_end.is_none()
529 && self.raw_children.is_empty()
530 {
531 return write_empty(writer, start);
532 }
533
534 write_start(writer, start)?;
535 emit_raw(writer, self.raw_children.at(0))?;
536 if let Some(fill) = &self.fill {
537 fill.write_xml(writer)?;
538 }
539 emit_raw(writer, self.raw_children.at(1))?;
540 if let Some(dash) = &self.dash {
541 write_dash(writer, dash)?;
542 }
543 emit_raw(writer, self.raw_children.at(2))?;
544 if let Some(join) = &self.join {
545 write_join(writer, join)?;
546 }
547 emit_raw(writer, self.raw_children.at(3))?;
548 if let Some(end) = &self.head_end {
549 write_line_end(writer, "a:headEnd", end)?;
550 }
551 emit_raw(writer, self.raw_children.at(4))?;
552 if let Some(end) = &self.tail_end {
553 write_line_end(writer, "a:tailEnd", end)?;
554 }
555 emit_raw(writer, self.raw_children.at(5))?;
556 write_end(writer, "a:ln")
557 }
558
559 pub fn raw_children(&self) -> &OrderedRawChildren {
560 &self.raw_children
561 }
562}
563
564fn parse_empty_preset_dash(start: &BytesStart<'_>) -> Result<PresetDash> {
565 let value = get_attr(start, b"val").unwrap_or_else(|| "solid".to_owned());
566 let value = ST_PresetLineDashVal::parse(&value).ok_or_else(|| invalid(start, b"val", value))?;
567 Ok(PresetDash::new(value))
568}
569
570fn parse_preset_dash(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<PresetDash> {
571 let mut dash = parse_empty_preset_dash(start)?;
572 dash.raw_children = capture_all_children(reader, b"prstDash")?;
573 Ok(dash)
574}
575
576fn parse_custom_dash(reader: &mut Reader<&[u8]>) -> Result<CustomDash> {
577 let mut dash = CustomDash::default();
578 let mut buffer = Vec::new();
579 loop {
580 match reader
581 .read_event_into(&mut buffer)
582 .map_err(OxmlError::from)?
583 {
584 Event::Start(element) if matches_local_name(element.name().as_ref(), b"ds") => {
585 let mut stop = parse_empty_dash_stop(&element)?;
586 stop.raw_children = capture_all_children(reader, b"ds")?;
587 dash.stops.push(stop);
588 }
589 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ds") => {
590 dash.stops.push(parse_empty_dash_stop(&element)?);
591 }
592 Event::Start(element) => dash
593 .raw_children
594 .push(dash.stops.len(), capture_element(reader, &element)?),
595 Event::Empty(element) => dash
596 .raw_children
597 .push(dash.stops.len(), capture_empty_element(&element)?),
598 Event::End(element) if matches_local_name(element.name().as_ref(), b"custDash") => {
599 break;
600 }
601 Event::Eof => return Err(missing_end("custDash")),
602 _ => {}
603 }
604 buffer.clear();
605 }
606 Ok(dash)
607}
608
609fn parse_empty_dash_stop(start: &BytesStart<'_>) -> Result<DashStop> {
610 DashStop::new(required_parse(start, b"d")?, required_parse(start, b"sp")?)
611}
612
613fn write_dash<W: Write>(writer: &mut Writer<W>, dash: &LineDash) -> Result<()> {
614 match dash {
615 LineDash::Preset(dash) => {
616 let mut start = BytesStart::new("a:prstDash");
617 start.push_attribute(("val", dash.value.as_str()));
618 if dash.raw_children.is_empty() {
619 return write_empty(writer, start);
620 }
621 write_start(writer, start)?;
622 emit_raw(writer, dash.raw_children.at(0))?;
623 write_end(writer, "a:prstDash")
624 }
625 LineDash::Custom(dash) => {
626 if dash.stops.is_empty() && dash.raw_children.is_empty() {
627 return write_empty(writer, BytesStart::new("a:custDash"));
628 }
629 write_start(writer, BytesStart::new("a:custDash"))?;
630 for boundary in 0..=dash.stops.len() {
631 emit_raw(writer, dash.raw_children.at(boundary))?;
632 if let Some(stop) = dash.stops.get(boundary) {
633 write_dash_stop(writer, stop)?;
634 }
635 }
636 write_end(writer, "a:custDash")
637 }
638 }
639}
640
641fn write_dash_stop<W: Write>(writer: &mut Writer<W>, stop: &DashStop) -> Result<()> {
642 validate_positive_percentage("ds", "d", stop.dash)?;
643 validate_positive_percentage("ds", "sp", stop.space)?;
644 let dash = stop.dash.to_string();
645 let space = stop.space.to_string();
646 let mut start = BytesStart::new("a:ds");
647 start.push_attribute(("d", dash.as_str()));
648 start.push_attribute(("sp", space.as_str()));
649 if stop.raw_children.is_empty() {
650 return write_empty(writer, start);
651 }
652 write_start(writer, start)?;
653 emit_raw(writer, stop.raw_children.at(0))?;
654 write_end(writer, "a:ds")
655}
656
657fn parse_empty_join(start: &BytesStart<'_>) -> Result<LineJoin> {
658 match local_name(start.name().as_ref()) {
659 b"round" => Ok(LineJoin::round()),
660 b"bevel" => Ok(LineJoin::bevel()),
661 b"miter" => LineJoin::miter(optional_parse(start, b"lim")?),
662 _ => Err(unexpected(start)),
663 }
664}
665
666fn parse_join(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<LineJoin> {
667 let mut join = parse_empty_join(start)?;
668 let raw = capture_all_children(reader, local_name(start.name().as_ref()))?;
669 match &mut join {
670 LineJoin::Round { raw_children }
671 | LineJoin::Bevel { raw_children }
672 | LineJoin::Miter { raw_children, .. } => *raw_children = raw,
673 }
674 Ok(join)
675}
676
677fn write_join<W: Write>(writer: &mut Writer<W>, join: &LineJoin) -> Result<()> {
678 let (name, limit, raw_children) = match join {
679 LineJoin::Round { raw_children } => ("a:round", None, raw_children),
680 LineJoin::Bevel { raw_children } => ("a:bevel", None, raw_children),
681 LineJoin::Miter {
682 limit,
683 raw_children,
684 } => ("a:miter", *limit, raw_children),
685 };
686 if let Some(limit) = limit {
687 validate_positive_percentage("miter", "lim", limit)?;
688 }
689 let limit_text = limit.map(|value| value.to_string());
690 let mut start = BytesStart::new(name);
691 if let Some(limit) = limit_text.as_deref() {
692 start.push_attribute(("lim", limit));
693 }
694 if raw_children.is_empty() {
695 return write_empty(writer, start);
696 }
697 write_start(writer, start)?;
698 emit_raw(writer, raw_children.at(0))?;
699 write_end(writer, name)
700}
701
702fn parse_empty_line_end(start: &BytesStart<'_>) -> Result<LineEnd> {
703 Ok(LineEnd {
704 kind: optional_enum(start, b"type", LineEndType::parse)?,
705 width: optional_enum(start, b"w", LineEndSize::parse)?,
706 length: optional_enum(start, b"len", LineEndSize::parse)?,
707 raw_children: OrderedRawChildren::default(),
708 })
709}
710
711fn parse_line_end(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<LineEnd> {
712 let mut end = parse_empty_line_end(start)?;
713 end.raw_children = capture_all_children(reader, local_name(start.name().as_ref()))?;
714 Ok(end)
715}
716
717fn write_line_end<W: Write>(writer: &mut Writer<W>, name: &str, end: &LineEnd) -> Result<()> {
718 let mut start = BytesStart::new(name);
719 if let Some(kind) = end.kind {
720 start.push_attribute(("type", kind.as_str()));
721 }
722 if let Some(width) = end.width {
723 start.push_attribute(("w", width.as_str()));
724 }
725 if let Some(length) = end.length {
726 start.push_attribute(("len", length.as_str()));
727 }
728 if end.raw_children.is_empty() {
729 return write_empty(writer, start);
730 }
731 write_start(writer, start)?;
732 emit_raw(writer, end.raw_children.at(0))?;
733 write_end(writer, name)
734}
735
736fn capture_all_children(reader: &mut Reader<&[u8]>, end_name: &[u8]) -> Result<OrderedRawChildren> {
737 let mut raw_children = OrderedRawChildren::default();
738 let mut buffer = Vec::new();
739 loop {
740 match reader
741 .read_event_into(&mut buffer)
742 .map_err(OxmlError::from)?
743 {
744 Event::Start(element) => raw_children.push(0, capture_element(reader, &element)?),
745 Event::Empty(element) => raw_children.push(0, capture_empty_element(&element)?),
746 Event::End(element) if matches_local_name(element.name().as_ref(), end_name) => break,
747 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(end_name))),
748 _ => {}
749 }
750 buffer.clear();
751 }
752 Ok(raw_children)
753}
754
755fn is_fill(name: &[u8]) -> bool {
756 matches!(
757 local_name(name),
758 b"noFill" | b"solidFill" | b"gradFill" | b"pattFill"
759 )
760}
761
762fn is_join(name: &[u8]) -> bool {
763 matches!(local_name(name), b"round" | b"bevel" | b"miter")
764}
765
766fn validate_line_width(value: u32) -> Result<()> {
767 if value <= MAX_LINE_WIDTH_EMU {
768 Ok(())
769 } else {
770 Err(LineError::LineWidthOutOfRange(value))
771 }
772}
773
774fn validate_positive_percentage(element: &str, attribute: &str, value: i32) -> Result<()> {
775 if value > 0 {
776 Ok(())
777 } else {
778 Err(LineError::InvalidAttribute {
779 element: element.to_owned(),
780 attribute: attribute.to_owned(),
781 value: value.to_string(),
782 })
783 }
784}
785
786fn required_parse<T: std::str::FromStr>(start: &BytesStart<'_>, name: &[u8]) -> Result<T> {
787 let value = get_attr(start, name).ok_or_else(|| LineError::MissingAttribute {
788 element: String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned(),
789 attribute: String::from_utf8_lossy(name).into_owned(),
790 })?;
791 value.parse().map_err(|_| invalid(start, name, value))
792}
793
794fn optional_parse<T: std::str::FromStr>(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<T>> {
795 get_attr(start, name)
796 .map(|value| value.parse().map_err(|_| invalid(start, name, value)))
797 .transpose()
798}
799
800fn optional_enum<T>(
801 start: &BytesStart<'_>,
802 name: &[u8],
803 parse: impl FnOnce(&str) -> Option<T>,
804) -> Result<Option<T>> {
805 get_attr(start, name)
806 .map(|value| parse(&value).ok_or_else(|| invalid(start, name, value)))
807 .transpose()
808}
809
810fn exact_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
811 for attribute in start.attributes() {
812 let attribute = attribute.map_err(OxmlError::from)?;
813 if attribute.key.as_ref() == name {
814 let value = attribute
815 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
816 .map_err(OxmlError::from)?;
817 return Ok(Some(value.into_owned()));
818 }
819 }
820 Ok(None)
821}
822
823fn optional_exact_parse<T: std::str::FromStr>(
824 start: &BytesStart<'_>,
825 name: &[u8],
826) -> Result<Option<T>> {
827 exact_attr(start, name)?
828 .map(|value| value.parse().map_err(|_| invalid(start, name, value)))
829 .transpose()
830}
831
832fn optional_exact_enum<T>(
833 start: &BytesStart<'_>,
834 name: &[u8],
835 parse: impl FnOnce(&str) -> Option<T>,
836) -> Result<Option<T>> {
837 exact_attr(start, name)?
838 .map(|value| parse(&value).ok_or_else(|| invalid(start, name, value)))
839 .transpose()
840}
841
842fn capture_raw_attributes(
843 start: &BytesStart<'_>,
844 modelled: &[&[u8]],
845) -> Result<Vec<(String, String)>> {
846 let mut raw = Vec::new();
847 for attribute in start.attributes() {
848 let attribute = attribute.map_err(OxmlError::from)?;
849 if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
850 continue;
851 }
852 let name = std::str::from_utf8(attribute.key.as_ref())
853 .map_err(OxmlError::from)?
854 .to_owned();
855 let value = attribute
856 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
857 .map_err(OxmlError::from)?
858 .into_owned();
859 raw.push((name, value));
860 }
861 Ok(raw)
862}
863
864fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
865 for (name, value) in attributes {
866 start.push_attribute((name.as_str(), value.as_str()));
867 }
868}
869
870fn invalid(start: &BytesStart<'_>, attribute: &[u8], value: String) -> LineError {
871 LineError::InvalidAttribute {
872 element: String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned(),
873 attribute: String::from_utf8_lossy(attribute).into_owned(),
874 value,
875 }
876}
877
878fn unexpected(start: &BytesStart<'_>) -> LineError {
879 LineError::UnexpectedElement(String::from_utf8_lossy(start.name().as_ref()).into_owned())
880}
881
882fn missing_end(name: &str) -> LineError {
883 LineError::Xml(OxmlError::MissingElement(format!("closing a:{name}")))
884}
885
886fn emit_raw<'a, W: Write>(
887 writer: &mut Writer<W>,
888 children: impl Iterator<Item = &'a [u8]>,
889) -> Result<()> {
890 for child in children {
891 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
892 }
893 Ok(())
894}
895
896fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
897 writer
898 .write_event(Event::Start(start))
899 .map_err(OxmlError::from)?;
900 Ok(())
901}
902
903fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
904 writer
905 .write_event(Event::Empty(start))
906 .map_err(OxmlError::from)?;
907 Ok(())
908}
909
910fn write_end<W: Write>(writer: &mut Writer<W>, name: &str) -> Result<()> {
911 writer
912 .write_event(Event::End(BytesEnd::new(name)))
913 .map_err(OxmlError::from)?;
914 Ok(())
915}
916
917#[cfg(test)]
918mod tests {
919 use super::{
920 CT_LineProperties, LineCap, LineDash, LineEndSize, LineEndType, LineError, LineJoin,
921 ST_PresetLineDashVal,
922 };
923
924 #[test]
925 fn every_preset_line_dash_value_maps_to_a_dash_array() {
926 let expected: &[(&str, &[u16])] = &[
927 ("solid", &[]),
928 ("dot", &[1, 1]),
929 ("sysDot", &[1, 1]),
930 ("dash", &[4, 3]),
931 ("sysDash", &[3, 1]),
932 ("lgDash", &[8, 3]),
933 ("dashDot", &[4, 3, 1, 3]),
934 ("sysDashDot", &[3, 1, 1, 1]),
935 ("lgDashDot", &[8, 3, 1, 3]),
936 ("lgDashDotDot", &[8, 3, 1, 3, 1, 3]),
937 ("sysDashDotDot", &[3, 1, 1, 1, 1, 1]),
938 ];
939
940 assert_eq!(ST_PresetLineDashVal::ALL.len(), expected.len());
941 for (value, (token, dash_array)) in ST_PresetLineDashVal::ALL.iter().zip(expected) {
942 assert_eq!(value.as_str(), *token);
943 assert_eq!(value.dash_array(), *dash_array);
944 }
945 }
946
947 #[test]
948 fn line_root_attributes_round_trip_without_loss() {
949 let line = CT_LineProperties::from_xml(
950 br#"<q:ln w="12700" cap="rnd" cmpd="dbl" algn="ctr" x:w="999"/>"#,
951 )
952 .unwrap();
953 assert_eq!(line.width, Some(12_700));
954 assert_eq!(
955 line.to_xml().unwrap(),
956 br#"<a:ln w="12700" cap="rnd" cmpd="dbl" algn="ctr" x:w="999"/>"#
957 );
958 }
959
960 #[test]
961 fn line_properties_round_trip_width_fill_dash_cap_join_and_ends() {
962 let xml = br#"<z:ln w="12700" cap="rnd"><z:solidFill><z:schemeClr val="accent1"/></z:solidFill><z:custDash><z:ds d="200000" sp="100000"/><z:ds d="50000" sp="25000"/></z:custDash><z:miter lim="800000"/><z:headEnd type="triangle" w="lg" len="sm"/><z:tailEnd type="oval" w="med" len="lg"/></z:ln>"#;
963 let parsed = CT_LineProperties::from_xml(xml).unwrap();
964
965 assert_eq!(parsed.width, Some(12_700));
966 assert_eq!(parsed.cap, Some(LineCap::Round));
967 assert!(matches!(&parsed.dash, Some(LineDash::Custom(dash)) if dash.stops.len() == 2));
968 assert!(matches!(
969 parsed.join,
970 Some(LineJoin::Miter {
971 limit: Some(800_000),
972 ..
973 })
974 ));
975 assert!(
976 matches!(parsed.head_end, Some(ref end) if end.kind == Some(LineEndType::Triangle) && end.width == Some(LineEndSize::Large) && end.length == Some(LineEndSize::Small))
977 );
978 assert!(
979 matches!(parsed.tail_end, Some(ref end) if end.kind == Some(LineEndType::Oval) && end.width == Some(LineEndSize::Medium) && end.length == Some(LineEndSize::Large))
980 );
981
982 let written = parsed.to_xml().unwrap();
983 assert_eq!(CT_LineProperties::from_xml(&written).unwrap(), parsed);
984 }
985
986 #[test]
987 fn line_properties_write_schema_order_and_preserve_unknown_children() {
988 let xml = br#"<z:ln w="25400"><x:before x:id="1"/><z:solidFill><z:srgbClr val="102030"/></z:solidFill><x:afterFill><x:item>one & two</x:item><!--note--></x:afterFill><z:prstDash val="lgDashDot"><x:dashExt x:v="kept"/></z:prstDash><x:afterDash/><z:round><x:joinExt/></z:round><z:headEnd type="arrow"><x:headExt/></z:headEnd><x:betweenEnds/><z:tailEnd type="diamond"/><x:after/></z:ln>"#;
989 let written = CT_LineProperties::from_xml(xml).unwrap().to_xml().unwrap();
990
991 assert_eq!(written, br#"<a:ln w="25400"><x:before x:id="1"/><a:solidFill><a:srgbClr val="102030"/></a:solidFill><x:afterFill><x:item>one & two</x:item><!--note--></x:afterFill><a:prstDash val="lgDashDot"><x:dashExt x:v="kept"/></a:prstDash><x:afterDash/><a:round><x:joinExt/></a:round><a:headEnd type="arrow"><x:headExt/></a:headEnd><x:betweenEnds/><a:tailEnd type="diamond"/><x:after/></a:ln>"#);
992 }
993
994 #[test]
995 fn malformed_line_values_return_errors_without_panicking() {
996 let cases: &[&[u8]] = &[
997 br#"<a:ln w="wide"/>"#,
998 br#"<a:ln w="20116801"/>"#,
999 br#"<a:ln cap="curved"/>"#,
1000 br#"<a:ln><a:prstDash val="longer"/></a:ln>"#,
1001 br#"<a:ln><a:custDash><a:ds sp="100000"/></a:custDash></a:ln>"#,
1002 br#"<a:ln><a:custDash><a:ds d="0" sp="100000"/></a:custDash></a:ln>"#,
1003 br#"<a:ln><a:miter lim="0"/></a:ln>"#,
1004 br#"<a:ln><a:headEnd type="spear"/></a:ln>"#,
1005 br#"<a:ln><a:tailEnd w="huge"/></a:ln>"#,
1006 ];
1007 for xml in cases {
1008 let result = std::panic::catch_unwind(|| CT_LineProperties::from_xml(xml));
1009 assert!(
1010 result.is_ok(),
1011 "line parser panicked for {}",
1012 String::from_utf8_lossy(xml)
1013 );
1014 assert!(
1015 result.unwrap().is_err(),
1016 "malformed line parsed successfully"
1017 );
1018 }
1019 assert!(matches!(
1020 CT_LineProperties::from_xml(cases[1]),
1021 Err(LineError::LineWidthOutOfRange(20_116_801))
1022 ));
1023 }
1024}