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