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 | 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/// One custom painted and unpainted dash pair, in thousandths of a percent.
194#[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/// A preset line dash and any extension children inside it.
214#[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/// Custom line dash stops in document order.
230#[derive(Clone, Debug, Default, Eq, PartialEq)]
231pub struct CustomDash {
232    pub stops: Vec<DashStop>,
233    raw_children: OrderedRawChildren,
234}
235
236/// Either a DrawingML preset or custom line dash.
237#[derive(Clone, Debug, Eq, PartialEq)]
238pub enum LineDash {
239    Preset(PresetDash),
240    Custom(CustomDash),
241}
242
243/// DrawingML line join behavior.
244#[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/// DrawingML line endpoint shape.
283#[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/// DrawingML line endpoint width or length.
319#[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/// One head or tail endpoint and its optional dimensions.
346#[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/// DrawingML `a:ln` properties.
355#[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    /// Parses one complete `a:ln` element with any namespace prefix.
371    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    /// Writes this line with canonical DrawingML prefixes and schema order.
501    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    /// Writes this line into an existing XML writer.
508    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 &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>"#;
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 &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>"#);
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}