Skip to main content

odox_core/
draw.rs

1//! `draw:enhanced-geometry`: the outline of a custom shape, worked out.
2//!
3//! A custom shape does not state its outline as points. It states a *path* in a
4//! compact command language, in a coordinate space of its own, whose numbers may
5//! be references to named formulas that are themselves arithmetic over the
6//! space's edges and over adjustment values a person dragged. A rounded
7//! rectangle is `M ?f7 0 X 0 ?f8 L 0 ?f9 Y ?f7 21600 …`, and none of that means
8//! anything until the formulas are evaluated.
9//!
10//! What comes out of here is [`Geometry`]: the same outline as flat polylines in
11//! the shape's own space, ready for a renderer to map onto a rectangle. Curves
12//! and arcs are flattened here rather than passed on, because the number of
13//! segments a curve needs depends on the size of the coordinate space and not on
14//! the size of the window, and this is where the space is known.
15//!
16//! # What is implemented
17//!
18//! The whole command language except `Q`'s smooth variants, and the formula
19//! grammar in full. What is *measured* is narrower: across the twenty-three
20//! presentation templates `LibreOffice` ships, the commands that occur are `M`,
21//! `L`, `C`, `Z`, `N`, `U`, `X`, `Y` and `V`, and the formulas use the four edge
22//! constants, `pi`, and `if`, `sin`, `cos` and `abs`. The arc commands `A`, `B`
23//! and `W` occur nowhere in that set and their sweep direction is taken from the
24//! specification rather than from a document.
25//
26// Author: David M. Anderson
27// Built with AI assistance (Claude, Anthropic)
28
29use std::cell::{Cell, RefCell};
30use std::collections::HashMap;
31
32use crate::xml::{Element, Ns};
33
34/// The coordinate space a shape states its outline in.
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct ViewBox {
37    /// The left edge.
38    pub x: f32,
39    /// The top edge.
40    pub y: f32,
41    /// How wide.
42    pub width: f32,
43    /// How tall.
44    pub height: f32,
45}
46
47impl ViewBox {
48    fn parse(text: &str) -> Option<Self> {
49        let numbers: Vec<f32> = text
50            .split_whitespace()
51            .filter_map(|n| n.parse().ok())
52            .collect();
53        let [x, y, width, height] = numbers[..] else {
54            return None;
55        };
56        (width > 0.0 && height > 0.0).then_some(Self {
57            x,
58            y,
59            width,
60            height,
61        })
62    }
63}
64
65/// One stroke of the pen: a run of points, and what is done with them.
66#[derive(Debug, Clone, PartialEq)]
67pub struct SubPath {
68    /// The points, in the shape's own coordinate space.
69    pub points: Vec<(f32, f32)>,
70    /// Whether the last point joins the first.
71    pub closed: bool,
72    /// Whether the inside is filled. `F` in the path turns it off for the rest
73    /// of the path, which is how a shape draws a detail over its own body.
74    pub fill: bool,
75    /// Whether the outline is drawn. `S` turns it off.
76    pub stroke: bool,
77}
78
79/// A custom shape's outline.
80#[derive(Debug, Clone, PartialEq)]
81pub struct Geometry {
82    /// The space the points are in.
83    pub view: ViewBox,
84    /// The strokes, in the order they are drawn.
85    pub paths: Vec<SubPath>,
86}
87
88/// How finely a curve is broken into straight lines.
89///
90/// Sixteen segments for a whole cubic and one for every six degrees of an arc,
91/// which at the sizes ODF's coordinate spaces use — twenty-one thousand six
92/// hundred units across, drawn into a few hundred pixels — is below what a
93/// screen can show.
94const CURVE_SEGMENTS: usize = 16;
95const DEGREES_PER_SEGMENT: f32 = 6.0;
96
97impl Geometry {
98    /// Read a `draw:enhanced-geometry` element.
99    ///
100    /// `None` where it states no path or no coordinate space, which is a shape
101    /// nothing can draw.
102    pub fn read(geometry: &Element) -> Option<Self> {
103        let view = ViewBox::parse(geometry.attr(&Ns::Svg, "viewBox")?)?;
104        let path = geometry.attr(&Ns::Draw, "enhanced-path")?;
105
106        let formulas = Formulas::new(geometry, view);
107        let mut pen = Pen::new();
108        pen.run(path, &formulas);
109        let mut paths = pen.finish();
110
111        // A mirrored shape states its outline once and is drawn flipped.
112        let flip_x = geometry.attr(&Ns::Draw, "mirror-horizontal") == Some("true");
113        let flip_y = geometry.attr(&Ns::Draw, "mirror-vertical") == Some("true");
114        if flip_x || flip_y {
115            for path in &mut paths {
116                for (x, y) in &mut path.points {
117                    if flip_x {
118                        *x = view.x + view.width - (*x - view.x);
119                    }
120                    if flip_y {
121                        *y = view.y + view.height - (*y - view.y);
122                    }
123                }
124            }
125        }
126
127        Some(Self { view, paths })
128    }
129}
130
131/// The named formulas of one shape, and the values they are over.
132struct Formulas<'a> {
133    by_name: HashMap<&'a str, &'a str>,
134    modifiers: Vec<f32>,
135    view: ViewBox,
136    /// Evaluated formulas, kept because a chain of thirty each referring to the
137    /// one before is ordinary and re-evaluating it per reference is not.
138    known: RefCell<HashMap<String, f32>>,
139    depth: Cell<u32>,
140}
141
142/// How deep a formula may refer before it is called a cycle.
143///
144/// No writer produces one; a hand-edited file can, and the alternative to a
145/// limit is a window that stops responding.
146const MAX_DEPTH: u32 = 64;
147
148impl<'a> Formulas<'a> {
149    fn new(geometry: &'a Element, view: ViewBox) -> Self {
150        let by_name = geometry
151            .elements()
152            .filter(|e| e.is(&Ns::Draw, "equation"))
153            .filter_map(|e| Some((e.attr(&Ns::Draw, "name")?, e.attr(&Ns::Draw, "formula")?)))
154            .collect();
155        let modifiers = geometry
156            .attr(&Ns::Draw, "modifiers")
157            .unwrap_or_default()
158            .split_whitespace()
159            .filter_map(|n| n.parse().ok())
160            .collect();
161        Self {
162            by_name,
163            modifiers,
164            view,
165            known: RefCell::new(HashMap::new()),
166            depth: Cell::new(0),
167        }
168    }
169
170    /// The value of a named formula.
171    fn named(&self, name: &str) -> f32 {
172        if let Some(value) = self.known.borrow().get(name) {
173            return *value;
174        }
175        if self.depth.get() >= MAX_DEPTH {
176            return 0.0;
177        }
178        let Some(text) = self.by_name.get(name) else {
179            return 0.0;
180        };
181        self.depth.set(self.depth.get() + 1);
182        let value = self.eval(text);
183        self.depth.set(self.depth.get() - 1);
184        self.known.borrow_mut().insert(name.to_owned(), value);
185        value
186    }
187
188    /// A modifier, which is an adjustment a person dragged and the document
189    /// stored.
190    fn modifier(&self, index: usize) -> f32 {
191        self.modifiers.get(index).copied().unwrap_or(0.0)
192    }
193
194    /// One of the constants a formula may name.
195    fn constant(&self, name: &str) -> Option<f32> {
196        Some(match name {
197            "left" => self.view.x,
198            "top" => self.view.y,
199            "right" => self.view.x + self.view.width,
200            "bottom" => self.view.y + self.view.height,
201            "width" | "logwidth" => self.view.width,
202            "height" | "logheight" => self.view.height,
203            "pi" => std::f32::consts::PI,
204            // A shape may ask whether it is being stroked or filled and draw
205            // differently. Nothing here answers no.
206            "hasstroke" | "hasfill" => 1.0,
207            "xstretch" | "ystretch" => 0.0,
208            _ => return None,
209        })
210    }
211
212    fn eval(&self, text: &str) -> f32 {
213        Expression {
214            text: text.as_bytes(),
215            at: 0,
216            formulas: self,
217        }
218        .expression()
219    }
220}
221
222/// A formula, read left to right.
223struct Expression<'a, 'f> {
224    text: &'a [u8],
225    at: usize,
226    formulas: &'a Formulas<'f>,
227}
228
229impl Expression<'_, '_> {
230    fn skip(&mut self) {
231        while self.at < self.text.len() && self.text[self.at].is_ascii_whitespace() {
232            self.at += 1;
233        }
234    }
235
236    fn peek(&mut self) -> Option<u8> {
237        self.skip();
238        self.text.get(self.at).copied()
239    }
240
241    fn take(&mut self, byte: u8) -> bool {
242        if self.peek() == Some(byte) {
243            self.at += 1;
244            return true;
245        }
246        false
247    }
248
249    fn expression(&mut self) -> f32 {
250        let mut value = self.term();
251        loop {
252            if self.take(b'+') {
253                value += self.term();
254            } else if self.take(b'-') {
255                value -= self.term();
256            } else {
257                return value;
258            }
259        }
260    }
261
262    fn term(&mut self) -> f32 {
263        let mut value = self.factor();
264        loop {
265            if self.take(b'*') {
266                value *= self.factor();
267            } else if self.take(b'/') {
268                let divisor = self.factor();
269                // A formula dividing by zero is a shape somebody edited by hand.
270                // Nought is a point that can be drawn; infinity is not.
271                value = if divisor == 0.0 { 0.0 } else { value / divisor };
272            } else {
273                return value;
274            }
275        }
276    }
277
278    fn factor(&mut self) -> f32 {
279        if self.take(b'-') {
280            return -self.factor();
281        }
282        if self.take(b'+') {
283            return self.factor();
284        }
285        if self.take(b'(') {
286            let value = self.expression();
287            self.take(b')');
288            return value;
289        }
290        if self.take(b'?') {
291            let name = self.word();
292            return self.formulas.named(&name);
293        }
294        if self.take(b'$') {
295            let index = self.word().parse().unwrap_or(0);
296            return self.formulas.modifier(index);
297        }
298        match self.peek() {
299            Some(byte) if byte.is_ascii_alphabetic() => {
300                let name = self.word();
301                if self.take(b'(') {
302                    let arguments = self.arguments();
303                    return call(&name, &arguments);
304                }
305                self.formulas.constant(&name).unwrap_or(0.0)
306            }
307            _ => self.number(),
308        }
309    }
310
311    fn arguments(&mut self) -> Vec<f32> {
312        let mut arguments = Vec::new();
313        if self.take(b')') {
314            return arguments;
315        }
316        loop {
317            arguments.push(self.expression());
318            if !self.take(b',') {
319                self.take(b')');
320                return arguments;
321            }
322        }
323    }
324
325    /// A name or a run of digits, whichever is under the cursor.
326    fn word(&mut self) -> String {
327        self.skip();
328        let start = self.at;
329        while self
330            .text
331            .get(self.at)
332            .is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_')
333        {
334            self.at += 1;
335        }
336        String::from_utf8_lossy(&self.text[start..self.at]).into_owned()
337    }
338
339    fn number(&mut self) -> f32 {
340        self.skip();
341        let start = self.at;
342        while self
343            .text
344            .get(self.at)
345            .is_some_and(|b| b.is_ascii_digit() || *b == b'.')
346        {
347            self.at += 1;
348        }
349        if start == self.at {
350            // Nothing readable here: step over it so that a malformed formula
351            // ends rather than spins.
352            self.at += 1;
353            return 0.0;
354        }
355        String::from_utf8_lossy(&self.text[start..self.at])
356            .parse()
357            .unwrap_or(0.0)
358    }
359}
360
361fn call(name: &str, arguments: &[f32]) -> f32 {
362    let argument = |n: usize| arguments.get(n).copied().unwrap_or(0.0);
363    match name {
364        "abs" => argument(0).abs(),
365        "sqrt" => argument(0).max(0.0).sqrt(),
366        // Radians. A formula that means degrees writes the conversion itself,
367        // as `sin($0 * (pi/180))`, which is how every one in the corpus does it.
368        "sin" => argument(0).sin(),
369        "cos" => argument(0).cos(),
370        "tan" => argument(0).tan(),
371        "atan" => argument(0).atan(),
372        "atan2" => argument(0).atan2(argument(1)),
373        "min" => argument(0).min(argument(1)),
374        "max" => argument(0).max(argument(1)),
375        // Greater than nought is true, which is the specification's rule and not
376        // the usual one.
377        "if" => {
378            if argument(0) > 0.0 {
379                argument(1)
380            } else {
381                argument(2)
382            }
383        }
384        _ => 0.0,
385    }
386}
387
388/// The state of drawing one path: where the pen is and what it has drawn.
389struct Pen {
390    done: Vec<SubPath>,
391    points: Vec<(f32, f32)>,
392    closed: bool,
393    fill: bool,
394    stroke: bool,
395}
396
397impl Pen {
398    fn new() -> Self {
399        Self {
400            done: Vec::new(),
401            points: Vec::new(),
402            closed: false,
403            fill: true,
404            stroke: true,
405        }
406    }
407
408    fn at(&self) -> (f32, f32) {
409        self.points.last().copied().unwrap_or((0.0, 0.0))
410    }
411
412    /// Finish the run of points in hand and begin another.
413    fn brk(&mut self) {
414        if self.points.len() >= 2 {
415            self.done.push(SubPath {
416                points: std::mem::take(&mut self.points),
417                closed: self.closed,
418                fill: self.fill,
419                stroke: self.stroke,
420            });
421        } else {
422            self.points.clear();
423        }
424        self.closed = false;
425    }
426
427    fn finish(mut self) -> Vec<SubPath> {
428        self.brk();
429        self.done
430    }
431
432    /// Walk the path, command by command.
433    fn run(&mut self, path: &str, formulas: &Formulas<'_>) {
434        let mut tokens = Tokens {
435            text: path.as_bytes(),
436            at: 0,
437            formulas,
438            pushed: None,
439        };
440        let mut command = None;
441        loop {
442            match tokens.next() {
443                Some(Token::Command(letter)) => {
444                    command = Some(letter);
445                    self.command(letter, &mut tokens);
446                }
447                // A command's arguments may repeat: `L x y x y x y` is three
448                // lines, and the letter is written once.
449                Some(Token::Number(first)) => match command {
450                    Some(letter) => {
451                        tokens.pushed = Some(first);
452                        self.command(letter, &mut tokens);
453                    }
454                    None => return,
455                },
456                None => return,
457            }
458        }
459    }
460
461    fn command(&mut self, letter: char, tokens: &mut Tokens<'_, '_>) {
462        match letter {
463            'M' => {
464                let point = tokens.point();
465                self.brk();
466                self.points.push(point);
467            }
468            'L' => {
469                let point = tokens.point();
470                self.points.push(point);
471            }
472            'C' => {
473                let (a, b, end) = (tokens.point(), tokens.point(), tokens.point());
474                self.cubic(a, b, end);
475            }
476            'Q' => {
477                let (control, end) = (tokens.point(), tokens.point());
478                // A quadratic is a cubic whose two controls sit two thirds of
479                // the way from each end towards the single one.
480                let from = self.at();
481                let third = |a: f32, b: f32| a + 2.0 / 3.0 * (b - a);
482                self.cubic(
483                    (third(from.0, control.0), third(from.1, control.1)),
484                    (third(end.0, control.0), third(end.1, control.1)),
485                    end,
486                );
487            }
488            'Z' => {
489                self.closed = true;
490                self.brk();
491            }
492            'N' => self.brk(),
493            'F' => self.fill = false,
494            'S' => self.stroke = false,
495            'T' | 'U' => {
496                let (centre, radii) = (tokens.point(), tokens.point());
497                let (from, to) = (tokens.number(), tokens.number());
498                if letter == 'U' {
499                    self.brk();
500                }
501                self.arc(centre, radii, from, to);
502            }
503            'X' | 'Y' => {
504                let to = tokens.point();
505                self.quadrant(to, letter == 'X');
506            }
507            'A' | 'B' | 'W' | 'V' => {
508                let (corner, opposite) = (tokens.point(), tokens.point());
509                let (from, to) = (tokens.point(), tokens.point());
510                if letter == 'B' || letter == 'V' {
511                    self.brk();
512                }
513                self.box_arc(corner, opposite, from, to, letter == 'W' || letter == 'V');
514            }
515            _ => {}
516        }
517    }
518
519    fn cubic(&mut self, a: (f32, f32), b: (f32, f32), end: (f32, f32)) {
520        let from = self.at();
521        for step in 1..=CURVE_SEGMENTS {
522            #[allow(clippy::cast_precision_loss)]
523            let t = step as f32 / CURVE_SEGMENTS as f32;
524            let u = 1.0 - t;
525            let blend = |p0: f32, p1: f32, p2: f32, p3: f32| {
526                u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3
527            };
528            self.points.push((
529                blend(from.0, a.0, b.0, end.0),
530                blend(from.1, a.1, b.1, end.1),
531            ));
532        }
533    }
534
535    /// A run of points along an ellipse, from one angle to another in degrees.
536    fn arc(&mut self, centre: (f32, f32), radii: (f32, f32), from: f32, to: f32) {
537        // A sweep that would be nothing or negative is the long way round, which
538        // is what `0 360` means and what every full ellipse in the corpus says.
539        let mut sweep = to - from;
540        if sweep <= 0.0 {
541            sweep += 360.0;
542        }
543        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
544        let steps = ((sweep / DEGREES_PER_SEGMENT).ceil() as usize).max(2);
545        for step in 0..=steps {
546            #[allow(clippy::cast_precision_loss)]
547            let angle = (from + sweep * step as f32 / steps as f32).to_radians();
548            self.points.push((
549                centre.0 + radii.0 * angle.cos(),
550                centre.1 + radii.1 * angle.sin(),
551            ));
552        }
553    }
554
555    /// A quarter of an ellipse from where the pen is to a point.
556    ///
557    /// `X` leaves horizontally and arrives vertically, `Y` the other way round.
558    /// Between them they are how every rounded corner in ODF is written.
559    fn quadrant(&mut self, to: (f32, f32), x_first: bool) {
560        let from = self.at();
561        let centre = if x_first {
562            (to.0, from.1)
563        } else {
564            (from.0, to.1)
565        };
566        let radii = ((to.0 - from.0).abs(), (to.1 - from.1).abs());
567        if radii.0 == 0.0 || radii.1 == 0.0 {
568            self.points.push(to);
569            return;
570        }
571        let angle_of = |p: (f32, f32)| (p.1 - centre.1).atan2(p.0 - centre.0).to_degrees();
572        let (start, end) = (angle_of(from), angle_of(to));
573        // The quarter that joins the two, taken the short way.
574        let mut sweep = end - start;
575        while sweep > 180.0 {
576            sweep -= 360.0;
577        }
578        while sweep < -180.0 {
579            sweep += 360.0;
580        }
581        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
582        let steps = ((sweep.abs() / DEGREES_PER_SEGMENT).ceil() as usize).max(2);
583        for step in 1..=steps {
584            #[allow(clippy::cast_precision_loss)]
585            let angle = (start + sweep * step as f32 / steps as f32).to_radians();
586            self.points.push((
587                centre.0 + radii.0 * angle.cos(),
588                centre.1 + radii.1 * angle.sin(),
589            ));
590        }
591    }
592
593    /// An arc of the ellipse that fills a box, between two points on it.
594    ///
595    /// **The sweep direction here is the specification's and not a measurement.**
596    /// `V` occurs four times in the twenty-three templates surveyed and `A`, `B`
597    /// and `W` occur in none of them, so nothing in the corpus tells these apart.
598    fn box_arc(
599        &mut self,
600        corner: (f32, f32),
601        opposite: (f32, f32),
602        from: (f32, f32),
603        to: (f32, f32),
604        clockwise: bool,
605    ) {
606        let centre = (
607            f32::midpoint(corner.0, opposite.0),
608            f32::midpoint(corner.1, opposite.1),
609        );
610        let radii = (
611            (opposite.0 - corner.0).abs() / 2.0,
612            (opposite.1 - corner.1).abs() / 2.0,
613        );
614        if radii.0 == 0.0 || radii.1 == 0.0 {
615            self.points.push(to);
616            return;
617        }
618        let angle_of = |p: (f32, f32)| {
619            ((p.1 - centre.1) / radii.1)
620                .atan2((p.0 - centre.0) / radii.0)
621                .to_degrees()
622        };
623        let (start, end) = (angle_of(from), angle_of(to));
624        let sweep = if clockwise { start - end } else { end - start };
625        let sweep = if sweep <= 0.0 { sweep + 360.0 } else { sweep };
626        let (a, b) = if clockwise {
627            (start, start - sweep)
628        } else {
629            (start, start + sweep)
630        };
631        self.arc_between(centre, radii, a, b);
632    }
633
634    fn arc_between(&mut self, centre: (f32, f32), radii: (f32, f32), from: f32, to: f32) {
635        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
636        let steps = (((to - from).abs() / DEGREES_PER_SEGMENT).ceil() as usize).max(2);
637        for step in 0..=steps {
638            #[allow(clippy::cast_precision_loss)]
639            let angle = (from + (to - from) * step as f32 / steps as f32).to_radians();
640            self.points.push((
641                centre.0 + radii.0 * angle.cos(),
642                centre.1 + radii.1 * angle.sin(),
643            ));
644        }
645    }
646}
647
648enum Token {
649    Command(char),
650    Number(f32),
651}
652
653/// The path, read one token at a time, with every reference already resolved.
654struct Tokens<'a, 'f> {
655    text: &'a [u8],
656    at: usize,
657    formulas: &'a Formulas<'f>,
658    // Set when a repeated argument group put a number back.
659    pushed: Option<f32>,
660}
661
662impl Tokens<'_, '_> {
663    fn next(&mut self) -> Option<Token> {
664        if let Some(number) = self.pushed.take() {
665            return Some(Token::Number(number));
666        }
667        while self
668            .text
669            .get(self.at)
670            .is_some_and(|b| b.is_ascii_whitespace() || *b == b',')
671        {
672            self.at += 1;
673        }
674        let byte = *self.text.get(self.at)?;
675        if byte.is_ascii_alphabetic() {
676            self.at += 1;
677            return Some(Token::Command(char::from(byte)));
678        }
679        Some(Token::Number(self.value()))
680    }
681
682    /// One number, which may be written as a reference to a formula or to a
683    /// modifier rather than as digits.
684    fn value(&mut self) -> f32 {
685        let mut expression = Expression {
686            text: self.text,
687            at: self.at,
688            formulas: self.formulas,
689        };
690        // A path's numbers are single values and never arithmetic, so a factor
691        // is the whole of what may appear — and `?f7` and `$0` are factors.
692        let value = expression.factor();
693        self.at = expression.at;
694        value
695    }
696
697    fn number(&mut self) -> f32 {
698        match self.next() {
699            Some(Token::Number(number)) => number,
700            // A command short of its arguments: nought keeps the pen somewhere
701            // rather than ending the shape.
702            _ => 0.0,
703        }
704    }
705
706    fn point(&mut self) -> (f32, f32) {
707        (self.number(), self.number())
708    }
709}