Skip to main content

oxml_drawing/
line.rs

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/// Errors produced while parsing or writing DrawingML line properties.
16#[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/// DrawingML line cap behavior.
85#[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/// The eleven values in `ST_PresetLineDashVal`.
112#[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    /// Returns alternating painted and unpainted lengths relative to line width.
177    pub const fn dash_array(self) -> &'static [u16] {
178        match self {
179            Self::Solid => &[],
180            Self::Dot => &[1, 3],
181            Self::SystemDot => &[1, 1],
182            Self::Dash => &[4, 3],
183            Self::SystemDash => &[3, 1],
184            Self::LargeDash => &[8, 3],
185            Self::DashDot => &[4, 3, 1, 3],
186            Self::SystemDashDot => &[3, 1, 1, 1],
187            Self::LargeDashDot => &[8, 3, 1, 3],
188            Self::LargeDashDotDot => &[8, 3, 1, 3, 1, 3],
189            Self::SystemDashDotDot => &[3, 1, 1, 1, 1, 1],
190        }
191    }
192}
193
194/// One custom painted and unpainted dash pair, in thousandths of a percent.
195#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct DashStop {
197    pub dash: i32,
198    pub space: i32,
199    raw_children: OrderedRawChildren,
200}
201
202impl DashStop {
203    pub fn new(dash: i32, space: i32) -> Result<Self> {
204        validate_positive_percentage("ds", "d", dash)?;
205        validate_positive_percentage("ds", "sp", space)?;
206        Ok(Self {
207            dash,
208            space,
209            raw_children: OrderedRawChildren::default(),
210        })
211    }
212}
213
214/// A preset line dash and any extension children inside it.
215#[derive(Clone, Debug, Eq, PartialEq)]
216pub struct PresetDash {
217    pub value: ST_PresetLineDashVal,
218    raw_children: OrderedRawChildren,
219}
220
221impl PresetDash {
222    pub fn new(value: ST_PresetLineDashVal) -> Self {
223        Self {
224            value,
225            raw_children: OrderedRawChildren::default(),
226        }
227    }
228}
229
230/// Custom line dash stops in document order.
231#[derive(Clone, Debug, Default, Eq, PartialEq)]
232pub struct CustomDash {
233    pub stops: Vec<DashStop>,
234    raw_children: OrderedRawChildren,
235}
236
237/// Either a DrawingML preset or custom line dash.
238#[derive(Clone, Debug, Eq, PartialEq)]
239pub enum LineDash {
240    Preset(PresetDash),
241    Custom(CustomDash),
242}
243
244/// DrawingML line join behavior.
245#[derive(Clone, Debug, Eq, PartialEq)]
246pub enum LineJoin {
247    Round {
248        raw_children: OrderedRawChildren,
249    },
250    Bevel {
251        raw_children: OrderedRawChildren,
252    },
253    Miter {
254        limit: Option<i32>,
255        raw_children: OrderedRawChildren,
256    },
257}
258
259impl LineJoin {
260    pub fn round() -> Self {
261        Self::Round {
262            raw_children: OrderedRawChildren::default(),
263        }
264    }
265
266    pub fn bevel() -> Self {
267        Self::Bevel {
268            raw_children: OrderedRawChildren::default(),
269        }
270    }
271
272    pub fn miter(limit: Option<i32>) -> Result<Self> {
273        if let Some(limit) = limit {
274            validate_positive_percentage("miter", "lim", limit)?;
275        }
276        Ok(Self::Miter {
277            limit,
278            raw_children: OrderedRawChildren::default(),
279        })
280    }
281}
282
283/// DrawingML line endpoint shape.
284#[derive(Clone, Copy, Debug, Eq, PartialEq)]
285pub enum LineEndType {
286    None,
287    Triangle,
288    Stealth,
289    Diamond,
290    Oval,
291    Arrow,
292}
293
294impl LineEndType {
295    fn parse(value: &str) -> Option<Self> {
296        match value {
297            "none" => Some(Self::None),
298            "triangle" => Some(Self::Triangle),
299            "stealth" => Some(Self::Stealth),
300            "diamond" => Some(Self::Diamond),
301            "oval" => Some(Self::Oval),
302            "arrow" => Some(Self::Arrow),
303            _ => None,
304        }
305    }
306
307    const fn as_str(self) -> &'static str {
308        match self {
309            Self::None => "none",
310            Self::Triangle => "triangle",
311            Self::Stealth => "stealth",
312            Self::Diamond => "diamond",
313            Self::Oval => "oval",
314            Self::Arrow => "arrow",
315        }
316    }
317}
318
319/// DrawingML line endpoint width or length.
320#[derive(Clone, Copy, Debug, Eq, PartialEq)]
321pub enum LineEndSize {
322    Small,
323    Medium,
324    Large,
325}
326
327impl LineEndSize {
328    fn parse(value: &str) -> Option<Self> {
329        match value {
330            "sm" => Some(Self::Small),
331            "med" => Some(Self::Medium),
332            "lg" => Some(Self::Large),
333            _ => None,
334        }
335    }
336
337    const fn as_str(self) -> &'static str {
338        match self {
339            Self::Small => "sm",
340            Self::Medium => "med",
341            Self::Large => "lg",
342        }
343    }
344}
345
346/// One head or tail endpoint and its optional dimensions.
347#[derive(Clone, Debug, Default, Eq, PartialEq)]
348pub struct LineEnd {
349    pub kind: Option<LineEndType>,
350    pub width: Option<LineEndSize>,
351    pub length: Option<LineEndSize>,
352    raw_children: OrderedRawChildren,
353}
354
355/// DrawingML `a:ln` properties.
356#[allow(non_camel_case_types)]
357#[derive(Clone, Debug, Default, Eq, PartialEq)]
358pub struct CT_LineProperties {
359    pub width: Option<u32>,
360    pub cap: Option<LineCap>,
361    pub fill: Option<Fill>,
362    pub dash: Option<LineDash>,
363    pub join: Option<LineJoin>,
364    pub head_end: Option<LineEnd>,
365    pub tail_end: Option<LineEnd>,
366    raw_attributes: Vec<(String, String)>,
367    raw_children: OrderedRawChildren,
368}
369
370impl CT_LineProperties {
371    /// Parses one complete `a:ln` element with any namespace prefix.
372    pub fn from_xml(xml: &[u8]) -> Result<Self> {
373        let mut reader = Reader::from_reader(xml);
374        let mut buffer = Vec::new();
375        loop {
376            match reader
377                .read_event_into(&mut buffer)
378                .map_err(OxmlError::from)?
379            {
380                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ln") => {
381                    return Self::from_element(&mut reader, &element);
382                }
383                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ln") => {
384                    return Self::from_start(&element);
385                }
386                Event::Start(element) | Event::Empty(element) => {
387                    return Err(unexpected(&element));
388                }
389                Event::Eof => {
390                    return Err(LineError::Xml(OxmlError::MissingElement(
391                        "DrawingML line properties".to_owned(),
392                    )));
393                }
394                _ => {}
395            }
396            buffer.clear();
397        }
398    }
399
400    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
401        let width = optional_exact_parse(start, b"w")?;
402        if let Some(width) = width {
403            validate_line_width(width)?;
404        }
405        Ok(Self {
406            width,
407            cap: optional_exact_enum(start, b"cap", LineCap::parse)?,
408            raw_attributes: capture_raw_attributes(start, &[b"w", b"cap"])?,
409            ..Self::default()
410        })
411    }
412
413    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
414        let mut line = Self::from_start(start)?;
415        let mut boundary = 0;
416        let mut buffer = Vec::new();
417        loop {
418            match reader
419                .read_event_into(&mut buffer)
420                .map_err(OxmlError::from)?
421            {
422                Event::Start(element) if is_fill(element.name().as_ref()) => {
423                    line.fill = Some(Fill::from_element(reader, &element)?);
424                    boundary = 1;
425                }
426                Event::Empty(element) if is_fill(element.name().as_ref()) => {
427                    line.fill = Some(Fill::from_empty_element(&element)?);
428                    boundary = 1;
429                }
430                Event::Start(element)
431                    if matches_local_name(element.name().as_ref(), b"prstDash") =>
432                {
433                    line.dash = Some(LineDash::Preset(parse_preset_dash(reader, &element)?));
434                    boundary = 2;
435                }
436                Event::Empty(element)
437                    if matches_local_name(element.name().as_ref(), b"prstDash") =>
438                {
439                    line.dash = Some(LineDash::Preset(parse_empty_preset_dash(&element)?));
440                    boundary = 2;
441                }
442                Event::Start(element)
443                    if matches_local_name(element.name().as_ref(), b"custDash") =>
444                {
445                    line.dash = Some(LineDash::Custom(parse_custom_dash(reader)?));
446                    boundary = 2;
447                }
448                Event::Empty(element)
449                    if matches_local_name(element.name().as_ref(), b"custDash") =>
450                {
451                    line.dash = Some(LineDash::Custom(CustomDash::default()));
452                    boundary = 2;
453                }
454                Event::Start(element) if is_join(element.name().as_ref()) => {
455                    line.join = Some(parse_join(reader, &element)?);
456                    boundary = 3;
457                }
458                Event::Empty(element) if is_join(element.name().as_ref()) => {
459                    line.join = Some(parse_empty_join(&element)?);
460                    boundary = 3;
461                }
462                Event::Start(element)
463                    if matches_local_name(element.name().as_ref(), b"headEnd") =>
464                {
465                    line.head_end = Some(parse_line_end(reader, &element)?);
466                    boundary = 4;
467                }
468                Event::Empty(element)
469                    if matches_local_name(element.name().as_ref(), b"headEnd") =>
470                {
471                    line.head_end = Some(parse_empty_line_end(&element)?);
472                    boundary = 4;
473                }
474                Event::Start(element)
475                    if matches_local_name(element.name().as_ref(), b"tailEnd") =>
476                {
477                    line.tail_end = Some(parse_line_end(reader, &element)?);
478                    boundary = 5;
479                }
480                Event::Empty(element)
481                    if matches_local_name(element.name().as_ref(), b"tailEnd") =>
482                {
483                    line.tail_end = Some(parse_empty_line_end(&element)?);
484                    boundary = 5;
485                }
486                Event::Start(element) => line.raw_children.push(
487                    raw_boundary(element.name().as_ref(), boundary),
488                    capture_element(reader, &element)?,
489                ),
490                Event::Empty(element) => line.raw_children.push(
491                    raw_boundary(element.name().as_ref(), boundary),
492                    capture_empty_element(&element)?,
493                ),
494                Event::End(element) if matches_local_name(element.name().as_ref(), b"ln") => break,
495                Event::Eof => return Err(missing_end("ln")),
496                _ => {}
497            }
498            buffer.clear();
499        }
500        Ok(line)
501    }
502
503    /// Writes this line with canonical DrawingML prefixes and schema order.
504    pub fn to_xml(&self) -> Result<Vec<u8>> {
505        let mut writer = Writer::new(Vec::new());
506        self.write_xml(&mut writer)?;
507        Ok(writer.into_inner())
508    }
509
510    /// Writes this line into an existing XML writer.
511    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
512        if let Some(width) = self.width {
513            validate_line_width(width)?;
514        }
515        if matches!(self.fill.as_ref(), Some(Fill::Blip(_))) {
516            return Err(LineError::UnexpectedElement("a:blipFill".to_owned()));
517        }
518        let mut start = BytesStart::new("a:ln");
519        let width = self.width.map(|value| value.to_string());
520        if let Some(width) = width.as_deref() {
521            start.push_attribute(("w", width));
522        }
523        if let Some(cap) = self.cap {
524            start.push_attribute(("cap", cap.as_str()));
525        }
526        push_raw_attributes(&mut start, &self.raw_attributes);
527        if self.fill.is_none()
528            && self.dash.is_none()
529            && self.join.is_none()
530            && self.head_end.is_none()
531            && self.tail_end.is_none()
532            && self.raw_children.is_empty()
533        {
534            return write_empty(writer, start);
535        }
536
537        write_start(writer, start)?;
538        emit_raw(writer, self.raw_children.at(0))?;
539        if let Some(fill) = &self.fill {
540            fill.write_xml(writer)?;
541        }
542        emit_raw(writer, self.raw_children.at(1))?;
543        if let Some(dash) = &self.dash {
544            write_dash(writer, dash)?;
545        }
546        emit_raw(writer, self.raw_children.at(2))?;
547        if let Some(join) = &self.join {
548            write_join(writer, join)?;
549        }
550        emit_raw(writer, self.raw_children.at(3))?;
551        if let Some(end) = &self.head_end {
552            write_line_end(writer, "a:headEnd", end)?;
553        }
554        emit_raw(writer, self.raw_children.at(4))?;
555        if let Some(end) = &self.tail_end {
556            write_line_end(writer, "a:tailEnd", end)?;
557        }
558        emit_raw(writer, self.raw_children.at(5))?;
559        write_end(writer, "a:ln")
560    }
561
562    pub fn raw_children(&self) -> &OrderedRawChildren {
563        &self.raw_children
564    }
565}
566
567fn parse_empty_preset_dash(start: &BytesStart<'_>) -> Result<PresetDash> {
568    let value = get_attr(start, b"val").unwrap_or_else(|| "solid".to_owned());
569    let value = ST_PresetLineDashVal::parse(&value).ok_or_else(|| invalid(start, b"val", value))?;
570    Ok(PresetDash::new(value))
571}
572
573fn parse_preset_dash(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<PresetDash> {
574    let mut dash = parse_empty_preset_dash(start)?;
575    dash.raw_children = capture_all_children(reader, b"prstDash")?;
576    Ok(dash)
577}
578
579fn parse_custom_dash(reader: &mut Reader<&[u8]>) -> Result<CustomDash> {
580    let mut dash = CustomDash::default();
581    let mut buffer = Vec::new();
582    loop {
583        match reader
584            .read_event_into(&mut buffer)
585            .map_err(OxmlError::from)?
586        {
587            Event::Start(element) if matches_local_name(element.name().as_ref(), b"ds") => {
588                let mut stop = parse_empty_dash_stop(&element)?;
589                stop.raw_children = capture_all_children(reader, b"ds")?;
590                dash.stops.push(stop);
591            }
592            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ds") => {
593                dash.stops.push(parse_empty_dash_stop(&element)?);
594            }
595            Event::Start(element) => dash
596                .raw_children
597                .push(dash.stops.len(), capture_element(reader, &element)?),
598            Event::Empty(element) => dash
599                .raw_children
600                .push(dash.stops.len(), capture_empty_element(&element)?),
601            Event::End(element) if matches_local_name(element.name().as_ref(), b"custDash") => {
602                break;
603            }
604            Event::Eof => return Err(missing_end("custDash")),
605            _ => {}
606        }
607        buffer.clear();
608    }
609    Ok(dash)
610}
611
612fn parse_empty_dash_stop(start: &BytesStart<'_>) -> Result<DashStop> {
613    DashStop::new(required_parse(start, b"d")?, required_parse(start, b"sp")?)
614}
615
616fn write_dash<W: Write>(writer: &mut Writer<W>, dash: &LineDash) -> Result<()> {
617    match dash {
618        LineDash::Preset(dash) => {
619            let mut start = BytesStart::new("a:prstDash");
620            start.push_attribute(("val", dash.value.as_str()));
621            if dash.raw_children.is_empty() {
622                return write_empty(writer, start);
623            }
624            write_start(writer, start)?;
625            emit_raw(writer, dash.raw_children.at(0))?;
626            write_end(writer, "a:prstDash")
627        }
628        LineDash::Custom(dash) => {
629            if dash.stops.is_empty() && dash.raw_children.is_empty() {
630                return write_empty(writer, BytesStart::new("a:custDash"));
631            }
632            write_start(writer, BytesStart::new("a:custDash"))?;
633            for boundary in 0..=dash.stops.len() {
634                emit_raw(writer, dash.raw_children.at(boundary))?;
635                if let Some(stop) = dash.stops.get(boundary) {
636                    write_dash_stop(writer, stop)?;
637                }
638            }
639            write_end(writer, "a:custDash")
640        }
641    }
642}
643
644fn write_dash_stop<W: Write>(writer: &mut Writer<W>, stop: &DashStop) -> Result<()> {
645    validate_positive_percentage("ds", "d", stop.dash)?;
646    validate_positive_percentage("ds", "sp", stop.space)?;
647    let dash = stop.dash.to_string();
648    let space = stop.space.to_string();
649    let mut start = BytesStart::new("a:ds");
650    start.push_attribute(("d", dash.as_str()));
651    start.push_attribute(("sp", space.as_str()));
652    if stop.raw_children.is_empty() {
653        return write_empty(writer, start);
654    }
655    write_start(writer, start)?;
656    emit_raw(writer, stop.raw_children.at(0))?;
657    write_end(writer, "a:ds")
658}
659
660fn parse_empty_join(start: &BytesStart<'_>) -> Result<LineJoin> {
661    match local_name(start.name().as_ref()) {
662        b"round" => Ok(LineJoin::round()),
663        b"bevel" => Ok(LineJoin::bevel()),
664        b"miter" => LineJoin::miter(optional_parse(start, b"lim")?),
665        _ => Err(unexpected(start)),
666    }
667}
668
669fn parse_join(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<LineJoin> {
670    let mut join = parse_empty_join(start)?;
671    let raw = capture_all_children(reader, local_name(start.name().as_ref()))?;
672    match &mut join {
673        LineJoin::Round { raw_children }
674        | LineJoin::Bevel { raw_children }
675        | LineJoin::Miter { raw_children, .. } => *raw_children = raw,
676    }
677    Ok(join)
678}
679
680fn write_join<W: Write>(writer: &mut Writer<W>, join: &LineJoin) -> Result<()> {
681    let (name, limit, raw_children) = match join {
682        LineJoin::Round { raw_children } => ("a:round", None, raw_children),
683        LineJoin::Bevel { raw_children } => ("a:bevel", None, raw_children),
684        LineJoin::Miter {
685            limit,
686            raw_children,
687        } => ("a:miter", *limit, raw_children),
688    };
689    if let Some(limit) = limit {
690        validate_positive_percentage("miter", "lim", limit)?;
691    }
692    let limit_text = limit.map(|value| value.to_string());
693    let mut start = BytesStart::new(name);
694    if let Some(limit) = limit_text.as_deref() {
695        start.push_attribute(("lim", limit));
696    }
697    if raw_children.is_empty() {
698        return write_empty(writer, start);
699    }
700    write_start(writer, start)?;
701    emit_raw(writer, raw_children.at(0))?;
702    write_end(writer, name)
703}
704
705fn parse_empty_line_end(start: &BytesStart<'_>) -> Result<LineEnd> {
706    Ok(LineEnd {
707        kind: optional_enum(start, b"type", LineEndType::parse)?,
708        width: optional_enum(start, b"w", LineEndSize::parse)?,
709        length: optional_enum(start, b"len", LineEndSize::parse)?,
710        raw_children: OrderedRawChildren::default(),
711    })
712}
713
714fn parse_line_end(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<LineEnd> {
715    let mut end = parse_empty_line_end(start)?;
716    end.raw_children = capture_all_children(reader, local_name(start.name().as_ref()))?;
717    Ok(end)
718}
719
720fn write_line_end<W: Write>(writer: &mut Writer<W>, name: &str, end: &LineEnd) -> Result<()> {
721    let mut start = BytesStart::new(name);
722    if let Some(kind) = end.kind {
723        start.push_attribute(("type", kind.as_str()));
724    }
725    if let Some(width) = end.width {
726        start.push_attribute(("w", width.as_str()));
727    }
728    if let Some(length) = end.length {
729        start.push_attribute(("len", length.as_str()));
730    }
731    if end.raw_children.is_empty() {
732        return write_empty(writer, start);
733    }
734    write_start(writer, start)?;
735    emit_raw(writer, end.raw_children.at(0))?;
736    write_end(writer, name)
737}
738
739fn capture_all_children(reader: &mut Reader<&[u8]>, end_name: &[u8]) -> Result<OrderedRawChildren> {
740    let mut raw_children = OrderedRawChildren::default();
741    let mut buffer = Vec::new();
742    loop {
743        match reader
744            .read_event_into(&mut buffer)
745            .map_err(OxmlError::from)?
746        {
747            Event::Start(element) => raw_children.push(0, capture_element(reader, &element)?),
748            Event::Empty(element) => raw_children.push(0, capture_empty_element(&element)?),
749            Event::End(element) if matches_local_name(element.name().as_ref(), end_name) => break,
750            Event::Eof => return Err(missing_end(&String::from_utf8_lossy(end_name))),
751            _ => {}
752        }
753        buffer.clear();
754    }
755    Ok(raw_children)
756}
757
758/// Keeps `a:extLst` last, after a head or tail end added later, as
759/// `CT_LineProperties` requires. Other raw children stay where they were read.
760fn raw_boundary(name: &[u8], boundary: usize) -> usize {
761    if matches_local_name(name, b"extLst") {
762        5
763    } else {
764        boundary
765    }
766}
767
768fn is_fill(name: &[u8]) -> bool {
769    matches!(
770        local_name(name),
771        b"noFill" | b"solidFill" | b"gradFill" | b"pattFill"
772    )
773}
774
775fn is_join(name: &[u8]) -> bool {
776    matches!(local_name(name), b"round" | b"bevel" | b"miter")
777}
778
779fn validate_line_width(value: u32) -> Result<()> {
780    if value <= MAX_LINE_WIDTH_EMU {
781        Ok(())
782    } else {
783        Err(LineError::LineWidthOutOfRange(value))
784    }
785}
786
787fn validate_positive_percentage(element: &str, attribute: &str, value: i32) -> Result<()> {
788    if value > 0 {
789        Ok(())
790    } else {
791        Err(LineError::InvalidAttribute {
792            element: element.to_owned(),
793            attribute: attribute.to_owned(),
794            value: value.to_string(),
795        })
796    }
797}
798
799fn required_parse<T: std::str::FromStr>(start: &BytesStart<'_>, name: &[u8]) -> Result<T> {
800    let value = get_attr(start, name).ok_or_else(|| LineError::MissingAttribute {
801        element: String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned(),
802        attribute: String::from_utf8_lossy(name).into_owned(),
803    })?;
804    value.parse().map_err(|_| invalid(start, name, value))
805}
806
807fn optional_parse<T: std::str::FromStr>(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<T>> {
808    get_attr(start, name)
809        .map(|value| value.parse().map_err(|_| invalid(start, name, value)))
810        .transpose()
811}
812
813fn optional_enum<T>(
814    start: &BytesStart<'_>,
815    name: &[u8],
816    parse: impl FnOnce(&str) -> Option<T>,
817) -> Result<Option<T>> {
818    get_attr(start, name)
819        .map(|value| parse(&value).ok_or_else(|| invalid(start, name, value)))
820        .transpose()
821}
822
823fn exact_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
824    for attribute in start.attributes() {
825        let attribute = attribute.map_err(OxmlError::from)?;
826        if attribute.key.as_ref() == name {
827            let value = attribute
828                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
829                .map_err(OxmlError::from)?;
830            return Ok(Some(value.into_owned()));
831        }
832    }
833    Ok(None)
834}
835
836fn optional_exact_parse<T: std::str::FromStr>(
837    start: &BytesStart<'_>,
838    name: &[u8],
839) -> Result<Option<T>> {
840    exact_attr(start, name)?
841        .map(|value| value.parse().map_err(|_| invalid(start, name, value)))
842        .transpose()
843}
844
845fn optional_exact_enum<T>(
846    start: &BytesStart<'_>,
847    name: &[u8],
848    parse: impl FnOnce(&str) -> Option<T>,
849) -> Result<Option<T>> {
850    exact_attr(start, name)?
851        .map(|value| parse(&value).ok_or_else(|| invalid(start, name, value)))
852        .transpose()
853}
854
855fn capture_raw_attributes(
856    start: &BytesStart<'_>,
857    modelled: &[&[u8]],
858) -> Result<Vec<(String, String)>> {
859    let mut raw = Vec::new();
860    for attribute in start.attributes() {
861        let attribute = attribute.map_err(OxmlError::from)?;
862        if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
863            continue;
864        }
865        let name = std::str::from_utf8(attribute.key.as_ref())
866            .map_err(OxmlError::from)?
867            .to_owned();
868        let value = attribute
869            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
870            .map_err(OxmlError::from)?
871            .into_owned();
872        raw.push((name, value));
873    }
874    Ok(raw)
875}
876
877fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
878    for (name, value) in attributes {
879        start.push_attribute((name.as_str(), value.as_str()));
880    }
881}
882
883fn invalid(start: &BytesStart<'_>, attribute: &[u8], value: String) -> LineError {
884    LineError::InvalidAttribute {
885        element: String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned(),
886        attribute: String::from_utf8_lossy(attribute).into_owned(),
887        value,
888    }
889}
890
891fn unexpected(start: &BytesStart<'_>) -> LineError {
892    LineError::UnexpectedElement(String::from_utf8_lossy(start.name().as_ref()).into_owned())
893}
894
895fn missing_end(name: &str) -> LineError {
896    LineError::Xml(OxmlError::MissingElement(format!("closing a:{name}")))
897}
898
899fn emit_raw<'a, W: Write>(
900    writer: &mut Writer<W>,
901    children: impl Iterator<Item = &'a [u8]>,
902) -> Result<()> {
903    for child in children {
904        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
905    }
906    Ok(())
907}
908
909fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
910    writer
911        .write_event(Event::Start(start))
912        .map_err(OxmlError::from)?;
913    Ok(())
914}
915
916fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
917    writer
918        .write_event(Event::Empty(start))
919        .map_err(OxmlError::from)?;
920    Ok(())
921}
922
923fn write_end<W: Write>(writer: &mut Writer<W>, name: &str) -> Result<()> {
924    writer
925        .write_event(Event::End(BytesEnd::new(name)))
926        .map_err(OxmlError::from)?;
927    Ok(())
928}
929
930#[cfg(test)]
931mod tests {
932    use super::{
933        CT_LineProperties, LineCap, LineDash, LineEnd, LineEndSize, LineEndType, LineError,
934        LineJoin, ST_PresetLineDashVal,
935    };
936
937    #[test]
938    fn every_preset_line_dash_value_maps_to_a_dash_array() {
939        let expected: &[(&str, &[u16])] = &[
940            ("solid", &[]),
941            ("dot", &[1, 3]),
942            ("sysDot", &[1, 1]),
943            ("dash", &[4, 3]),
944            ("sysDash", &[3, 1]),
945            ("lgDash", &[8, 3]),
946            ("dashDot", &[4, 3, 1, 3]),
947            ("sysDashDot", &[3, 1, 1, 1]),
948            ("lgDashDot", &[8, 3, 1, 3]),
949            ("lgDashDotDot", &[8, 3, 1, 3, 1, 3]),
950            ("sysDashDotDot", &[3, 1, 1, 1, 1, 1]),
951        ];
952
953        assert_eq!(ST_PresetLineDashVal::ALL.len(), expected.len());
954        for (value, (token, dash_array)) in ST_PresetLineDashVal::ALL.iter().zip(expected) {
955            assert_eq!(value.as_str(), *token);
956            assert_eq!(value.dash_array(), *dash_array);
957        }
958    }
959
960    #[test]
961    fn line_root_attributes_round_trip_without_loss() {
962        let line = CT_LineProperties::from_xml(
963            br#"<q:ln w="12700" cap="rnd" cmpd="dbl" algn="ctr" x:w="999"/>"#,
964        )
965        .unwrap();
966        assert_eq!(line.width, Some(12_700));
967        assert_eq!(
968            line.to_xml().unwrap(),
969            br#"<a:ln w="12700" cap="rnd" cmpd="dbl" algn="ctr" x:w="999"/>"#
970        );
971    }
972
973    #[test]
974    fn line_properties_round_trip_width_fill_dash_cap_join_and_ends() {
975        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>"#;
976        let parsed = CT_LineProperties::from_xml(xml).unwrap();
977
978        assert_eq!(parsed.width, Some(12_700));
979        assert_eq!(parsed.cap, Some(LineCap::Round));
980        assert!(matches!(&parsed.dash, Some(LineDash::Custom(dash)) if dash.stops.len() == 2));
981        assert!(matches!(
982            parsed.join,
983            Some(LineJoin::Miter {
984                limit: Some(800_000),
985                ..
986            })
987        ));
988        assert!(
989            matches!(parsed.head_end, Some(ref end) if end.kind == Some(LineEndType::Triangle) && end.width == Some(LineEndSize::Large) && end.length == Some(LineEndSize::Small))
990        );
991        assert!(
992            matches!(parsed.tail_end, Some(ref end) if end.kind == Some(LineEndType::Oval) && end.width == Some(LineEndSize::Medium) && end.length == Some(LineEndSize::Large))
993        );
994
995        let written = parsed.to_xml().unwrap();
996        assert_eq!(CT_LineProperties::from_xml(&written).unwrap(), parsed);
997    }
998
999    #[test]
1000    fn line_properties_write_schema_order_and_preserve_unknown_children() {
1001        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 &amp; 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>"#;
1002        let written = CT_LineProperties::from_xml(xml).unwrap().to_xml().unwrap();
1003
1004        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 &amp; 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>"#);
1005    }
1006
1007    #[test]
1008    fn line_extension_list_stays_last_when_ends_are_added() {
1009        let xml = br#"<a:ln><a:noFill/><a:bevel/><a:extLst><a:ext uri="{X}"/></a:extLst></a:ln>"#;
1010        let mut line = CT_LineProperties::from_xml(xml).unwrap();
1011        line.head_end = Some(LineEnd::default());
1012        line.tail_end = Some(LineEnd {
1013            kind: Some(LineEndType::Stealth),
1014            ..LineEnd::default()
1015        });
1016
1017        assert_eq!(
1018            line.to_xml().unwrap(),
1019            br#"<a:ln><a:noFill/><a:bevel/><a:headEnd/><a:tailEnd type="stealth"/><a:extLst><a:ext uri="{X}"/></a:extLst></a:ln>"#
1020        );
1021    }
1022
1023    #[test]
1024    fn malformed_line_values_return_errors_without_panicking() {
1025        let cases: &[&[u8]] = &[
1026            br#"<a:ln w="wide"/>"#,
1027            br#"<a:ln w="20116801"/>"#,
1028            br#"<a:ln cap="curved"/>"#,
1029            br#"<a:ln><a:prstDash val="longer"/></a:ln>"#,
1030            br#"<a:ln><a:custDash><a:ds sp="100000"/></a:custDash></a:ln>"#,
1031            br#"<a:ln><a:custDash><a:ds d="0" sp="100000"/></a:custDash></a:ln>"#,
1032            br#"<a:ln><a:miter lim="0"/></a:ln>"#,
1033            br#"<a:ln><a:headEnd type="spear"/></a:ln>"#,
1034            br#"<a:ln><a:tailEnd w="huge"/></a:ln>"#,
1035        ];
1036        for xml in cases {
1037            let result = std::panic::catch_unwind(|| CT_LineProperties::from_xml(xml));
1038            assert!(
1039                result.is_ok(),
1040                "line parser panicked for {}",
1041                String::from_utf8_lossy(xml)
1042            );
1043            assert!(
1044                result.unwrap().is_err(),
1045                "malformed line parsed successfully"
1046            );
1047        }
1048        assert!(matches!(
1049            CT_LineProperties::from_xml(cases[1]),
1050            Err(LineError::LineWidthOutOfRange(20_116_801))
1051        ));
1052    }
1053}