Skip to main content

oxml_drawing/
geometry.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::error::Error;
3use std::f64::consts::PI;
4use std::fmt;
5use std::io::Write;
6
7use oxml_core::OxmlError;
8use oxml_core::raw_xml::{capture_element, capture_empty_element};
9use oxml_core::xml::{get_attr, local_name, matches_local_name};
10use quick_xml::events::{BytesEnd, BytesStart, Event};
11use quick_xml::{Reader, Writer};
12
13use crate::order::OrderedRawChildren;
14use crate::preset_shape_data::preset_shape_definition;
15
16const ANGLE_UNITS_PER_DEGREE: f64 = 60_000.0;
17const QUARTER_CIRCLE: f64 = 90.0 * ANGLE_UNITS_PER_DEGREE;
18const MAX_ARC_SEGMENTS: usize = 4_096;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum GuideOp {
22    MulDiv,
23    AddSub,
24    AddDiv,
25    IfElse,
26    Abs,
27    At2,
28    Cat2,
29    Cos,
30    Max,
31    Min,
32    Mod,
33    Pin,
34    Sat2,
35    Sin,
36    Sqrt,
37    Tan,
38    Val,
39}
40
41impl GuideOp {
42    pub fn parse(token: &str) -> Result<Self, GeometryError> {
43        match token {
44            "*/" => Ok(Self::MulDiv),
45            "+-" => Ok(Self::AddSub),
46            "+/" => Ok(Self::AddDiv),
47            "?:" => Ok(Self::IfElse),
48            "abs" => Ok(Self::Abs),
49            "at2" => Ok(Self::At2),
50            "cat2" => Ok(Self::Cat2),
51            "cos" => Ok(Self::Cos),
52            "max" => Ok(Self::Max),
53            "min" => Ok(Self::Min),
54            "mod" => Ok(Self::Mod),
55            "pin" => Ok(Self::Pin),
56            "sat2" => Ok(Self::Sat2),
57            "sin" => Ok(Self::Sin),
58            "sqrt" => Ok(Self::Sqrt),
59            "tan" => Ok(Self::Tan),
60            "val" => Ok(Self::Val),
61            _ => Err(GeometryError::UnknownGuideOperation(token.to_owned())),
62        }
63    }
64
65    fn argument_count(self) -> usize {
66        match self {
67            Self::Abs | Self::Sqrt | Self::Val => 1,
68            Self::At2 | Self::Cos | Self::Max | Self::Min | Self::Sin | Self::Tan => 2,
69            Self::MulDiv
70            | Self::AddSub
71            | Self::AddDiv
72            | Self::IfElse
73            | Self::Cat2
74            | Self::Mod
75            | Self::Pin
76            | Self::Sat2 => 3,
77        }
78    }
79
80    fn token(self) -> &'static str {
81        match self {
82            Self::MulDiv => "*/",
83            Self::AddSub => "+-",
84            Self::AddDiv => "+/",
85            Self::IfElse => "?:",
86            Self::Abs => "abs",
87            Self::At2 => "at2",
88            Self::Cat2 => "cat2",
89            Self::Cos => "cos",
90            Self::Max => "max",
91            Self::Min => "min",
92            Self::Mod => "mod",
93            Self::Pin => "pin",
94            Self::Sat2 => "sat2",
95            Self::Sin => "sin",
96            Self::Sqrt => "sqrt",
97            Self::Tan => "tan",
98            Self::Val => "val",
99        }
100    }
101}
102
103#[derive(Clone, Debug, PartialEq)]
104pub enum GuideOperand {
105    Literal(f64),
106    Guide(String),
107}
108
109impl GuideOperand {
110    pub fn parse(value: &str) -> Result<Self, GeometryError> {
111        match value.parse::<f64>() {
112            Ok(value) if value.is_finite() => Ok(Self::Literal(value)),
113            Ok(_) => Err(GeometryError::NonFiniteValue(value.to_owned())),
114            Err(_) if value.is_empty() => Err(GeometryError::EmptyGuideOperand),
115            Err(_) => Ok(Self::Guide(value.to_owned())),
116        }
117    }
118}
119
120impl From<f64> for GuideOperand {
121    fn from(value: f64) -> Self {
122        Self::Literal(value)
123    }
124}
125
126impl From<&str> for GuideOperand {
127    fn from(value: &str) -> Self {
128        Self::Guide(value.to_owned())
129    }
130}
131
132#[derive(Clone, Debug, PartialEq)]
133pub struct Guide {
134    pub name: String,
135    pub op: GuideOp,
136    pub args: Vec<GuideOperand>,
137}
138
139impl Guide {
140    pub fn parse(name: impl Into<String>, formula: &str) -> Result<Self, GeometryError> {
141        let mut parts = formula.split_ascii_whitespace();
142        let token = parts.next().ok_or(GeometryError::EmptyGuideFormula)?;
143        let op = GuideOp::parse(token)?;
144        let args = parts
145            .map(GuideOperand::parse)
146            .collect::<Result<Vec<_>, _>>()?;
147        let expected = op.argument_count();
148        if args.len() != expected {
149            return Err(GeometryError::WrongArgumentCount {
150                operation: token.to_owned(),
151                expected,
152                actual: args.len(),
153            });
154        }
155        Ok(Self {
156            name: name.into(),
157            op,
158            args,
159        })
160    }
161
162    fn formula(&self) -> String {
163        let mut formula = self.op.token().to_owned();
164        for argument in &self.args {
165            formula.push(' ');
166            match argument {
167                GuideOperand::Literal(value) => formula.push_str(&value.to_string()),
168                GuideOperand::Guide(name) => formula.push_str(name),
169            }
170        }
171        formula
172    }
173}
174
175#[derive(Clone, Debug, PartialEq)]
176pub enum PathCommand {
177    MoveTo {
178        x: GuideOperand,
179        y: GuideOperand,
180    },
181    LineTo {
182        x: GuideOperand,
183        y: GuideOperand,
184    },
185    CubicTo {
186        x1: GuideOperand,
187        y1: GuideOperand,
188        x2: GuideOperand,
189        y2: GuideOperand,
190        x: GuideOperand,
191        y: GuideOperand,
192    },
193    ArcTo {
194        width_radius: GuideOperand,
195        height_radius: GuideOperand,
196        start_angle: GuideOperand,
197        sweep_angle: GuideOperand,
198    },
199    Close,
200}
201
202#[derive(Clone, Copy, Debug, PartialEq)]
203pub enum EvaluatedPathCommand {
204    MoveTo {
205        x: f64,
206        y: f64,
207    },
208    LineTo {
209        x: f64,
210        y: f64,
211    },
212    CubicTo {
213        x1: f64,
214        y1: f64,
215        x2: f64,
216        y2: f64,
217        x: f64,
218        y: f64,
219    },
220    Close,
221}
222
223#[derive(Clone, Debug, PartialEq)]
224pub enum GeometryError {
225    Xml(String),
226    UnexpectedElement(String),
227    MissingAttribute {
228        element: String,
229        attribute: String,
230    },
231    InvalidAttribute {
232        element: String,
233        attribute: String,
234        value: String,
235    },
236    MissingPathList,
237    MissingPathDimensions,
238    EmptyGuideFormula,
239    EmptyGuideOperand,
240    UnknownPreset(String),
241    UnknownGuideOperation(String),
242    WrongArgumentCount {
243        operation: String,
244        expected: usize,
245        actual: usize,
246    },
247    UnknownGuide(String),
248    DuplicateGuide(String),
249    UnknownAdjustOverride(String),
250    DivisionByZero,
251    NonFiniteValue(String),
252    PathHasNoCurrentPoint,
253    InvalidArcRadius,
254    ArcSweepTooLarge,
255}
256
257impl fmt::Display for GeometryError {
258    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
259        match self {
260            Self::Xml(error) => formatter.write_str(error),
261            Self::UnexpectedElement(element) => {
262                write!(formatter, "unexpected custom geometry element: {element}")
263            }
264            Self::MissingAttribute { element, attribute } => {
265                write!(formatter, "DrawingML {element} requires @{attribute}")
266            }
267            Self::InvalidAttribute {
268                element,
269                attribute,
270                value,
271            } => write!(
272                formatter,
273                "DrawingML {element} has invalid @{attribute}: {value}"
274            ),
275            Self::MissingPathList => formatter.write_str("custom geometry requires a path list"),
276            Self::MissingPathDimensions => {
277                formatter.write_str("custom geometry path requires width and height")
278            }
279            Self::EmptyGuideFormula => formatter.write_str("empty guide formula"),
280            Self::EmptyGuideOperand => formatter.write_str("empty guide operand"),
281            Self::UnknownPreset(preset) => {
282                write!(formatter, "unknown DrawingML preset geometry: {preset}")
283            }
284            Self::UnknownGuideOperation(operation) => {
285                write!(formatter, "unknown guide operation: {operation}")
286            }
287            Self::WrongArgumentCount {
288                operation,
289                expected,
290                actual,
291            } => write!(
292                formatter,
293                "guide operation {operation} expects {expected} arguments, got {actual}"
294            ),
295            Self::UnknownGuide(name) => write!(formatter, "unknown guide: {name}"),
296            Self::DuplicateGuide(name) => write!(formatter, "duplicate guide: {name}"),
297            Self::UnknownAdjustOverride(name) => {
298                write!(formatter, "unknown adjust override: {name}")
299            }
300            Self::DivisionByZero => formatter.write_str("division by zero"),
301            Self::NonFiniteValue(context) => write!(formatter, "non-finite value: {context}"),
302            Self::PathHasNoCurrentPoint => {
303                formatter.write_str("path command requires a current point")
304            }
305            Self::InvalidArcRadius => formatter.write_str("arc radii must be positive"),
306            Self::ArcSweepTooLarge => formatter.write_str("arc sweep requires too many segments"),
307        }
308    }
309}
310
311impl Error for GeometryError {}
312
313impl From<OxmlError> for GeometryError {
314    fn from(error: OxmlError) -> Self {
315        Self::Xml(error.to_string())
316    }
317}
318
319#[allow(non_camel_case_types)]
320#[derive(Clone, Debug, PartialEq)]
321pub struct CT_AdjPoint2D {
322    pub x: GuideOperand,
323    pub y: GuideOperand,
324    raw_children: OrderedRawChildren,
325}
326
327#[allow(non_camel_case_types)]
328#[derive(Clone, Debug, PartialEq)]
329pub struct CT_GeomRect {
330    pub left: GuideOperand,
331    pub top: GuideOperand,
332    pub right: GuideOperand,
333    pub bottom: GuideOperand,
334    raw_children: OrderedRawChildren,
335}
336
337#[allow(non_camel_case_types)]
338#[derive(Clone, Debug, PartialEq)]
339pub enum CT_Path2DCommand {
340    MoveTo(CT_AdjPoint2D),
341    LineTo(CT_AdjPoint2D),
342    CubicTo {
343        control_1: CT_AdjPoint2D,
344        control_2: CT_AdjPoint2D,
345        end: CT_AdjPoint2D,
346    },
347    ArcTo {
348        width_radius: GuideOperand,
349        height_radius: GuideOperand,
350        start_angle: GuideOperand,
351        sweep_angle: GuideOperand,
352    },
353    Close,
354}
355
356#[derive(Clone, Debug, PartialEq)]
357struct PathCommandRecord {
358    command: CT_Path2DCommand,
359    raw_children: OrderedRawChildren,
360}
361
362#[allow(non_camel_case_types)]
363#[derive(Clone, Debug, PartialEq)]
364pub struct CT_Path2D {
365    pub width: Option<f64>,
366    pub height: Option<f64>,
367    pub fill: Option<String>,
368    pub stroke: Option<bool>,
369    pub extrusion_ok: Option<bool>,
370    commands: Vec<PathCommandRecord>,
371    raw_children: OrderedRawChildren,
372}
373
374impl CT_Path2D {
375    pub fn commands(&self) -> impl Iterator<Item = &CT_Path2DCommand> {
376        self.commands.iter().map(|record| &record.command)
377    }
378}
379
380#[derive(Clone, Debug, PartialEq)]
381struct GuideList {
382    guides: Vec<Guide>,
383    guide_raw_children: Vec<OrderedRawChildren>,
384    raw_children: OrderedRawChildren,
385}
386
387#[derive(Clone, Debug, PartialEq)]
388struct PathList {
389    paths: Vec<CT_Path2D>,
390    raw_children: OrderedRawChildren,
391}
392
393#[allow(non_camel_case_types)]
394#[derive(Clone, Debug, PartialEq)]
395pub struct CT_CustomGeometry2D {
396    adjust_values: Option<GuideList>,
397    guides: Option<GuideList>,
398    pub text_rectangle: Option<CT_GeomRect>,
399    path_list: PathList,
400    raw_children: OrderedRawChildren,
401}
402
403#[allow(non_camel_case_types)]
404#[derive(Clone, Debug, PartialEq)]
405pub struct CT_PresetGeometry2D {
406    pub preset: String,
407    adjust_values: Option<GuideList>,
408    raw_children: OrderedRawChildren,
409}
410
411#[derive(Clone, Copy, Debug, PartialEq)]
412pub struct EvaluatedTextRectangle {
413    pub left: f64,
414    pub top: f64,
415    pub right: f64,
416    pub bottom: f64,
417}
418
419#[derive(Clone, Debug, PartialEq)]
420pub struct EvaluatedCustomGeometry {
421    pub paths: Vec<Vec<EvaluatedPathCommand>>,
422    pub text_rectangle: Option<EvaluatedTextRectangle>,
423}
424
425impl CT_CustomGeometry2D {
426    /// Parses one complete `a:custGeom` element with any namespace prefix.
427    pub fn from_xml(xml: &[u8]) -> Result<Self, GeometryError> {
428        let mut reader = Reader::from_reader(xml);
429        let mut buffer = Vec::new();
430        loop {
431            match reader
432                .read_event_into(&mut buffer)
433                .map_err(|error| GeometryError::Xml(error.to_string()))?
434            {
435                Event::Start(element)
436                    if matches_local_name(element.name().as_ref(), b"custGeom") =>
437                {
438                    return Self::from_element(&mut reader, &element);
439                }
440                Event::Empty(element)
441                    if matches_local_name(element.name().as_ref(), b"custGeom") =>
442                {
443                    return Err(GeometryError::MissingPathList);
444                }
445                Event::Start(element) | Event::Empty(element) => {
446                    return Err(GeometryError::UnexpectedElement(
447                        String::from_utf8_lossy(element.name().as_ref()).into_owned(),
448                    ));
449                }
450                Event::Eof => {
451                    return Err(GeometryError::UnexpectedElement("EOF".to_owned()));
452                }
453                _ => {}
454            }
455            buffer.clear();
456        }
457    }
458
459    /// Parses an `a:custGeom` after the caller consumed its start event.
460    pub fn from_element(
461        reader: &mut Reader<&[u8]>,
462        start: &BytesStart<'_>,
463    ) -> Result<Self, GeometryError> {
464        if !matches_local_name(start.name().as_ref(), b"custGeom") {
465            return Err(GeometryError::UnexpectedElement(element_name(start)));
466        }
467
468        let mut adjust_values = None;
469        let mut guides = None;
470        let mut text_rectangle = None;
471        let mut path_list = None;
472        let mut raw_children = OrderedRawChildren::default();
473        let mut boundary = 0;
474        let mut buffer = Vec::new();
475
476        loop {
477            match reader
478                .read_event_into(&mut buffer)
479                .map_err(|error| GeometryError::Xml(error.to_string()))?
480            {
481                Event::Start(element) => match local_name(element.name().as_ref()) {
482                    b"avLst" if adjust_values.is_none() => {
483                        adjust_values = Some(parse_guide_list(reader, &element, b"avLst")?);
484                        boundary = boundary.max(1);
485                    }
486                    b"gdLst" if guides.is_none() => {
487                        guides = Some(parse_guide_list(reader, &element, b"gdLst")?);
488                        boundary = boundary.max(2);
489                    }
490                    b"rect" if text_rectangle.is_none() => {
491                        text_rectangle = Some(parse_rect(reader, &element)?);
492                        boundary = boundary.max(5);
493                    }
494                    b"pathLst" if path_list.is_none() => {
495                        path_list = Some(parse_path_list(reader, &element)?);
496                        boundary = boundary.max(6);
497                    }
498                    _ => raw_children.push(boundary, capture_element(reader, &element)?),
499                },
500                Event::Empty(element) => match local_name(element.name().as_ref()) {
501                    b"avLst" if adjust_values.is_none() => {
502                        adjust_values = Some(GuideList {
503                            guides: Vec::new(),
504                            guide_raw_children: Vec::new(),
505                            raw_children: OrderedRawChildren::default(),
506                        });
507                        boundary = boundary.max(1);
508                    }
509                    b"gdLst" if guides.is_none() => {
510                        guides = Some(GuideList {
511                            guides: Vec::new(),
512                            guide_raw_children: Vec::new(),
513                            raw_children: OrderedRawChildren::default(),
514                        });
515                        boundary = boundary.max(2);
516                    }
517                    b"rect" if text_rectangle.is_none() => {
518                        text_rectangle = Some(parse_empty_rect(&element)?);
519                        boundary = boundary.max(5);
520                    }
521                    b"pathLst" if path_list.is_none() => {
522                        path_list = Some(PathList {
523                            paths: Vec::new(),
524                            raw_children: OrderedRawChildren::default(),
525                        });
526                        boundary = boundary.max(6);
527                    }
528                    _ => raw_children.push(boundary, capture_empty_element(&element)?),
529                },
530                Event::End(element) if matches_local_name(element.name().as_ref(), b"custGeom") => {
531                    break;
532                }
533                Event::Eof => {
534                    return Err(GeometryError::Xml("missing closing a:custGeom".to_owned()));
535                }
536                _ => {}
537            }
538            buffer.clear();
539        }
540
541        Ok(Self {
542            adjust_values,
543            guides,
544            text_rectangle,
545            path_list: path_list.ok_or(GeometryError::MissingPathList)?,
546            raw_children,
547        })
548    }
549
550    pub fn adjust_values(&self) -> &[Guide] {
551        self.adjust_values
552            .as_ref()
553            .map_or(&[], |list| list.guides.as_slice())
554    }
555
556    pub fn guides(&self) -> &[Guide] {
557        self.guides
558            .as_ref()
559            .map_or(&[], |list| list.guides.as_slice())
560    }
561
562    pub fn paths(&self) -> &[CT_Path2D] {
563        &self.path_list.paths
564    }
565
566    /// Writes with the canonical `a:` prefix and DrawingML schema order.
567    pub fn to_xml(&self) -> Result<Vec<u8>, GeometryError> {
568        let mut writer = Writer::new(Vec::new());
569        writer
570            .write_event(Event::Start(BytesStart::new("a:custGeom")))
571            .map_err(|error| GeometryError::Xml(error.to_string()))?;
572        emit_raw(&mut writer, self.raw_children.at(0))?;
573        if let Some(list) = &self.adjust_values {
574            write_guide_list(&mut writer, "a:avLst", list)?;
575        }
576        emit_raw(&mut writer, self.raw_children.at(1))?;
577        if let Some(list) = &self.guides {
578            write_guide_list(&mut writer, "a:gdLst", list)?;
579        }
580        emit_raw(&mut writer, self.raw_children.at(2))?;
581        emit_raw(&mut writer, self.raw_children.at(3))?;
582        emit_raw(&mut writer, self.raw_children.at(4))?;
583        if let Some(rectangle) = &self.text_rectangle {
584            write_rect(&mut writer, rectangle)?;
585        }
586        emit_raw(&mut writer, self.raw_children.at(5))?;
587        write_path_list(&mut writer, &self.path_list)?;
588        emit_raw(&mut writer, self.raw_children.at(6))?;
589        writer
590            .write_event(Event::End(BytesEnd::new("a:custGeom")))
591            .map_err(|error| GeometryError::Xml(error.to_string()))?;
592        Ok(writer.into_inner())
593    }
594
595    /// Evaluates all paths and the text rectangle in each path's coordinate space.
596    pub fn evaluate(
597        &self,
598        overrides: &BTreeMap<String, f64>,
599    ) -> Result<EvaluatedCustomGeometry, GeometryError> {
600        self.evaluate_with_default_dimensions(overrides, None, None)
601    }
602
603    /// Evaluates paths with the containing shape size on omitted coordinate axes.
604    pub fn evaluate_with_size(
605        &self,
606        overrides: &BTreeMap<String, f64>,
607        size: (f64, f64),
608    ) -> Result<EvaluatedCustomGeometry, GeometryError> {
609        let text_dimensions = self.paths().first().map_or(size, |path| {
610            (path.width.unwrap_or(size.0), path.height.unwrap_or(size.1))
611        });
612        self.evaluate_with_default_dimensions(overrides, Some(size), Some(text_dimensions))
613    }
614
615    fn evaluate_with_default_dimensions(
616        &self,
617        overrides: &BTreeMap<String, f64>,
618        default_dimensions: Option<(f64, f64)>,
619        text_dimensions: Option<(f64, f64)>,
620    ) -> Result<EvaluatedCustomGeometry, GeometryError> {
621        let mut paths = Vec::with_capacity(self.path_list.paths.len());
622        let mut text_rectangle = None;
623        for (index, path) in self.path_list.paths.iter().enumerate() {
624            let width = path
625                .width
626                .or(default_dimensions.map(|dimensions| dimensions.0))
627                .ok_or(GeometryError::MissingPathDimensions)?;
628            let height = path
629                .height
630                .or(default_dimensions.map(|dimensions| dimensions.1))
631                .ok_or(GeometryError::MissingPathDimensions)?;
632            let mut evaluator = GuideEvaluator::new(width, height)?;
633            evaluator.apply_adjust_values(self.adjust_values(), overrides)?;
634            evaluator.evaluate_guides(self.guides())?;
635            let commands = path
636                .commands
637                .iter()
638                .map(|record| record.command.to_evaluator_command())
639                .collect::<Vec<_>>();
640            paths.push(evaluator.evaluate_path(&commands)?);
641            if index == 0 && default_dimensions.is_none() {
642                text_rectangle = self
643                    .text_rectangle
644                    .as_ref()
645                    .map(|rectangle| rectangle.evaluate(&evaluator))
646                    .transpose()?;
647            }
648        }
649        if let Some((width, height)) = text_dimensions {
650            let mut evaluator = GuideEvaluator::new(width, height)?;
651            evaluator.apply_adjust_values(self.adjust_values(), overrides)?;
652            evaluator.evaluate_guides(self.guides())?;
653            text_rectangle = self
654                .text_rectangle
655                .as_ref()
656                .map(|rectangle| rectangle.evaluate(&evaluator))
657                .transpose()?;
658        }
659        Ok(EvaluatedCustomGeometry {
660            paths,
661            text_rectangle,
662        })
663    }
664}
665
666impl CT_PresetGeometry2D {
667    /// Creates preset geometry with a canonical empty adjustment list.
668    pub fn new(preset: &str) -> Result<Self, GeometryError> {
669        if preset_shape_definition(preset).is_none() {
670            return Err(GeometryError::UnknownPreset(preset.to_owned()));
671        }
672        Ok(Self {
673            preset: preset.to_owned(),
674            adjust_values: Some(GuideList {
675                guides: Vec::new(),
676                guide_raw_children: Vec::new(),
677                raw_children: OrderedRawChildren::default(),
678            }),
679            raw_children: OrderedRawChildren::default(),
680        })
681    }
682
683    /// Parses one complete `a:prstGeom` element with any namespace prefix.
684    pub fn from_xml(xml: &[u8]) -> Result<Self, GeometryError> {
685        let mut reader = Reader::from_reader(xml);
686        let mut buffer = Vec::new();
687        loop {
688            match reader
689                .read_event_into(&mut buffer)
690                .map_err(|error| GeometryError::Xml(error.to_string()))?
691            {
692                Event::Start(element)
693                    if matches_local_name(element.name().as_ref(), b"prstGeom") =>
694                {
695                    return Self::from_element(&mut reader, &element);
696                }
697                Event::Empty(element)
698                    if matches_local_name(element.name().as_ref(), b"prstGeom") =>
699                {
700                    return Ok(Self {
701                        preset: required_attr(&element, b"prst")?,
702                        adjust_values: None,
703                        raw_children: OrderedRawChildren::default(),
704                    });
705                }
706                Event::Start(element) | Event::Empty(element) => {
707                    return Err(GeometryError::UnexpectedElement(element_name(&element)));
708                }
709                Event::Eof => {
710                    return Err(GeometryError::UnexpectedElement("EOF".to_owned()));
711                }
712                _ => {}
713            }
714            buffer.clear();
715        }
716    }
717
718    fn from_element(
719        reader: &mut Reader<&[u8]>,
720        start: &BytesStart<'_>,
721    ) -> Result<Self, GeometryError> {
722        let preset = required_attr(start, b"prst")?;
723        let mut adjust_values = None;
724        let mut raw_children = OrderedRawChildren::default();
725        let mut boundary = 0;
726        let mut buffer = Vec::new();
727        loop {
728            match reader
729                .read_event_into(&mut buffer)
730                .map_err(|error| GeometryError::Xml(error.to_string()))?
731            {
732                Event::Start(element)
733                    if matches_local_name(element.name().as_ref(), b"avLst")
734                        && adjust_values.is_none() =>
735                {
736                    adjust_values = Some(parse_guide_list(reader, &element, b"avLst")?);
737                    boundary = 1;
738                }
739                Event::Empty(element)
740                    if matches_local_name(element.name().as_ref(), b"avLst")
741                        && adjust_values.is_none() =>
742                {
743                    adjust_values = Some(GuideList {
744                        guides: Vec::new(),
745                        guide_raw_children: Vec::new(),
746                        raw_children: OrderedRawChildren::default(),
747                    });
748                    boundary = 1;
749                }
750                Event::Start(element) => {
751                    raw_children.push(boundary, capture_element(reader, &element)?)
752                }
753                Event::Empty(element) => {
754                    raw_children.push(boundary, capture_empty_element(&element)?)
755                }
756                Event::End(element) if matches_local_name(element.name().as_ref(), b"prstGeom") => {
757                    break;
758                }
759                Event::Eof => {
760                    return Err(GeometryError::Xml("missing closing a:prstGeom".to_owned()));
761                }
762                _ => {}
763            }
764            buffer.clear();
765        }
766        Ok(Self {
767            preset,
768            adjust_values,
769            raw_children,
770        })
771    }
772
773    pub fn adjust_values(&self) -> &[Guide] {
774        self.adjust_values
775            .as_ref()
776            .map_or(&[], |list| list.guides.as_slice())
777    }
778
779    /// Inserts or replaces one named `val` adjustment guide.
780    pub fn set_adjust_value(&mut self, name: &str, value: f64) -> Result<(), GeometryError> {
781        if !value.is_finite() {
782            return Err(GeometryError::NonFiniteValue(name.to_owned()));
783        }
784        let guide = Guide {
785            name: name.to_owned(),
786            op: GuideOp::Val,
787            args: vec![GuideOperand::Literal(value)],
788        };
789        let list = self.adjust_values.get_or_insert_with(|| GuideList {
790            guides: Vec::new(),
791            guide_raw_children: Vec::new(),
792            raw_children: OrderedRawChildren::default(),
793        });
794        if let Some(index) = list.guides.iter().position(|guide| guide.name == name) {
795            list.guides[index] = guide;
796        } else {
797            let trailing_boundary = list.guides.len();
798            list.raw_children.shift_boundaries_from(trailing_boundary);
799            list.guides.push(guide);
800            list.guide_raw_children.push(OrderedRawChildren::default());
801        }
802        Ok(())
803    }
804
805    /// Writes with the canonical `a:` prefix and DrawingML schema order.
806    pub fn to_xml(&self) -> Result<Vec<u8>, GeometryError> {
807        let mut writer = Writer::new(Vec::new());
808        let mut start = BytesStart::new("a:prstGeom");
809        start.push_attribute(("prst", self.preset.as_str()));
810        if self.adjust_values.is_none() && self.raw_children.is_empty() {
811            writer
812                .write_event(Event::Empty(start))
813                .map_err(|error| GeometryError::Xml(error.to_string()))?;
814            return Ok(writer.into_inner());
815        }
816        writer
817            .write_event(Event::Start(start))
818            .map_err(|error| GeometryError::Xml(error.to_string()))?;
819        emit_raw(&mut writer, self.raw_children.at(0))?;
820        if let Some(list) = &self.adjust_values {
821            write_guide_list(&mut writer, "a:avLst", list)?;
822        }
823        emit_raw(&mut writer, self.raw_children.at(1))?;
824        writer
825            .write_event(Event::End(BytesEnd::new("a:prstGeom")))
826            .map_err(|error| GeometryError::Xml(error.to_string()))?;
827        Ok(writer.into_inner())
828    }
829
830    /// Evaluates a known preset in the shape's coordinate space.
831    pub fn evaluate(
832        &self,
833        size: (f64, f64),
834    ) -> Result<Option<EvaluatedCustomGeometry>, GeometryError> {
835        let Some(xml) = preset_shape_definition(&self.preset) else {
836            return Ok(None);
837        };
838        let definition = CT_CustomGeometry2D::from_xml(xml)?;
839        let mut override_evaluator = GuideEvaluator::new(size.0, size.1)?;
840        override_evaluator.apply_adjust_values(self.adjust_values(), &BTreeMap::new())?;
841        let overrides = self
842            .adjust_values()
843            .iter()
844            .map(|guide| Ok((guide.name.clone(), override_evaluator.value(&guide.name)?)))
845            .collect::<Result<BTreeMap<_, _>, GeometryError>>()?;
846        let mut evaluated =
847            definition.evaluate_with_default_dimensions(&overrides, Some(size), Some(size))?;
848        for (commands, path) in evaluated.paths.iter_mut().zip(definition.paths()) {
849            let scale_x = path.width.map_or(1.0, |width| size.0 / width);
850            let scale_y = path.height.map_or(1.0, |height| size.1 / height);
851            for command in commands {
852                scale_evaluated_path_command(command, scale_x, scale_y);
853            }
854        }
855        Ok(Some(evaluated))
856    }
857}
858
859fn scale_evaluated_path_command(command: &mut EvaluatedPathCommand, scale_x: f64, scale_y: f64) {
860    match command {
861        EvaluatedPathCommand::MoveTo { x, y } | EvaluatedPathCommand::LineTo { x, y } => {
862            *x *= scale_x;
863            *y *= scale_y;
864        }
865        EvaluatedPathCommand::CubicTo {
866            x1,
867            y1,
868            x2,
869            y2,
870            x,
871            y,
872        } => {
873            *x1 *= scale_x;
874            *y1 *= scale_y;
875            *x2 *= scale_x;
876            *y2 *= scale_y;
877            *x *= scale_x;
878            *y *= scale_y;
879        }
880        EvaluatedPathCommand::Close => {}
881    }
882}
883
884impl CT_GeomRect {
885    fn evaluate(
886        &self,
887        evaluator: &GuideEvaluator,
888    ) -> Result<EvaluatedTextRectangle, GeometryError> {
889        Ok(EvaluatedTextRectangle {
890            left: evaluator.resolve(&self.left)?,
891            top: evaluator.resolve(&self.top)?,
892            right: evaluator.resolve(&self.right)?,
893            bottom: evaluator.resolve(&self.bottom)?,
894        })
895    }
896}
897
898impl CT_Path2DCommand {
899    fn to_evaluator_command(&self) -> PathCommand {
900        match self {
901            Self::MoveTo(point) => PathCommand::MoveTo {
902                x: point.x.clone(),
903                y: point.y.clone(),
904            },
905            Self::LineTo(point) => PathCommand::LineTo {
906                x: point.x.clone(),
907                y: point.y.clone(),
908            },
909            Self::CubicTo {
910                control_1,
911                control_2,
912                end,
913            } => PathCommand::CubicTo {
914                x1: control_1.x.clone(),
915                y1: control_1.y.clone(),
916                x2: control_2.x.clone(),
917                y2: control_2.y.clone(),
918                x: end.x.clone(),
919                y: end.y.clone(),
920            },
921            Self::ArcTo {
922                width_radius,
923                height_radius,
924                start_angle,
925                sweep_angle,
926            } => PathCommand::ArcTo {
927                width_radius: width_radius.clone(),
928                height_radius: height_radius.clone(),
929                start_angle: start_angle.clone(),
930                sweep_angle: sweep_angle.clone(),
931            },
932            Self::Close => PathCommand::Close,
933        }
934    }
935}
936
937fn parse_guide_list(
938    reader: &mut Reader<&[u8]>,
939    _start: &BytesStart<'_>,
940    end_name: &[u8],
941) -> Result<GuideList, GeometryError> {
942    let mut guides = Vec::new();
943    let mut guide_raw_children = Vec::new();
944    let mut raw_children = OrderedRawChildren::default();
945    let mut buffer = Vec::new();
946    loop {
947        match reader
948            .read_event_into(&mut buffer)
949            .map_err(|error| GeometryError::Xml(error.to_string()))?
950        {
951            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"gd") => {
952                guides.push(parse_guide(&element)?);
953                guide_raw_children.push(OrderedRawChildren::default());
954            }
955            Event::Start(element) if matches_local_name(element.name().as_ref(), b"gd") => {
956                let guide = parse_guide(&element)?;
957                let mut children = OrderedRawChildren::default();
958                consume_leaf_children(reader, b"gd", &mut children, 0)?;
959                guides.push(guide);
960                guide_raw_children.push(children);
961            }
962            Event::Start(element) => {
963                raw_children.push(guides.len(), capture_element(reader, &element)?)
964            }
965            Event::Empty(element) => {
966                raw_children.push(guides.len(), capture_empty_element(&element)?)
967            }
968            Event::End(element) if matches_local_name(element.name().as_ref(), end_name) => break,
969            Event::Eof => {
970                return Err(GeometryError::Xml(format!(
971                    "missing closing a:{}",
972                    String::from_utf8_lossy(end_name)
973                )));
974            }
975            _ => {}
976        }
977        buffer.clear();
978    }
979    Ok(GuideList {
980        guides,
981        guide_raw_children,
982        raw_children,
983    })
984}
985
986fn consume_leaf_children(
987    reader: &mut Reader<&[u8]>,
988    end_name: &[u8],
989    raw_children: &mut OrderedRawChildren,
990    boundary: usize,
991) -> Result<(), GeometryError> {
992    let mut buffer = Vec::new();
993    loop {
994        match reader
995            .read_event_into(&mut buffer)
996            .map_err(|error| GeometryError::Xml(error.to_string()))?
997        {
998            Event::Start(element) => {
999                raw_children.push(boundary, capture_element(reader, &element)?)
1000            }
1001            Event::Empty(element) => raw_children.push(boundary, capture_empty_element(&element)?),
1002            Event::End(element) if matches_local_name(element.name().as_ref(), end_name) => break,
1003            Event::Eof => {
1004                return Err(GeometryError::Xml(format!(
1005                    "missing closing a:{}",
1006                    String::from_utf8_lossy(end_name)
1007                )));
1008            }
1009            _ => {}
1010        }
1011        buffer.clear();
1012    }
1013    Ok(())
1014}
1015
1016fn parse_guide(element: &BytesStart<'_>) -> Result<Guide, GeometryError> {
1017    Guide::parse(
1018        required_attr(element, b"name")?,
1019        &required_attr(element, b"fmla")?,
1020    )
1021}
1022
1023fn parse_empty_rect(element: &BytesStart<'_>) -> Result<CT_GeomRect, GeometryError> {
1024    Ok(CT_GeomRect {
1025        left: required_operand(element, b"l")?,
1026        top: required_operand(element, b"t")?,
1027        right: required_operand(element, b"r")?,
1028        bottom: required_operand(element, b"b")?,
1029        raw_children: OrderedRawChildren::default(),
1030    })
1031}
1032
1033fn parse_rect(
1034    reader: &mut Reader<&[u8]>,
1035    element: &BytesStart<'_>,
1036) -> Result<CT_GeomRect, GeometryError> {
1037    let mut rectangle = parse_empty_rect(element)?;
1038    consume_leaf_children(reader, b"rect", &mut rectangle.raw_children, 0)?;
1039    Ok(rectangle)
1040}
1041
1042fn parse_path_list(
1043    reader: &mut Reader<&[u8]>,
1044    _start: &BytesStart<'_>,
1045) -> Result<PathList, GeometryError> {
1046    let mut paths = Vec::new();
1047    let mut raw_children = OrderedRawChildren::default();
1048    let mut buffer = Vec::new();
1049    loop {
1050        match reader
1051            .read_event_into(&mut buffer)
1052            .map_err(|error| GeometryError::Xml(error.to_string()))?
1053        {
1054            Event::Start(element) if matches_local_name(element.name().as_ref(), b"path") => {
1055                paths.push(parse_path(reader, &element)?);
1056            }
1057            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"path") => {
1058                paths.push(parse_empty_path(&element)?);
1059            }
1060            Event::Start(element) => {
1061                raw_children.push(paths.len(), capture_element(reader, &element)?)
1062            }
1063            Event::Empty(element) => {
1064                raw_children.push(paths.len(), capture_empty_element(&element)?)
1065            }
1066            Event::End(element) if matches_local_name(element.name().as_ref(), b"pathLst") => {
1067                break;
1068            }
1069            Event::Eof => {
1070                return Err(GeometryError::Xml("missing closing a:pathLst".to_owned()));
1071            }
1072            _ => {}
1073        }
1074        buffer.clear();
1075    }
1076    Ok(PathList {
1077        paths,
1078        raw_children,
1079    })
1080}
1081
1082fn parse_empty_path(element: &BytesStart<'_>) -> Result<CT_Path2D, GeometryError> {
1083    Ok(CT_Path2D {
1084        width: optional_f64(element, b"w")?,
1085        height: optional_f64(element, b"h")?,
1086        fill: get_attr(element, b"fill"),
1087        stroke: optional_bool(element, b"stroke")?,
1088        extrusion_ok: optional_bool(element, b"extrusionOk")?,
1089        commands: Vec::new(),
1090        raw_children: OrderedRawChildren::default(),
1091    })
1092}
1093
1094fn parse_path(
1095    reader: &mut Reader<&[u8]>,
1096    element: &BytesStart<'_>,
1097) -> Result<CT_Path2D, GeometryError> {
1098    let mut path = parse_empty_path(element)?;
1099    let mut buffer = Vec::new();
1100    loop {
1101        match reader
1102            .read_event_into(&mut buffer)
1103            .map_err(|error| GeometryError::Xml(error.to_string()))?
1104        {
1105            Event::Start(element) => {
1106                if let Some(command) = parse_path_command(reader, &element)? {
1107                    path.commands.push(command);
1108                } else {
1109                    path.raw_children
1110                        .push(path.commands.len(), capture_element(reader, &element)?);
1111                }
1112            }
1113            Event::Empty(element) => {
1114                if let Some(command) = parse_empty_path_command(&element)? {
1115                    path.commands.push(command);
1116                } else {
1117                    path.raw_children
1118                        .push(path.commands.len(), capture_empty_element(&element)?);
1119                }
1120            }
1121            Event::End(element) if matches_local_name(element.name().as_ref(), b"path") => break,
1122            Event::Eof => {
1123                return Err(GeometryError::Xml("missing closing a:path".to_owned()));
1124            }
1125            _ => {}
1126        }
1127        buffer.clear();
1128    }
1129    Ok(path)
1130}
1131
1132fn parse_path_command(
1133    reader: &mut Reader<&[u8]>,
1134    element: &BytesStart<'_>,
1135) -> Result<Option<PathCommandRecord>, GeometryError> {
1136    match local_name(element.name().as_ref()) {
1137        b"moveTo" => parse_point_command(reader, b"moveTo", 1, |mut points| {
1138            CT_Path2DCommand::MoveTo(points.remove(0))
1139        })
1140        .map(Some),
1141        b"lnTo" => parse_point_command(reader, b"lnTo", 1, |mut points| {
1142            CT_Path2DCommand::LineTo(points.remove(0))
1143        })
1144        .map(Some),
1145        b"cubicBezTo" => parse_point_command(reader, b"cubicBezTo", 3, |mut points| {
1146            CT_Path2DCommand::CubicTo {
1147                control_1: points.remove(0),
1148                control_2: points.remove(0),
1149                end: points.remove(0),
1150            }
1151        })
1152        .map(Some),
1153        b"arcTo" => {
1154            let command = parse_arc(element)?;
1155            let mut raw_children = OrderedRawChildren::default();
1156            consume_leaf_children(reader, b"arcTo", &mut raw_children, 0)?;
1157            Ok(Some(PathCommandRecord {
1158                command,
1159                raw_children,
1160            }))
1161        }
1162        b"close" => {
1163            let mut raw_children = OrderedRawChildren::default();
1164            consume_leaf_children(reader, b"close", &mut raw_children, 0)?;
1165            Ok(Some(PathCommandRecord {
1166                command: CT_Path2DCommand::Close,
1167                raw_children,
1168            }))
1169        }
1170        _ => Ok(None),
1171    }
1172}
1173
1174fn parse_empty_path_command(
1175    element: &BytesStart<'_>,
1176) -> Result<Option<PathCommandRecord>, GeometryError> {
1177    match local_name(element.name().as_ref()) {
1178        b"arcTo" => Ok(Some(PathCommandRecord {
1179            command: parse_arc(element)?,
1180            raw_children: OrderedRawChildren::default(),
1181        })),
1182        b"close" => Ok(Some(PathCommandRecord {
1183            command: CT_Path2DCommand::Close,
1184            raw_children: OrderedRawChildren::default(),
1185        })),
1186        b"moveTo" | b"lnTo" | b"cubicBezTo" => Err(GeometryError::Xml(format!(
1187            "DrawingML {} requires point children",
1188            element_name(element)
1189        ))),
1190        _ => Ok(None),
1191    }
1192}
1193
1194fn parse_point_command(
1195    reader: &mut Reader<&[u8]>,
1196    end_name: &[u8],
1197    expected_points: usize,
1198    make_command: impl FnOnce(Vec<CT_AdjPoint2D>) -> CT_Path2DCommand,
1199) -> Result<PathCommandRecord, GeometryError> {
1200    let mut points = Vec::new();
1201    let mut raw_children = OrderedRawChildren::default();
1202    let mut buffer = Vec::new();
1203    loop {
1204        match reader
1205            .read_event_into(&mut buffer)
1206            .map_err(|error| GeometryError::Xml(error.to_string()))?
1207        {
1208            Event::Empty(element)
1209                if matches_local_name(element.name().as_ref(), b"pt")
1210                    && points.len() < expected_points =>
1211            {
1212                points.push(parse_point(&element)?);
1213            }
1214            Event::Start(element)
1215                if matches_local_name(element.name().as_ref(), b"pt")
1216                    && points.len() < expected_points =>
1217            {
1218                let mut point = parse_point(&element)?;
1219                consume_leaf_children(reader, b"pt", &mut point.raw_children, 0)?;
1220                points.push(point);
1221            }
1222            Event::Start(element) => {
1223                raw_children.push(points.len(), capture_element(reader, &element)?)
1224            }
1225            Event::Empty(element) => {
1226                raw_children.push(points.len(), capture_empty_element(&element)?)
1227            }
1228            Event::End(element) if matches_local_name(element.name().as_ref(), end_name) => break,
1229            Event::Eof => {
1230                return Err(GeometryError::Xml(format!(
1231                    "missing closing a:{}",
1232                    String::from_utf8_lossy(end_name)
1233                )));
1234            }
1235            _ => {}
1236        }
1237        buffer.clear();
1238    }
1239    if points.len() != expected_points {
1240        return Err(GeometryError::Xml(format!(
1241            "DrawingML {} requires {expected_points} point children",
1242            String::from_utf8_lossy(end_name)
1243        )));
1244    }
1245    Ok(PathCommandRecord {
1246        command: make_command(points),
1247        raw_children,
1248    })
1249}
1250
1251fn parse_point(element: &BytesStart<'_>) -> Result<CT_AdjPoint2D, GeometryError> {
1252    Ok(CT_AdjPoint2D {
1253        x: required_operand(element, b"x")?,
1254        y: required_operand(element, b"y")?,
1255        raw_children: OrderedRawChildren::default(),
1256    })
1257}
1258
1259fn parse_arc(element: &BytesStart<'_>) -> Result<CT_Path2DCommand, GeometryError> {
1260    Ok(CT_Path2DCommand::ArcTo {
1261        width_radius: required_operand(element, b"wR")?,
1262        height_radius: required_operand(element, b"hR")?,
1263        start_angle: required_operand(element, b"stAng")?,
1264        sweep_angle: required_operand(element, b"swAng")?,
1265    })
1266}
1267
1268fn write_guide_list<W: Write>(
1269    writer: &mut Writer<W>,
1270    tag: &str,
1271    list: &GuideList,
1272) -> Result<(), GeometryError> {
1273    if list.guides.is_empty() && list.raw_children.is_empty() {
1274        writer
1275            .write_event(Event::Empty(BytesStart::new(tag)))
1276            .map_err(|error| GeometryError::Xml(error.to_string()))?;
1277        return Ok(());
1278    }
1279    writer
1280        .write_event(Event::Start(BytesStart::new(tag)))
1281        .map_err(|error| GeometryError::Xml(error.to_string()))?;
1282    for (index, guide) in list.guides.iter().enumerate() {
1283        emit_raw(writer, list.raw_children.at(index))?;
1284        let formula = guide.formula();
1285        let mut element = BytesStart::new("a:gd");
1286        element.push_attribute(("name", guide.name.as_str()));
1287        element.push_attribute(("fmla", formula.as_str()));
1288        let children = &list.guide_raw_children[index];
1289        write_leaf_with_raw(writer, element, "a:gd", children)?;
1290    }
1291    emit_raw(writer, list.raw_children.at(list.guides.len()))?;
1292    writer
1293        .write_event(Event::End(BytesEnd::new(tag)))
1294        .map_err(|error| GeometryError::Xml(error.to_string()))?;
1295    Ok(())
1296}
1297
1298fn write_rect<W: Write>(
1299    writer: &mut Writer<W>,
1300    rectangle: &CT_GeomRect,
1301) -> Result<(), GeometryError> {
1302    let values = [
1303        operand_text(&rectangle.left),
1304        operand_text(&rectangle.top),
1305        operand_text(&rectangle.right),
1306        operand_text(&rectangle.bottom),
1307    ];
1308    let mut element = BytesStart::new("a:rect");
1309    element.push_attribute(("l", values[0].as_str()));
1310    element.push_attribute(("t", values[1].as_str()));
1311    element.push_attribute(("r", values[2].as_str()));
1312    element.push_attribute(("b", values[3].as_str()));
1313    if rectangle.raw_children.is_empty() {
1314        writer
1315            .write_event(Event::Empty(element))
1316            .map_err(|error| GeometryError::Xml(error.to_string()))?;
1317    } else {
1318        writer
1319            .write_event(Event::Start(element))
1320            .map_err(|error| GeometryError::Xml(error.to_string()))?;
1321        emit_raw(writer, rectangle.raw_children.at(0))?;
1322        writer
1323            .write_event(Event::End(BytesEnd::new("a:rect")))
1324            .map_err(|error| GeometryError::Xml(error.to_string()))?;
1325    }
1326    Ok(())
1327}
1328
1329fn write_path_list<W: Write>(writer: &mut Writer<W>, list: &PathList) -> Result<(), GeometryError> {
1330    writer
1331        .write_event(Event::Start(BytesStart::new("a:pathLst")))
1332        .map_err(|error| GeometryError::Xml(error.to_string()))?;
1333    for (index, path) in list.paths.iter().enumerate() {
1334        emit_raw(writer, list.raw_children.at(index))?;
1335        write_path(writer, path)?;
1336    }
1337    emit_raw(writer, list.raw_children.at(list.paths.len()))?;
1338    writer
1339        .write_event(Event::End(BytesEnd::new("a:pathLst")))
1340        .map_err(|error| GeometryError::Xml(error.to_string()))?;
1341    Ok(())
1342}
1343
1344fn write_path<W: Write>(writer: &mut Writer<W>, path: &CT_Path2D) -> Result<(), GeometryError> {
1345    let width = path.width.map(|value| value.to_string());
1346    let height = path.height.map(|value| value.to_string());
1347    let mut element = BytesStart::new("a:path");
1348    if let Some(value) = width.as_deref() {
1349        element.push_attribute(("w", value));
1350    }
1351    if let Some(value) = height.as_deref() {
1352        element.push_attribute(("h", value));
1353    }
1354    if let Some(value) = path.fill.as_deref() {
1355        element.push_attribute(("fill", value));
1356    }
1357    if let Some(value) = path.stroke {
1358        element.push_attribute(("stroke", if value { "1" } else { "0" }));
1359    }
1360    if let Some(value) = path.extrusion_ok {
1361        element.push_attribute(("extrusionOk", if value { "1" } else { "0" }));
1362    }
1363    if path.commands.is_empty() && path.raw_children.is_empty() {
1364        writer
1365            .write_event(Event::Empty(element))
1366            .map_err(|error| GeometryError::Xml(error.to_string()))?;
1367        return Ok(());
1368    }
1369    writer
1370        .write_event(Event::Start(element))
1371        .map_err(|error| GeometryError::Xml(error.to_string()))?;
1372    for (index, command) in path.commands.iter().enumerate() {
1373        emit_raw(writer, path.raw_children.at(index))?;
1374        write_path_command(writer, command)?;
1375    }
1376    emit_raw(writer, path.raw_children.at(path.commands.len()))?;
1377    writer
1378        .write_event(Event::End(BytesEnd::new("a:path")))
1379        .map_err(|error| GeometryError::Xml(error.to_string()))?;
1380    Ok(())
1381}
1382
1383fn write_path_command<W: Write>(
1384    writer: &mut Writer<W>,
1385    record: &PathCommandRecord,
1386) -> Result<(), GeometryError> {
1387    match &record.command {
1388        CT_Path2DCommand::MoveTo(point) => {
1389            write_point_command(writer, "a:moveTo", std::slice::from_ref(point), record)?
1390        }
1391        CT_Path2DCommand::LineTo(point) => {
1392            write_point_command(writer, "a:lnTo", std::slice::from_ref(point), record)?
1393        }
1394        CT_Path2DCommand::CubicTo {
1395            control_1,
1396            control_2,
1397            end,
1398        } => write_point_command(
1399            writer,
1400            "a:cubicBezTo",
1401            &[control_1.clone(), control_2.clone(), end.clone()],
1402            record,
1403        )?,
1404        CT_Path2DCommand::ArcTo {
1405            width_radius,
1406            height_radius,
1407            start_angle,
1408            sweep_angle,
1409        } => {
1410            let values = [
1411                operand_text(width_radius),
1412                operand_text(height_radius),
1413                operand_text(start_angle),
1414                operand_text(sweep_angle),
1415            ];
1416            let mut element = BytesStart::new("a:arcTo");
1417            element.push_attribute(("wR", values[0].as_str()));
1418            element.push_attribute(("hR", values[1].as_str()));
1419            element.push_attribute(("stAng", values[2].as_str()));
1420            element.push_attribute(("swAng", values[3].as_str()));
1421            write_leaf_with_raw(writer, element, "a:arcTo", &record.raw_children)?;
1422        }
1423        CT_Path2DCommand::Close => {
1424            write_leaf_with_raw(
1425                writer,
1426                BytesStart::new("a:close"),
1427                "a:close",
1428                &record.raw_children,
1429            )?;
1430        }
1431    }
1432    Ok(())
1433}
1434
1435fn write_point_command<W: Write>(
1436    writer: &mut Writer<W>,
1437    tag: &str,
1438    points: &[CT_AdjPoint2D],
1439    record: &PathCommandRecord,
1440) -> Result<(), GeometryError> {
1441    writer
1442        .write_event(Event::Start(BytesStart::new(tag)))
1443        .map_err(|error| GeometryError::Xml(error.to_string()))?;
1444    for (index, point) in points.iter().enumerate() {
1445        emit_raw(writer, record.raw_children.at(index))?;
1446        write_point(writer, point)?;
1447    }
1448    emit_raw(writer, record.raw_children.at(points.len()))?;
1449    writer
1450        .write_event(Event::End(BytesEnd::new(tag)))
1451        .map_err(|error| GeometryError::Xml(error.to_string()))?;
1452    Ok(())
1453}
1454
1455fn write_point<W: Write>(
1456    writer: &mut Writer<W>,
1457    point: &CT_AdjPoint2D,
1458) -> Result<(), GeometryError> {
1459    let x = operand_text(&point.x);
1460    let y = operand_text(&point.y);
1461    let mut element = BytesStart::new("a:pt");
1462    element.push_attribute(("x", x.as_str()));
1463    element.push_attribute(("y", y.as_str()));
1464    write_leaf_with_raw(writer, element, "a:pt", &point.raw_children)?;
1465    Ok(())
1466}
1467
1468fn write_leaf_with_raw<W: Write>(
1469    writer: &mut Writer<W>,
1470    element: BytesStart<'_>,
1471    tag: &str,
1472    raw_children: &OrderedRawChildren,
1473) -> Result<(), GeometryError> {
1474    if raw_children.is_empty() {
1475        writer
1476            .write_event(Event::Empty(element))
1477            .map_err(|error| GeometryError::Xml(error.to_string()))?;
1478    } else {
1479        writer
1480            .write_event(Event::Start(element))
1481            .map_err(|error| GeometryError::Xml(error.to_string()))?;
1482        emit_raw(writer, raw_children.at(0))?;
1483        writer
1484            .write_event(Event::End(BytesEnd::new(tag)))
1485            .map_err(|error| GeometryError::Xml(error.to_string()))?;
1486    }
1487    Ok(())
1488}
1489
1490fn emit_raw<'a, W: Write>(
1491    writer: &mut Writer<W>,
1492    children: impl Iterator<Item = &'a [u8]>,
1493) -> Result<(), GeometryError> {
1494    for child in children {
1495        writer
1496            .get_mut()
1497            .write_all(child)
1498            .map_err(|error| GeometryError::Xml(error.to_string()))?;
1499    }
1500    Ok(())
1501}
1502
1503fn required_operand(
1504    element: &BytesStart<'_>,
1505    attribute: &[u8],
1506) -> Result<GuideOperand, GeometryError> {
1507    GuideOperand::parse(&required_attr(element, attribute)?)
1508}
1509
1510fn required_attr(element: &BytesStart<'_>, attribute: &[u8]) -> Result<String, GeometryError> {
1511    get_attr(element, attribute).ok_or_else(|| GeometryError::MissingAttribute {
1512        element: element_name(element),
1513        attribute: String::from_utf8_lossy(attribute).into_owned(),
1514    })
1515}
1516
1517fn optional_f64(element: &BytesStart<'_>, attribute: &[u8]) -> Result<Option<f64>, GeometryError> {
1518    get_attr(element, attribute)
1519        .map(|value| {
1520            value
1521                .parse::<f64>()
1522                .ok()
1523                .filter(|value| value.is_finite() && *value >= 0.0)
1524                .ok_or_else(|| invalid_attribute(element, attribute, value))
1525        })
1526        .transpose()
1527}
1528
1529fn optional_bool(
1530    element: &BytesStart<'_>,
1531    attribute: &[u8],
1532) -> Result<Option<bool>, GeometryError> {
1533    get_attr(element, attribute)
1534        .map(|value| match value.as_str() {
1535            "1" | "true" => Ok(true),
1536            "0" | "false" => Ok(false),
1537            _ => Err(invalid_attribute(element, attribute, value)),
1538        })
1539        .transpose()
1540}
1541
1542fn invalid_attribute(element: &BytesStart<'_>, attribute: &[u8], value: String) -> GeometryError {
1543    GeometryError::InvalidAttribute {
1544        element: element_name(element),
1545        attribute: String::from_utf8_lossy(attribute).into_owned(),
1546        value,
1547    }
1548}
1549
1550fn element_name(element: &BytesStart<'_>) -> String {
1551    String::from_utf8_lossy(local_name(element.name().as_ref())).into_owned()
1552}
1553
1554fn operand_text(operand: &GuideOperand) -> String {
1555    match operand {
1556        GuideOperand::Literal(value) => value.to_string(),
1557        GuideOperand::Guide(name) => name.clone(),
1558    }
1559}
1560
1561#[derive(Clone, Debug)]
1562pub struct GuideEvaluator {
1563    values: BTreeMap<String, f64>,
1564}
1565
1566impl GuideEvaluator {
1567    pub fn new(width: f64, height: f64) -> Result<Self, GeometryError> {
1568        ensure_finite(width, "shape width")?;
1569        ensure_finite(height, "shape height")?;
1570
1571        let mut evaluator = Self {
1572            values: BTreeMap::new(),
1573        };
1574        evaluator.seed("w", width);
1575        evaluator.seed("h", height);
1576        evaluator.seed("l", 0.0);
1577        evaluator.seed("t", 0.0);
1578        evaluator.seed("r", width);
1579        evaluator.seed("b", height);
1580        evaluator.seed("hc", width / 2.0);
1581        evaluator.seed("vc", height / 2.0);
1582        evaluator.seed("ss", width.min(height));
1583        evaluator.seed("ls", width.max(height));
1584
1585        for divisor in [2_u32, 3, 4, 5, 6, 8, 10, 12, 32] {
1586            evaluator.seed(&format!("wd{divisor}"), width / f64::from(divisor));
1587        }
1588        for divisor in [2_u32, 3, 4, 5, 6, 8, 10] {
1589            evaluator.seed(&format!("hd{divisor}"), height / f64::from(divisor));
1590        }
1591        for divisor in [2_u32, 4, 6, 8, 16, 32] {
1592            evaluator.seed(
1593                &format!("ssd{divisor}"),
1594                width.min(height) / f64::from(divisor),
1595            );
1596        }
1597        evaluator.seed("cd2", 180.0 * ANGLE_UNITS_PER_DEGREE);
1598        evaluator.seed("cd4", 90.0 * ANGLE_UNITS_PER_DEGREE);
1599        evaluator.seed("cd8", 45.0 * ANGLE_UNITS_PER_DEGREE);
1600        evaluator.seed("3cd4", 270.0 * ANGLE_UNITS_PER_DEGREE);
1601        evaluator.seed("3cd8", 135.0 * ANGLE_UNITS_PER_DEGREE);
1602        evaluator.seed("5cd8", 225.0 * ANGLE_UNITS_PER_DEGREE);
1603        evaluator.seed("7cd8", 315.0 * ANGLE_UNITS_PER_DEGREE);
1604        Ok(evaluator)
1605    }
1606
1607    pub fn value(&self, name: &str) -> Result<f64, GeometryError> {
1608        self.values
1609            .get(name)
1610            .copied()
1611            .ok_or_else(|| GeometryError::UnknownGuide(name.to_owned()))
1612    }
1613
1614    pub fn apply_adjust_values(
1615        &mut self,
1616        adjustments: &[Guide],
1617        overrides: &BTreeMap<String, f64>,
1618    ) -> Result<(), GeometryError> {
1619        let declared = adjustments
1620            .iter()
1621            .map(|guide| guide.name.as_str())
1622            .collect::<BTreeSet<_>>();
1623        if let Some(name) = overrides
1624            .keys()
1625            .find(|name| !declared.contains(name.as_str()))
1626        {
1627            return Err(GeometryError::UnknownAdjustOverride(name.clone()));
1628        }
1629
1630        for adjustment in adjustments {
1631            let value = match overrides.get(&adjustment.name) {
1632                Some(value) => {
1633                    ensure_finite(*value, &adjustment.name)?;
1634                    *value
1635                }
1636                None => self.evaluate_operation(adjustment.op, &adjustment.args)?,
1637            };
1638            self.insert_named(&adjustment.name, value)?;
1639        }
1640        Ok(())
1641    }
1642
1643    pub fn evaluate_guides(&mut self, guides: &[Guide]) -> Result<(), GeometryError> {
1644        let mut evaluated = BTreeSet::new();
1645        for guide in guides {
1646            let value = self.evaluate_operation(guide.op, &guide.args)?;
1647            if evaluated.insert(guide.name.clone()) {
1648                self.insert_named(&guide.name, value)?;
1649            } else {
1650                ensure_finite(value, &guide.name)?;
1651                self.values.insert(guide.name.clone(), value);
1652            }
1653        }
1654        Ok(())
1655    }
1656
1657    pub fn evaluate_path(
1658        &self,
1659        commands: &[PathCommand],
1660    ) -> Result<Vec<EvaluatedPathCommand>, GeometryError> {
1661        let mut output = Vec::with_capacity(commands.len());
1662        let mut current = None;
1663        let mut subpath_start = None;
1664
1665        for command in commands {
1666            match command {
1667                PathCommand::MoveTo { x, y } => {
1668                    let point = (self.resolve(x)?, self.resolve(y)?);
1669                    output.push(EvaluatedPathCommand::MoveTo {
1670                        x: point.0,
1671                        y: point.1,
1672                    });
1673                    current = Some(point);
1674                    subpath_start = Some(point);
1675                }
1676                PathCommand::LineTo { x, y } => {
1677                    current.ok_or(GeometryError::PathHasNoCurrentPoint)?;
1678                    let point = (self.resolve(x)?, self.resolve(y)?);
1679                    output.push(EvaluatedPathCommand::LineTo {
1680                        x: point.0,
1681                        y: point.1,
1682                    });
1683                    current = Some(point);
1684                }
1685                PathCommand::CubicTo {
1686                    x1,
1687                    y1,
1688                    x2,
1689                    y2,
1690                    x,
1691                    y,
1692                } => {
1693                    current.ok_or(GeometryError::PathHasNoCurrentPoint)?;
1694                    let command = EvaluatedPathCommand::CubicTo {
1695                        x1: self.resolve(x1)?,
1696                        y1: self.resolve(y1)?,
1697                        x2: self.resolve(x2)?,
1698                        y2: self.resolve(y2)?,
1699                        x: self.resolve(x)?,
1700                        y: self.resolve(y)?,
1701                    };
1702                    if let EvaluatedPathCommand::CubicTo { x, y, .. } = command {
1703                        current = Some((x, y));
1704                    }
1705                    output.push(command);
1706                }
1707                PathCommand::ArcTo {
1708                    width_radius,
1709                    height_radius,
1710                    start_angle,
1711                    sweep_angle,
1712                } => {
1713                    let start = current.ok_or(GeometryError::PathHasNoCurrentPoint)?;
1714                    let cubics = flatten_arc(
1715                        start,
1716                        self.resolve(width_radius)?,
1717                        self.resolve(height_radius)?,
1718                        self.resolve(start_angle)?,
1719                        self.resolve(sweep_angle)?,
1720                    )?;
1721                    if let Some(EvaluatedPathCommand::CubicTo { x, y, .. }) = cubics.last() {
1722                        current = Some((*x, *y));
1723                    }
1724                    output.extend(cubics);
1725                }
1726                PathCommand::Close => {
1727                    current.ok_or(GeometryError::PathHasNoCurrentPoint)?;
1728                    output.push(EvaluatedPathCommand::Close);
1729                    current = subpath_start;
1730                }
1731            }
1732        }
1733        Ok(output)
1734    }
1735
1736    fn seed(&mut self, name: &str, value: f64) {
1737        self.values.insert(name.to_owned(), value);
1738    }
1739
1740    fn insert_named(&mut self, name: &str, value: f64) -> Result<(), GeometryError> {
1741        ensure_finite(value, name)?;
1742        if self.values.contains_key(name) {
1743            return Err(GeometryError::DuplicateGuide(name.to_owned()));
1744        }
1745        self.values.insert(name.to_owned(), value);
1746        Ok(())
1747    }
1748
1749    fn resolve(&self, operand: &GuideOperand) -> Result<f64, GeometryError> {
1750        match operand {
1751            GuideOperand::Literal(value) => {
1752                ensure_finite(*value, "literal")?;
1753                Ok(*value)
1754            }
1755            GuideOperand::Guide(name) => self.value(name),
1756        }
1757    }
1758
1759    fn evaluate_operation(
1760        &self,
1761        op: GuideOp,
1762        operands: &[GuideOperand],
1763    ) -> Result<f64, GeometryError> {
1764        let expected = op.argument_count();
1765        if operands.len() != expected {
1766            return Err(GeometryError::WrongArgumentCount {
1767                operation: format!("{op:?}"),
1768                expected,
1769                actual: operands.len(),
1770            });
1771        }
1772        let args = operands
1773            .iter()
1774            .map(|operand| self.resolve(operand))
1775            .collect::<Result<Vec<_>, _>>()?;
1776
1777        let value = match op {
1778            GuideOp::MulDiv => checked_div(args[0] * args[1], args[2])?,
1779            GuideOp::AddSub => args[0] + args[1] - args[2],
1780            GuideOp::AddDiv => checked_div(args[0] + args[1], args[2])?,
1781            GuideOp::IfElse => {
1782                if args[0] > 0.0 {
1783                    args[1]
1784                } else {
1785                    args[2]
1786                }
1787            }
1788            GuideOp::Abs => args[0].abs(),
1789            GuideOp::At2 => radians_to_angle(args[1].atan2(args[0])),
1790            GuideOp::Cat2 => args[0] * args[2].atan2(args[1]).cos(),
1791            GuideOp::Cos => args[0] * angle_to_radians(args[1]).cos(),
1792            GuideOp::Max => args[0].max(args[1]),
1793            GuideOp::Min => args[0].min(args[1]),
1794            GuideOp::Mod => args[0].hypot(args[1]).hypot(args[2]),
1795            GuideOp::Pin => {
1796                if args[1] < args[0] {
1797                    args[0]
1798                } else if args[1] > args[2] {
1799                    args[2]
1800                } else {
1801                    args[1]
1802                }
1803            }
1804            GuideOp::Sat2 => args[0] * args[2].atan2(args[1]).sin(),
1805            GuideOp::Sin => args[0] * angle_to_radians(args[1]).sin(),
1806            GuideOp::Sqrt => args[0].abs().sqrt(),
1807            GuideOp::Tan => args[0] * angle_to_radians(args[1]).tan(),
1808            GuideOp::Val => args[0],
1809        };
1810        ensure_finite(value, "guide result")?;
1811        Ok(value)
1812    }
1813}
1814
1815fn checked_div(numerator: f64, denominator: f64) -> Result<f64, GeometryError> {
1816    if denominator == 0.0 {
1817        return Err(GeometryError::DivisionByZero);
1818    }
1819    Ok(numerator / denominator)
1820}
1821
1822fn angle_to_radians(angle: f64) -> f64 {
1823    angle / ANGLE_UNITS_PER_DEGREE * PI / 180.0
1824}
1825
1826fn radians_to_angle(radians: f64) -> f64 {
1827    radians * 180.0 / PI * ANGLE_UNITS_PER_DEGREE
1828}
1829
1830fn ensure_finite(value: f64, context: &str) -> Result<(), GeometryError> {
1831    if value.is_finite() {
1832        Ok(())
1833    } else {
1834        Err(GeometryError::NonFiniteValue(context.to_owned()))
1835    }
1836}
1837
1838fn flatten_arc(
1839    current: (f64, f64),
1840    width_radius: f64,
1841    height_radius: f64,
1842    start_angle: f64,
1843    sweep_angle: f64,
1844) -> Result<Vec<EvaluatedPathCommand>, GeometryError> {
1845    for (value, context) in [
1846        (current.0, "arc start x"),
1847        (current.1, "arc start y"),
1848        (width_radius, "arc width radius"),
1849        (height_radius, "arc height radius"),
1850        (start_angle, "arc start angle"),
1851        (sweep_angle, "arc sweep angle"),
1852    ] {
1853        ensure_finite(value, context)?;
1854    }
1855    if width_radius <= 0.0 || height_radius <= 0.0 {
1856        return Err(GeometryError::InvalidArcRadius);
1857    }
1858    if sweep_angle == 0.0 {
1859        return Ok(Vec::new());
1860    }
1861
1862    let segment_count = (sweep_angle.abs() / QUARTER_CIRCLE).ceil();
1863    if segment_count > MAX_ARC_SEGMENTS as f64 {
1864        return Err(GeometryError::ArcSweepTooLarge);
1865    }
1866    let segment_count = segment_count as usize;
1867    let segment_sweep = sweep_angle / segment_count as f64;
1868    let start_radians = angle_to_radians(start_angle);
1869    let center = (
1870        current.0 - width_radius * start_radians.cos(),
1871        current.1 - height_radius * start_radians.sin(),
1872    );
1873    let mut cubics = Vec::with_capacity(segment_count);
1874
1875    for index in 0..segment_count {
1876        let angle_1 = angle_to_radians(start_angle + segment_sweep * index as f64);
1877        let angle_2 = angle_to_radians(start_angle + segment_sweep * (index + 1) as f64);
1878        let alpha = 4.0 / 3.0 * ((angle_2 - angle_1) / 4.0).tan();
1879        let point_1 = (
1880            center.0 + width_radius * angle_1.cos(),
1881            center.1 + height_radius * angle_1.sin(),
1882        );
1883        let point_2 = (
1884            center.0 + width_radius * angle_2.cos(),
1885            center.1 + height_radius * angle_2.sin(),
1886        );
1887        let tangent_1 = (-width_radius * angle_1.sin(), height_radius * angle_1.cos());
1888        let tangent_2 = (-width_radius * angle_2.sin(), height_radius * angle_2.cos());
1889        let command = EvaluatedPathCommand::CubicTo {
1890            x1: point_1.0 + alpha * tangent_1.0,
1891            y1: point_1.1 + alpha * tangent_1.1,
1892            x2: point_2.0 - alpha * tangent_2.0,
1893            y2: point_2.1 - alpha * tangent_2.1,
1894            x: point_2.0,
1895            y: point_2.1,
1896        };
1897        if let EvaluatedPathCommand::CubicTo {
1898            x1,
1899            y1,
1900            x2,
1901            y2,
1902            x,
1903            y,
1904        } = command
1905        {
1906            for value in [x1, y1, x2, y2, x, y] {
1907                ensure_finite(value, "arc cubic coordinate")?;
1908            }
1909        }
1910        cubics.push(command);
1911    }
1912    Ok(cubics)
1913}
1914
1915#[cfg(test)]
1916mod tests {
1917    use super::*;
1918
1919    #[test]
1920    fn preset_constructor_accepts_generated_names_and_rejects_unknown_names() {
1921        assert_eq!(
1922            CT_PresetGeometry2D::new("triangle").unwrap().preset,
1923            "triangle"
1924        );
1925        assert_eq!(
1926            CT_PresetGeometry2D::new("not-a-preset").unwrap_err(),
1927            GeometryError::UnknownPreset("not-a-preset".to_owned())
1928        );
1929    }
1930
1931    #[test]
1932    fn preset_adjustment_setter_inserts_and_replaces_named_values() {
1933        let mut geometry = CT_PresetGeometry2D::from_xml(
1934            br#"<x:prstGeom xmlns:x="urn:a" prst="roundRect"><x:avLst><x:gd name="adj" fmla="val 12000"><ext:raw xmlns:ext="urn:ext"/></x:gd><ext:tail xmlns:ext="urn:ext"/></x:avLst><ext:after xmlns:ext="urn:ext"/></x:prstGeom>"#,
1935        )
1936        .unwrap();
1937
1938        geometry.set_adjust_value("adj", 25_000.0).unwrap();
1939        geometry.set_adjust_value("adj2", 7_500.5).unwrap();
1940        let xml = String::from_utf8(geometry.to_xml().unwrap()).unwrap();
1941
1942        assert_eq!(geometry.adjust_values().len(), 2);
1943        assert_eq!(geometry.adjust_values()[0].name, "adj");
1944        assert_eq!(geometry.adjust_values()[0].args, vec![literal(25_000.0)]);
1945        assert_eq!(geometry.adjust_values()[1].name, "adj2");
1946        assert_eq!(geometry.adjust_values()[1].args, vec![literal(7_500.5)]);
1947        assert_eq!(xml.matches("name=\"adj\"").count(), 1);
1948        assert_eq!(xml.matches("name=\"adj2\"").count(), 1);
1949        assert!(xml.contains("<ext:raw xmlns:ext=\"urn:ext\"/>"));
1950        assert!(xml.contains("<a:gd name=\"adj2\" fmla=\"val 7500.5\"/><ext:tail"));
1951        assert!(geometry.set_adjust_value("bad", f64::INFINITY).is_err());
1952    }
1953
1954    fn literal(value: f64) -> GuideOperand {
1955        GuideOperand::Literal(value)
1956    }
1957
1958    fn guide(name: &str) -> GuideOperand {
1959        GuideOperand::Guide(name.to_owned())
1960    }
1961
1962    fn assert_close(actual: f64, expected: f64) {
1963        assert!((actual - expected).abs() < 1.0e-9, "{actual} != {expected}");
1964    }
1965
1966    #[test]
1967    fn hand_written_custom_geometry_guides_produce_expected_path_coordinates() {
1968        let adjustments = [Guide::parse("adj1", "val 25000").unwrap()];
1969        let guides = [
1970            Guide::parse("x1", "*/ w adj1 100000").unwrap(),
1971            Guide::parse("y1", "+/ hd2 0 2").unwrap(),
1972            Guide::parse("x2", "+- r 0 x1").unwrap(),
1973            Guide::parse("y2", "?: adj1 75 25").unwrap(),
1974        ];
1975        let overrides = BTreeMap::from([("adj1".to_owned(), 20_000.0)]);
1976        let mut evaluator = GuideEvaluator::new(100.0, 100.0).unwrap();
1977        assert_eq!(evaluator.value("wd32").unwrap(), 3.125);
1978        assert_eq!(evaluator.value("wd12").unwrap(), 100.0 / 12.0);
1979        assert_eq!(evaluator.value("hd8").unwrap(), 12.5);
1980        assert_eq!(evaluator.value("hd10").unwrap(), 10.0);
1981        assert_eq!(evaluator.value("ssd32").unwrap(), 3.125);
1982        assert_eq!(evaluator.value("3cd8").unwrap(), 8_100_000.0);
1983        evaluator
1984            .apply_adjust_values(&adjustments, &overrides)
1985            .unwrap();
1986        evaluator.evaluate_guides(&guides).unwrap();
1987
1988        let path = evaluator
1989            .evaluate_path(&[
1990                PathCommand::MoveTo {
1991                    x: guide("x1"),
1992                    y: guide("y1"),
1993                },
1994                PathCommand::LineTo {
1995                    x: guide("x2"),
1996                    y: guide("y2"),
1997                },
1998            ])
1999            .unwrap();
2000        assert_eq!(
2001            path,
2002            [
2003                EvaluatedPathCommand::MoveTo { x: 20.0, y: 25.0 },
2004                EvaluatedPathCommand::LineTo { x: 80.0, y: 75.0 },
2005            ]
2006        );
2007    }
2008
2009    #[test]
2010    fn ordinary_guides_replace_in_order_without_relaxing_adjustment_validation() {
2011        let mut evaluator = GuideEvaluator::new(100.0, 100.0).unwrap();
2012        evaluator
2013            .evaluate_guides(&[
2014                Guide::parse("connsiteX0", "val 10").unwrap(),
2015                Guide::parse("connsiteX0", "val 20").unwrap(),
2016            ])
2017            .unwrap();
2018        assert_eq!(evaluator.value("connsiteX0").unwrap(), 20.0);
2019
2020        let duplicate_adjustments = [
2021            Guide::parse("adj", "val 10").unwrap(),
2022            Guide::parse("adj", "val 20").unwrap(),
2023        ];
2024        let error = GuideEvaluator::new(100.0, 100.0)
2025            .unwrap()
2026            .apply_adjust_values(&duplicate_adjustments, &BTreeMap::new())
2027            .unwrap_err();
2028        assert_eq!(error, GeometryError::DuplicateGuide("adj".to_owned()));
2029
2030        let error = GuideEvaluator::new(100.0, 100.0)
2031            .unwrap()
2032            .apply_adjust_values(
2033                &[Guide::parse("adj", "val 10").unwrap()],
2034                &BTreeMap::from([("unknown".to_owned(), 20.0)]),
2035            )
2036            .unwrap_err();
2037        assert_eq!(
2038            error,
2039            GeometryError::UnknownAdjustOverride("unknown".to_owned())
2040        );
2041    }
2042
2043    #[test]
2044    fn all_seventeen_formula_tokens_parse_and_evaluate_with_drawingml_argument_order() {
2045        let cases = [
2046            ("*/ 6 7 2", GuideOp::MulDiv, 21.0),
2047            ("+- 10 4 3", GuideOp::AddSub, 11.0),
2048            ("+/ 10 4 2", GuideOp::AddDiv, 7.0),
2049            ("?: 1 5 6", GuideOp::IfElse, 5.0),
2050            ("abs -7", GuideOp::Abs, 7.0),
2051            ("at2 1 1", GuideOp::At2, 2_700_000.0),
2052            ("cat2 10 3 4", GuideOp::Cat2, 6.0),
2053            ("cos 10 3600000", GuideOp::Cos, 5.0),
2054            ("max 3 7", GuideOp::Max, 7.0),
2055            ("min 3 7", GuideOp::Min, 3.0),
2056            ("mod 3 4 12", GuideOp::Mod, 13.0),
2057            ("pin 0 15 10", GuideOp::Pin, 10.0),
2058            ("sat2 10 3 4", GuideOp::Sat2, 8.0),
2059            ("sin 10 1800000", GuideOp::Sin, 5.0),
2060            ("sqrt -9", GuideOp::Sqrt, 3.0),
2061            ("tan 10 2700000", GuideOp::Tan, 10.0),
2062            ("val 42", GuideOp::Val, 42.0),
2063        ];
2064        let guides = cases
2065            .iter()
2066            .enumerate()
2067            .map(|(index, (formula, expected_op, _))| {
2068                let guide = Guide::parse(format!("g{index}"), formula).unwrap();
2069                assert_eq!(guide.op, *expected_op);
2070                guide
2071            })
2072            .collect::<Vec<_>>();
2073        let mut evaluator = GuideEvaluator::new(100.0, 80.0).unwrap();
2074        evaluator.evaluate_guides(&guides).unwrap();
2075
2076        for (index, (_, _, expected)) in cases.iter().enumerate() {
2077            assert_close(evaluator.value(&format!("g{index}")).unwrap(), *expected);
2078        }
2079    }
2080
2081    #[test]
2082    fn arc_to_is_flattened_to_finite_cubics_with_matching_endpoints() {
2083        let evaluator = GuideEvaluator::new(100.0, 100.0).unwrap();
2084        let path = evaluator
2085            .evaluate_path(&[
2086                PathCommand::MoveTo {
2087                    x: literal(10.0),
2088                    y: literal(0.0),
2089                },
2090                PathCommand::ArcTo {
2091                    width_radius: literal(10.0),
2092                    height_radius: literal(5.0),
2093                    start_angle: literal(0.0),
2094                    sweep_angle: literal(27_000_000.0),
2095                },
2096            ])
2097            .unwrap();
2098        assert_eq!(path.len(), 6);
2099        assert!(path.iter().skip(1).all(|command| matches!(
2100            command,
2101            EvaluatedPathCommand::CubicTo {
2102                x1,
2103                y1,
2104                x2,
2105                y2,
2106                x,
2107                y
2108            } if [x1, y1, x2, y2, x, y].iter().all(|value| value.is_finite())
2109        )));
2110        let EvaluatedPathCommand::CubicTo { x, y, .. } = path[5] else {
2111            panic!("arc did not end in a cubic command")
2112        };
2113        assert_close(x, 0.0);
2114        assert_close(y, 5.0);
2115        let EvaluatedPathCommand::CubicTo { x, y, .. } = path[1] else {
2116            panic!("arc did not start with a cubic command")
2117        };
2118        assert_close(x, 0.0);
2119        assert_close(y, 5.0);
2120    }
2121
2122    #[test]
2123    fn office_mod_and_negative_sqrt_semantics_produce_finite_values() {
2124        let guides = [
2125            Guide::parse("norm", "mod 3 4 12").unwrap(),
2126            Guide::parse("root", "sqrt -9").unwrap(),
2127        ];
2128        let mut evaluator = GuideEvaluator::new(10.0, 10.0).unwrap();
2129        evaluator.evaluate_guides(&guides).unwrap();
2130        assert_eq!(evaluator.value("norm").unwrap(), 13.0);
2131        assert_eq!(evaluator.value("root").unwrap(), 3.0);
2132    }
2133
2134    #[test]
2135    fn division_by_zero_returns_an_error_instead_of_non_finite_coordinates() {
2136        let mut evaluator = GuideEvaluator::new(10.0, 10.0).unwrap();
2137        let error = evaluator
2138            .evaluate_guides(&[Guide::parse("bad", "*/ 1 2 0").unwrap()])
2139            .unwrap_err();
2140        assert_eq!(error, GeometryError::DivisionByZero);
2141        assert_eq!(error.to_string(), "division by zero");
2142    }
2143
2144    #[test]
2145    fn corpus_custom_geometry_round_trips_and_evaluates_to_a_closed_path() {
2146        let xml = br#"<z:custGeom xmlns:z="http://schemas.openxmlformats.org/drawingml/2006/main"><z:avLst><z:gd name="adj" fmla="val 25000"/></z:avLst><z:gdLst><z:gd name="x1" fmla="*/ w adj 100000"/><z:gd name="x2" fmla="+- r 0 x1"/></z:gdLst><z:rect l="x1" t="t" r="x2" b="b"/><z:pathLst><z:path w="100" h="100"><z:moveTo><z:pt x="l" y="t"/></z:moveTo><z:lnTo><z:pt x="r" y="t"/></z:lnTo><z:cubicBezTo><z:pt x="r" y="t"/><z:pt x="r" y="b"/><z:pt x="x2" y="b"/></z:cubicBezTo><z:close/></z:path></z:pathLst></z:custGeom>"#;
2147        let geometry = CT_CustomGeometry2D::from_xml(xml).unwrap();
2148        let evaluated = geometry.evaluate(&BTreeMap::new()).unwrap();
2149
2150        assert_eq!(geometry.adjust_values().len(), 1);
2151        assert_eq!(geometry.guides().len(), 2);
2152        assert_eq!(geometry.paths().len(), 1);
2153        assert_eq!(evaluated.paths.len(), 1);
2154        assert_eq!(
2155            evaluated.paths[0].last(),
2156            Some(&EvaluatedPathCommand::Close)
2157        );
2158        assert_eq!(
2159            evaluated.text_rectangle,
2160            Some(EvaluatedTextRectangle {
2161                left: 25.0,
2162                top: 0.0,
2163                right: 75.0,
2164                bottom: 100.0,
2165            })
2166        );
2167
2168        let written = geometry.to_xml().unwrap();
2169        let reparsed = CT_CustomGeometry2D::from_xml(&written).unwrap();
2170        assert_eq!(reparsed, geometry);
2171    }
2172
2173    #[test]
2174    fn custom_geometry_reads_any_prefix_and_writes_fixed_a_prefix_in_schema_order() {
2175        let xml = br#"<q:custGeom><q:avLst/><q:gdLst/><q:ahLst/><q:cxnLst/><q:rect l="l" t="t" r="r" b="b"/><q:pathLst><q:path w="100" h="80" fill="none" stroke="false" extrusionOk="true"><q:moveTo><q:pt x="0" y="0"/></q:moveTo><q:arcTo wR="10" hR="5" stAng="0" swAng="5400000"/><q:close/></q:path></q:pathLst></q:custGeom>"#;
2176        let geometry = CT_CustomGeometry2D::from_xml(xml).unwrap();
2177        let written = String::from_utf8(geometry.to_xml().unwrap()).unwrap();
2178
2179        assert!(written.starts_with("<a:custGeom>"));
2180        assert!(written.contains("<a:avLst/>"));
2181        assert!(written.contains("<a:gdLst/>"));
2182        assert!(written.contains("<a:rect l=\"l\" t=\"t\" r=\"r\" b=\"b\"/>"));
2183        assert!(written.contains("<a:arcTo wR=\"10\" hR=\"5\" stAng=\"0\" swAng=\"5400000\"/>"));
2184        assert!(written.contains("<a:close/>"));
2185        let av = written.find("<a:avLst").unwrap();
2186        let gd = written.find("<a:gdLst").unwrap();
2187        let ah = written.find("<q:ahLst/>").unwrap();
2188        let cxn = written.find("<q:cxnLst/>").unwrap();
2189        let rect = written.find("<a:rect").unwrap();
2190        let paths = written.find("<a:pathLst").unwrap();
2191        assert!(av < gd && gd < ah && ah < cxn && cxn < rect && rect < paths);
2192    }
2193
2194    #[test]
2195    fn empty_custom_geometry_path_list_from_theme_defaults_round_trips() {
2196        let geometry = CT_CustomGeometry2D::from_xml(
2197            br#"<q:custGeom><q:avLst/><q:gdLst/><q:ahLst/><q:cxnLst/><q:rect l="0" t="0" r="0" b="0"/><q:pathLst/></q:custGeom>"#,
2198        )
2199        .unwrap();
2200        assert!(geometry.paths().is_empty());
2201        let written = geometry.to_xml().unwrap();
2202        assert_eq!(CT_CustomGeometry2D::from_xml(&written).unwrap(), geometry);
2203    }
2204
2205    #[test]
2206    fn unknown_custom_geometry_children_round_trip_byte_for_byte_in_place() {
2207        let xml = br#"<a:custGeom><u:before/><a:avLst><u:avBefore/><a:gd name="adj" fmla="val 25000"><u:insideGuide/></a:gd><u:avAfter/></a:avLst><u:middle u:id="7"><u:child/></u:middle><a:pathLst><u:pathBefore/><a:path w="100" h="100"><a:moveTo><a:pt x="0" y="0"><u:insidePoint/></a:pt><u:insideMove/></a:moveTo><u:between/><a:lnTo><a:pt x="100" y="100"/></a:lnTo><a:close/></a:path><u:pathAfter/></a:pathLst><u:after/></a:custGeom>"#;
2208        let written = String::from_utf8(
2209            CT_CustomGeometry2D::from_xml(xml)
2210                .unwrap()
2211                .to_xml()
2212                .unwrap(),
2213        )
2214        .unwrap();
2215
2216        for raw in [
2217            "<u:before/>",
2218            "<u:avBefore/>",
2219            "<u:insideGuide/>",
2220            "<u:avAfter/>",
2221            "<u:middle u:id=\"7\"><u:child/></u:middle>",
2222            "<u:pathBefore/>",
2223            "<u:insidePoint/>",
2224            "<u:insideMove/>",
2225            "<u:between/>",
2226            "<u:pathAfter/>",
2227            "<u:after/>",
2228        ] {
2229            assert!(written.contains(raw), "missing raw subtree {raw}");
2230        }
2231        assert!(written.find("<u:before/>").unwrap() < written.find("<a:avLst").unwrap());
2232        assert!(written.find("<u:avBefore/>").unwrap() < written.find("<a:gd ").unwrap());
2233        assert!(written.find("<a:gd ").unwrap() < written.find("<u:avAfter/>").unwrap());
2234        assert!(written.find("<u:insideGuide/>").unwrap() < written.find("</a:gd>").unwrap());
2235        assert!(written.find("<u:insidePoint/>").unwrap() < written.find("</a:pt>").unwrap());
2236        assert!(written.find("<u:insideMove/>").unwrap() < written.find("</a:moveTo>").unwrap());
2237        assert!(written.find("</a:moveTo>").unwrap() < written.find("<u:between/>").unwrap());
2238    }
2239
2240    #[test]
2241    fn malformed_custom_geometry_returns_an_error_without_panicking() {
2242        let malformed: [&[u8]; 3] = [
2243            br#"<a:custGeom><a:avLst><a:gd fmla="val 1"/></a:avLst><a:pathLst><a:path w="1" h="1"/></a:pathLst></a:custGeom>"#,
2244            br#"<a:custGeom><a:gdLst><a:gd name="bad" fmla="nope 1"/></a:gdLst><a:pathLst><a:path w="1" h="1"/></a:pathLst></a:custGeom>"#,
2245            br#"<a:custGeom><a:pathLst><a:path w="1" h="1">"#,
2246        ];
2247
2248        for xml in malformed {
2249            let result = std::panic::catch_unwind(|| CT_CustomGeometry2D::from_xml(xml));
2250            assert!(result.is_ok(), "malformed XML panicked");
2251            assert!(result.unwrap().is_err(), "malformed XML was accepted");
2252        }
2253    }
2254
2255    #[test]
2256    fn rectangle_preset_evaluates_to_expected_bounds_and_text_rect() {
2257        let preset = CT_PresetGeometry2D::from_xml(br#"<q:prstGeom prst="rect"/>"#).unwrap();
2258        let evaluated = preset.evaluate((120.0, 80.0)).unwrap().unwrap();
2259
2260        assert_eq!(
2261            evaluated.paths[0],
2262            [
2263                EvaluatedPathCommand::MoveTo { x: 0.0, y: 0.0 },
2264                EvaluatedPathCommand::LineTo { x: 120.0, y: 0.0 },
2265                EvaluatedPathCommand::LineTo { x: 120.0, y: 80.0 },
2266                EvaluatedPathCommand::LineTo { x: 0.0, y: 80.0 },
2267                EvaluatedPathCommand::Close,
2268            ]
2269        );
2270        assert_eq!(
2271            evaluated.text_rectangle,
2272            Some(EvaluatedTextRectangle {
2273                left: 0.0,
2274                top: 0.0,
2275                right: 120.0,
2276                bottom: 80.0,
2277            })
2278        );
2279    }
2280
2281    #[test]
2282    fn preset_adjustments_override_generated_defaults() {
2283        let default = CT_PresetGeometry2D::from_xml(
2284            br#"<a:prstGeom prst="trapezoid"><a:avLst/></a:prstGeom>"#,
2285        )
2286        .unwrap()
2287        .evaluate((200.0, 100.0))
2288        .unwrap()
2289        .unwrap();
2290        let adjusted = CT_PresetGeometry2D::from_xml(
2291            br#"<a:prstGeom prst="trapezoid"><a:avLst><a:gd name="adj" fmla="val 50000"/></a:avLst></a:prstGeom>"#,
2292        )
2293        .unwrap()
2294        .evaluate((200.0, 100.0))
2295        .unwrap()
2296        .unwrap();
2297
2298        assert_eq!(
2299            default.paths[0][1],
2300            EvaluatedPathCommand::LineTo { x: 25.0, y: 0.0 }
2301        );
2302        assert_eq!(
2303            adjusted.paths[0][1],
2304            EvaluatedPathCommand::LineTo { x: 50.0, y: 0.0 }
2305        );
2306    }
2307}