Skip to main content

odox_core/
style.rs

1//! Styles: the property sets a document names, the chain each one inherits
2//! through, and the resolution of a name into the properties a renderer wants.
3//!
4//! ODF keeps formatting in two places that work identically. A *named* style in
5//! `styles.xml` is the one a person chose and can see in an application's style
6//! list; an *automatic* style in `content.xml` is the one an application
7//! generated for a direct formatting change, named `P1`, `T3`, `ce2`. Both are
8//! `style:style` elements, both inherit through `style:parent-style-name`, and
9//! nothing downstream needs to know which kind it is holding. They are collected
10//! into one table here, keyed by family and name, because ODF scopes a style
11//! name within its family and a paragraph style and a cell style may share one.
12//!
13//! Resolution walks the chain from its root down, so that the nearest style
14//! wins, beginning at the family's `style:default-style`. The answer is cached,
15//! because a spreadsheet asks for the same handful of cell styles once per
16//! visible cell per frame.
17//
18// Author: David M. Anderson
19// Built with AI assistance (Claude, Anthropic)
20
21use std::cell::RefCell;
22use std::collections::HashMap;
23use std::rc::Rc;
24
25use crate::value::{Color, Length, Measure, Percent};
26use crate::xml::{Element, Ns};
27
28/// Which kind of thing a style applies to.
29///
30/// ODF calls this the style family, and it is what makes a style name
31/// meaningful: `Standard` names a paragraph style and a table style and they are
32/// unrelated.
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub enum Family {
35    /// `paragraph`
36    Paragraph,
37    /// `text`, which is a character style.
38    Text,
39    /// `section`
40    Section,
41    /// `table`
42    Table,
43    /// `table-column`
44    TableColumn,
45    /// `table-row`
46    TableRow,
47    /// `table-cell`
48    TableCell,
49    /// `graphic`
50    Graphic,
51    /// `presentation`, the family a placeholder on a slide takes.
52    Presentation,
53    /// `drawing-page`, which is a slide's own background and transition.
54    DrawingPage,
55    /// `chart`
56    Chart,
57    /// `ruby`
58    Ruby,
59    /// A family this crate has no name for, kept so that its styles are still
60    /// collected and still resolve.
61    Other(Box<str>),
62}
63
64impl Family {
65    /// The family an attribute value names.
66    pub fn parse(text: &str) -> Self {
67        match text {
68            "paragraph" => Self::Paragraph,
69            "text" => Self::Text,
70            "section" => Self::Section,
71            "table" => Self::Table,
72            "table-column" => Self::TableColumn,
73            "table-row" => Self::TableRow,
74            "table-cell" => Self::TableCell,
75            "graphic" => Self::Graphic,
76            "presentation" => Self::Presentation,
77            "drawing-page" => Self::DrawingPage,
78            "chart" => Self::Chart,
79            "ruby" => Self::Ruby,
80            other => Self::Other(other.into()),
81        }
82    }
83}
84
85/// How a paragraph's lines sit against its edges.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum TextAlign {
88    /// Against the start edge, which for a left-to-right document is the left.
89    Start,
90    /// Against the end edge.
91    End,
92    /// Centred.
93    Center,
94    /// Both edges, by stretching the spaces.
95    Justify,
96}
97
98/// Where a run sits relative to the baseline.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum Position {
101    /// On it.
102    Baseline,
103    /// Above it, smaller.
104    Super,
105    /// Below it, smaller.
106    Sub,
107}
108
109/// Vertical placement inside a cell.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum VerticalAlign {
112    /// Against the top.
113    Top,
114    /// Centred.
115    Middle,
116    /// Against the bottom.
117    Bottom,
118}
119
120/// A page or column break asked for before or after a paragraph.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum Break {
123    /// No break.
124    Auto,
125    /// A new page.
126    Page,
127    /// A new column.
128    Column,
129}
130
131/// One edge of a border, as ODF writes all three of its parts in one attribute.
132#[derive(Debug, Clone, Copy, PartialEq)]
133pub struct Border {
134    /// How thick.
135    pub width: Length,
136    /// What colour.
137    pub color: Color,
138}
139
140impl Border {
141    /// Parse `0.5pt solid #000000`, in any order of the three parts.
142    ///
143    /// `None` for `none` and for `hidden`, which are how ODF says there is no
144    /// border on this edge — a distinction from the attribute being absent,
145    /// which means inherit.
146    fn parse(text: &str) -> Option<Self> {
147        let mut width = None;
148        let mut color = None;
149        for word in text.split_whitespace() {
150            match word {
151                "none" | "hidden" => return None,
152                _ => {
153                    if let Some(length) = Length::parse(word) {
154                        width = Some(length);
155                    } else if let Some(parsed) = Color::parse(word) {
156                        color = Some(parsed);
157                    }
158                }
159            }
160        }
161        Some(Self {
162            // A border with a style and a colour but no width is the hairline
163            // every application draws for one.
164            width: width.unwrap_or(Length(0.5)),
165            color: color.unwrap_or(Color { r: 0, g: 0, b: 0 }),
166        })
167    }
168}
169
170/// The four edges of a box, in the order ODF's shorthand implies.
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub struct Edges<T> {
173    /// Left.
174    pub left: Option<T>,
175    /// Right.
176    pub right: Option<T>,
177    /// Top.
178    pub top: Option<T>,
179    /// Bottom.
180    pub bottom: Option<T>,
181}
182
183/// Four edges, none of them set. Written out rather than derived because
184/// deriving it would require the edge's own type to have a default, and neither
185/// a length nor a border has one that means anything: the absence of a border is
186/// `None`, not a border of zero width.
187impl<T> Default for Edges<T> {
188    fn default() -> Self {
189        Self {
190            left: None,
191            right: None,
192            top: None,
193            bottom: None,
194        }
195    }
196}
197
198/// Character formatting.
199#[derive(Debug, Clone, Default, PartialEq)]
200pub struct TextProperties {
201    /// The family asked for, after a `style:font-name` has been resolved through
202    /// the document's font face declarations.
203    pub font_family: Option<String>,
204    /// Size, absolute or a proportion of the parent's.
205    pub size: Option<Measure>,
206    /// Bold.
207    pub bold: Option<bool>,
208    /// Italic or oblique, which are not distinguished here because a renderer
209    /// picking a face cannot honour the difference.
210    pub italic: Option<bool>,
211    /// Underlined, of any line style.
212    pub underline: Option<bool>,
213    /// Struck through, of any line style.
214    pub strike: Option<bool>,
215    /// Ink colour.
216    pub color: Option<Color>,
217    /// Highlight behind the characters.
218    pub background: Option<Color>,
219    /// Superscript or subscript.
220    pub position: Option<Position>,
221    /// Drawn in capitals whatever the text says.
222    pub uppercase: Option<bool>,
223}
224
225/// Paragraph formatting.
226#[derive(Debug, Clone, Default, PartialEq)]
227pub struct ParagraphProperties {
228    /// Horizontal alignment.
229    pub align: Option<TextAlign>,
230    /// Space outside the paragraph on each edge.
231    pub margin: Edges<Length>,
232    /// The first line's extra indent, which is negative for a hanging indent.
233    pub text_indent: Option<Length>,
234    /// Line spacing, absolute or a proportion of the font size.
235    pub line_height: Option<Measure>,
236    /// Fill behind the paragraph.
237    pub background: Option<Color>,
238    /// A break asked for before the paragraph.
239    pub break_before: Option<Break>,
240    /// A break asked for after it.
241    pub break_after: Option<Break>,
242    /// Borders, per edge.
243    pub border: Edges<Border>,
244    /// Space between the border and the text, per edge.
245    pub padding: Edges<Length>,
246}
247
248/// Cell formatting, which a spreadsheet reads for every visible cell.
249#[derive(Debug, Clone, Default, PartialEq)]
250pub struct CellProperties {
251    /// Fill.
252    pub background: Option<Color>,
253    /// Vertical placement of the text in the cell.
254    pub vertical_align: Option<VerticalAlign>,
255    /// Borders, per edge.
256    pub border: Edges<Border>,
257    /// Space between the border and the text, per edge.
258    pub padding: Edges<Length>,
259    /// Whether a line too long for the cell wraps rather than overflowing.
260    pub wrap: Option<bool>,
261}
262
263/// What fills a shape or the ground behind a slide.
264///
265/// A named gradient rather than the gradient itself, because ODF defines each
266/// one once in `office:styles` and refers to it by name from every style that
267/// uses it — resolving it here would copy it per style. [`Styles::gradient`]
268/// looks it up.
269#[derive(Debug, Clone, Default, PartialEq)]
270pub enum Fill {
271    /// Nothing is drawn: whatever is behind shows through.
272    #[default]
273    None,
274    /// One colour.
275    Solid(Color),
276    /// The gradient of this name.
277    Gradient(String),
278    /// The picture of this name, stretched over the shape.
279    ///
280    /// Only stretched. ODF also tiles a fill image, at a size the style gives,
281    /// and a tiled fill is [`Fill::None`] here rather than a stretched
282    /// approximation — one tile blown up to the size of a slide is not a
283    /// smaller version of the same thing.
284    Image(String),
285}
286
287/// How a gradient runs, which is the part of it a renderer has to understand.
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum GradientStyle {
290    /// Along an axis, from one edge to the other.
291    Linear,
292    /// Along an axis, from the middle outwards to both edges.
293    Axial,
294    /// Out from a point, in circles.
295    Radial,
296    /// Out from a point, in ellipses.
297    Ellipsoidal,
298    /// Out from a point, in squares.
299    Square,
300    /// Out from a point, in rectangles.
301    Rectangular,
302}
303
304/// One of a document's named gradients.
305///
306/// ODF 1.3 also permits a list of `loext:gradient-stop` children, which
307/// `LibreOffice` writes alongside the two colour attributes and which say the same
308/// thing for a two-stop gradient. The attributes are read and the stops are not:
309/// every gradient in the corpus has exactly two stops that repeat what
310/// `draw:start-color` and `draw:end-color` already say, and a renderer that
311/// interpolated more of them would be drawing something no fixture can check.
312#[derive(Debug, Clone, PartialEq)]
313pub struct Gradient {
314    /// How it runs.
315    pub style: GradientStyle,
316    /// The colour it begins at.
317    pub start: Color,
318    /// The colour it ends at.
319    pub end: Color,
320    /// The direction, in degrees. ODF measures it counter-clockwise from the
321    /// direction that runs bottom to top, so 0 is upward and 90 points left.
322    pub angle: f32,
323    /// How much of each end is the flat colour before the blend begins, as a
324    /// proportion of the whole.
325    pub border: f32,
326    /// Where the centre is, for the styles that have one, as a proportion of the
327    /// shape's width and height.
328    pub center: (f32, f32),
329}
330
331/// Which of ODF's fill styles a shape uses, before the value it needs is found.
332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
333enum FillKind {
334    None,
335    Solid,
336    Gradient,
337    /// A tiled or stretched picture.
338    Bitmap,
339    /// A pattern of lines, which nothing here draws.
340    Hatch,
341}
342
343/// How a shape is filled and outlined.
344///
345/// **The kind of fill and the value it uses are separate properties and inherit
346/// separately**, which is the whole reason they are separate fields here. A
347/// style may set `draw:fill-color` and say nothing about `draw:fill`: that names
348/// the colour a solid fill would use and does *not* turn the fill on, so a shape
349/// whose parent style says `draw:fill="none"` stays empty. Reading the colour as
350/// though it were the fill puts a white box over the slide, which is what a
351/// template's subtitle placeholder did until this was split.
352#[derive(Debug, Clone, Default, PartialEq)]
353pub struct GraphicProperties {
354    kind: Option<FillKind>,
355    color: Option<Color>,
356    gradient: Option<String>,
357    image: Option<String>,
358    /// Outline colour, absent when the stroke is `none`.
359    pub stroke: Option<Color>,
360    /// Outline width.
361    pub stroke_width: Option<Length>,
362    /// How opaque the fill is, from zero to one.
363    pub opacity: Option<f32>,
364}
365
366impl GraphicProperties {
367    /// What actually fills the shape, once the kind and its value are put
368    /// together.
369    ///
370    /// A kind whose value the style never gave — `draw:fill="gradient"` with no
371    /// gradient named — fills nothing, which is the honest answer and not a
372    /// guess at which gradient was meant.
373    pub fn fill(&self) -> Fill {
374        match self.kind {
375            None | Some(FillKind::None | FillKind::Hatch) => Fill::None,
376            Some(FillKind::Solid) => self.color.map_or(Fill::None, Fill::Solid),
377            Some(FillKind::Gradient) => self.gradient.clone().map_or(Fill::None, Fill::Gradient),
378            Some(FillKind::Bitmap) => self.image.clone().map_or(Fill::None, Fill::Image),
379        }
380    }
381}
382
383/// Everything a resolved style says, across every family.
384///
385/// One type rather than one per family, because a paragraph carries character
386/// properties, a cell carries paragraph properties, and a renderer asking for a
387/// cell's font would otherwise have to resolve three styles and merge them
388/// itself.
389#[derive(Debug, Clone, Default, PartialEq)]
390pub struct Properties {
391    /// Character formatting.
392    pub text: TextProperties,
393    /// Paragraph formatting.
394    pub paragraph: ParagraphProperties,
395    /// Cell formatting.
396    pub cell: CellProperties,
397    /// Shape formatting.
398    pub graphic: GraphicProperties,
399    /// Whether the master page's own background shows through, from a
400    /// `drawing-page` style. A presentation's, and `None` where nothing said.
401    pub background_visible: Option<bool>,
402    /// Whether the master page's decorations are drawn.
403    pub background_objects_visible: Option<bool>,
404    /// A column's width, from a `table-column` style.
405    pub column_width: Option<Length>,
406    /// A row's height, from a `table-row` style.
407    pub row_height: Option<Length>,
408}
409
410/// A page's dimensions and margins, from a `style:page-layout`.
411#[derive(Debug, Clone, PartialEq)]
412pub struct PageLayout {
413    /// The page's width.
414    pub width: Length,
415    /// Its height.
416    pub height: Length,
417    /// The margins inside it.
418    pub margin: Edges<Length>,
419}
420
421impl Default for PageLayout {
422    /// US Letter portrait with one-inch margins.
423    ///
424    /// Reached only by a document that declares no page layout at all, which no
425    /// office application produces: the answer normally comes from the document's
426    /// own master page and this is never consulted. The size still has to be
427    /// something, and it is the paper the producer on this machine writes —
428    /// `LibreOffice` 25.2 puts `8.5in` by `11in` in every document it creates here.
429    /// A4 would be the other choice and neither is more right.
430    fn default() -> Self {
431        Self {
432            width: Length(612.0),
433            height: Length(792.0),
434            margin: Edges {
435                left: Some(Length(72.0)),
436                right: Some(Length(72.0)),
437                top: Some(Length(72.0)),
438                bottom: Some(Length(72.0)),
439            },
440        }
441    }
442}
443
444/// One style as the document declares it.
445pub struct Style {
446    /// Its name, as everything else refers to it.
447    pub name: String,
448    /// The name a person sees in an application's style list, where it differs
449    /// from the name in the file. ODF encodes a space as `_20_` in a style name
450    /// and carries the readable spelling in this attribute.
451    pub display_name: Option<String>,
452    /// Its family.
453    pub family: Family,
454    /// The style it inherits from.
455    pub parent: Option<String>,
456    /// The `style:style` element itself, so that a property this crate does not
457    /// model is still reachable.
458    pub element: Element,
459}
460
461/// Every style in a document, from both of the places they live.
462pub struct Styles {
463    by_name: HashMap<(Family, String), Style>,
464    defaults: HashMap<Family, Element>,
465    page_layouts: HashMap<String, PageLayout>,
466    master_pages: HashMap<String, Element>,
467    lists: HashMap<String, Element>,
468    gradients: HashMap<String, Gradient>,
469    fill_images: HashMap<String, String>,
470    font_faces: HashMap<String, String>,
471    cache: RefCell<HashMap<(Family, String), Rc<Properties>>>,
472}
473
474impl Styles {
475    /// Collect the styles of a document from its two parts.
476    ///
477    /// Both are optional because both are optional in the format: a package may
478    /// carry all its formatting in automatic styles in `content.xml` and no
479    /// `styles.xml` at all.
480    pub fn collect(content: Option<&Element>, styles: Option<&Element>) -> Self {
481        let mut this = Self {
482            by_name: HashMap::new(),
483            defaults: HashMap::new(),
484            page_layouts: HashMap::new(),
485            master_pages: HashMap::new(),
486            lists: HashMap::new(),
487            gradients: HashMap::new(),
488            fill_images: HashMap::new(),
489            font_faces: HashMap::new(),
490            cache: RefCell::new(HashMap::new()),
491        };
492        // styles.xml first, so that an automatic style in content.xml with the
493        // same family and name as a named one wins. They do not collide in
494        // practice, and the order is what decides if they ever do.
495        for root in [styles, content].into_iter().flatten() {
496            this.collect_from(root);
497        }
498        this
499    }
500
501    fn collect_from(&mut self, root: &Element) {
502        for container in root.elements() {
503            match () {
504                () if container.is(&Ns::Office, "font-face-decls") => {
505                    for face in container.elements() {
506                        let Some(name) = face.attr(&Ns::Style, "name") else {
507                            continue;
508                        };
509                        // `svg:font-family` is the family a renderer asks the
510                        // system for; the declaration's own name is only a
511                        // handle that the styles refer to it by, and the two
512                        // usually but not always agree.
513                        let family = face
514                            .attr(&Ns::Svg, "font-family")
515                            .unwrap_or(name)
516                            .trim_matches('\'')
517                            .to_owned();
518                        self.font_faces.insert(name.to_owned(), family);
519                    }
520                }
521                () if container.is(&Ns::Office, "styles")
522                    || container.is(&Ns::Office, "automatic-styles") =>
523                {
524                    for element in container.elements() {
525                        self.collect_style(element);
526                    }
527                }
528                () if container.is(&Ns::Office, "master-styles") => {
529                    for master in container.elements() {
530                        if let Some(name) = master.attr(&Ns::Style, "name") {
531                            self.master_pages.insert(name.to_owned(), master.clone());
532                        }
533                    }
534                }
535                () => {}
536            }
537        }
538    }
539
540    fn collect_style(&mut self, element: &Element) {
541        if element.is(&Ns::Style, "style") {
542            let Some(name) = element.attr(&Ns::Style, "name") else {
543                return;
544            };
545            let family = Family::parse(element.attr(&Ns::Style, "family").unwrap_or_default());
546            let style = Style {
547                name: name.to_owned(),
548                display_name: element
549                    .attr(&Ns::Style, "display-name")
550                    .map(ToOwned::to_owned),
551                family: family.clone(),
552                parent: element
553                    .attr(&Ns::Style, "parent-style-name")
554                    .map(ToOwned::to_owned),
555                element: element.clone(),
556            };
557            self.by_name.insert((family, name.to_owned()), style);
558        } else if element.is(&Ns::Style, "default-style") {
559            let family = Family::parse(element.attr(&Ns::Style, "family").unwrap_or_default());
560            self.defaults.insert(family, element.clone());
561        } else if element.is(&Ns::Style, "page-layout") {
562            if let Some(name) = element.attr(&Ns::Style, "name") {
563                self.page_layouts
564                    .insert(name.to_owned(), page_layout(element));
565            }
566        } else if element.is(&Ns::Draw, "fill-image") {
567            if let (Some(name), Some(href)) = (
568                element.attr(&Ns::Draw, "name"),
569                element.attr(&Ns::Xlink, "href"),
570            ) {
571                self.fill_images.insert(name.to_owned(), href.to_owned());
572            }
573        } else if element.is(&Ns::Draw, "gradient") {
574            if let Some(name) = element.attr(&Ns::Draw, "name") {
575                self.gradients.insert(name.to_owned(), gradient(element));
576            }
577        } else if element.is(&Ns::Text, "list-style")
578            && let Some(name) = element.attr(&Ns::Style, "name")
579        {
580            self.lists.insert(name.to_owned(), element.clone());
581        }
582    }
583
584    /// A style by family and name.
585    pub fn style(&self, family: &Family, name: &str) -> Option<&Style> {
586        self.by_name.get(&(family.clone(), name.to_owned()))
587    }
588
589    /// A gradient by name, as a fill refers to one.
590    pub fn gradient(&self, name: &str) -> Option<&Gradient> {
591        self.gradients.get(name)
592    }
593
594    /// Where the picture of a named fill image lives inside the package.
595    pub fn fill_image(&self, name: &str) -> Option<&str> {
596        self.fill_images.get(name).map(String::as_str)
597    }
598
599    /// A list style by name, as the `text:list-style-name` of a list refers to
600    /// one.
601    pub fn list_style(&self, name: &str) -> Option<&Element> {
602        self.lists.get(name)
603    }
604
605    /// A master page by name.
606    pub fn master_page(&self, name: &str) -> Option<&Element> {
607        self.master_pages.get(name)
608    }
609
610    /// The page layout a master page points at.
611    pub fn page_layout_of(&self, master_page: &str) -> Option<&PageLayout> {
612        let master = self.master_pages.get(master_page)?;
613        let layout = master.attr(&Ns::Style, "page-layout-name")?;
614        self.page_layouts.get(layout)
615    }
616
617    /// The resolved properties of a style, with its whole inheritance chain and
618    /// its family's default applied.
619    ///
620    /// A name that is not in the document resolves to the family's default,
621    /// which is what an application does with a dangling style reference: the
622    /// paragraph is shown rather than refused.
623    pub fn resolve(&self, family: &Family, name: &str) -> Rc<Properties> {
624        let key = (family.clone(), name.to_owned());
625        if let Some(cached) = self.cache.borrow().get(&key) {
626            return Rc::clone(cached);
627        }
628
629        let mut properties = Properties::default();
630        if let Some(default) = self.defaults.get(family) {
631            properties.apply(default, &self.font_faces);
632        }
633        // Root first, so that the style asked for is applied last and wins.
634        for style in self.chain(family, name).into_iter().rev() {
635            properties.apply(&style.element, &self.font_faces);
636        }
637
638        let properties = Rc::new(properties);
639        self.cache.borrow_mut().insert(key, Rc::clone(&properties));
640        properties
641    }
642
643    /// The style and its ancestors, nearest first.
644    ///
645    /// A cycle in the chain — which no writer produces and a hand-edited file
646    /// can — stops at the style it returns to rather than hanging.
647    fn chain(&self, family: &Family, name: &str) -> Vec<&Style> {
648        let mut chain: Vec<&Style> = Vec::new();
649        let mut next: Option<&str> = Some(name);
650        while let Some(current) = next {
651            if chain.iter().any(|s| s.name == current) {
652                break;
653            }
654            let Some(style) = self.style(family, current) else {
655                break;
656            };
657            next = style.parent.as_deref();
658            chain.push(style);
659        }
660        chain
661    }
662
663    /// The family the properties of a cell's text come from: a cell style's
664    /// `style:parent-style-name` chain carries the paragraph and text
665    /// properties, so a cell resolves in one call.
666    pub fn font_family(&self, declared: &str) -> String {
667        self.font_faces
668            .get(declared)
669            .cloned()
670            .unwrap_or_else(|| declared.to_owned())
671    }
672}
673
674fn gradient(element: &Element) -> Gradient {
675    let color = |local: &str, fallback: Color| {
676        element
677            .attr(&Ns::Draw, local)
678            .and_then(Color::parse)
679            .unwrap_or(fallback)
680    };
681    let proportion = |local: &str| {
682        element
683            .attr(&Ns::Draw, local)
684            .and_then(Percent::parse)
685            .map_or(0.0, Percent::fraction)
686    };
687    Gradient {
688        style: match element.attr(&Ns::Draw, "style") {
689            Some("axial") => GradientStyle::Axial,
690            Some("radial") => GradientStyle::Radial,
691            Some("ellipsoid") => GradientStyle::Ellipsoidal,
692            Some("square") => GradientStyle::Square,
693            Some("rectangular") => GradientStyle::Rectangular,
694            _ => GradientStyle::Linear,
695        },
696        start: color("start-color", Color { r: 0, g: 0, b: 0 }),
697        end: color(
698            "end-color",
699            Color {
700                r: 0xff,
701                g: 0xff,
702                b: 0xff,
703            },
704        ),
705        // Written as `270deg`, and occasionally as a bare tenth of a degree by
706        // producers older than the unit.
707        angle: element.attr(&Ns::Draw, "angle").map_or(0.0, parse_angle),
708        border: proportion("border"),
709        center: (proportion("cx"), proportion("cy")),
710    }
711}
712
713/// An ODF angle in degrees.
714///
715/// `270deg` is the spelling ODF 1.2 introduced. Before it the attribute was a
716/// plain number in tenths of a degree, which some producers still write, so a
717/// value with no unit is read that way.
718fn parse_angle(text: &str) -> f32 {
719    let text = text.trim();
720    match text.strip_suffix("deg") {
721        Some(degrees) => degrees.trim().parse().unwrap_or(0.0),
722        None => text.parse::<f32>().unwrap_or(0.0) / 10.0,
723    }
724}
725
726fn page_layout(element: &Element) -> PageLayout {
727    let mut layout = PageLayout::default();
728    if let Some(properties) = element.child(&Ns::Style, "page-layout-properties") {
729        if let Some(width) = properties
730            .attr(&Ns::Fo, "page-width")
731            .and_then(Length::parse)
732        {
733            layout.width = width;
734        }
735        if let Some(height) = properties
736            .attr(&Ns::Fo, "page-height")
737            .and_then(Length::parse)
738        {
739            layout.height = height;
740        }
741        let mut margin = Edges::default();
742        read_edges(properties, "margin", &mut margin, Length::parse);
743        // An edge the layout does not name keeps the default rather than
744        // becoming nothing, because a page layout that sets only its top margin
745        // is not asking for the other three to be zero.
746        layout.margin.left = margin.left.or(layout.margin.left);
747        layout.margin.right = margin.right.or(layout.margin.right);
748        layout.margin.top = margin.top.or(layout.margin.top);
749        layout.margin.bottom = margin.bottom.or(layout.margin.bottom);
750    }
751    layout
752}
753
754/// Read ODF's edge shorthand: `fo:margin` sets all four, and
755/// `fo:margin-left` and its siblings override one each.
756fn read_edges<T: Copy>(
757    properties: &Element,
758    base: &str,
759    into: &mut Edges<T>,
760    parse: impl Fn(&str) -> Option<T>,
761) {
762    if let Some(all) = properties.attr(&Ns::Fo, base).and_then(&parse) {
763        *into = Edges {
764            left: Some(all),
765            right: Some(all),
766            top: Some(all),
767            bottom: Some(all),
768        };
769    }
770    if let Some(v) = properties
771        .attr(&Ns::Fo, &format!("{base}-left"))
772        .and_then(&parse)
773    {
774        into.left = Some(v);
775    }
776    if let Some(v) = properties
777        .attr(&Ns::Fo, &format!("{base}-right"))
778        .and_then(&parse)
779    {
780        into.right = Some(v);
781    }
782    if let Some(v) = properties
783        .attr(&Ns::Fo, &format!("{base}-top"))
784        .and_then(&parse)
785    {
786        into.top = Some(v);
787    }
788    if let Some(v) = properties
789        .attr(&Ns::Fo, &format!("{base}-bottom"))
790        .and_then(&parse)
791    {
792        into.bottom = Some(v);
793    }
794}
795
796impl Properties {
797    /// Apply one style's property elements over what is already here.
798    ///
799    /// Only a property the element states is changed; everything else keeps the
800    /// value it inherited, which is what makes walking a chain root first give
801    /// the right answer.
802    fn apply(&mut self, style: &Element, font_faces: &HashMap<String, String>) {
803        for properties in style.elements() {
804            if properties.is(&Ns::Style, "text-properties") {
805                self.text.apply(properties, font_faces);
806            } else if properties.is(&Ns::Style, "paragraph-properties") {
807                self.paragraph.apply(properties);
808            } else if properties.is(&Ns::Style, "table-cell-properties") {
809                self.cell.apply(properties);
810            } else if properties.is(&Ns::Style, "graphic-properties")
811                || properties.is(&Ns::Style, "drawing-page-properties")
812            {
813                self.graphic.apply(properties);
814                // Two of a slide's own switches over what its master gives it.
815                // They live on the same element as the fill and are read here so
816                // that they inherit through the style chain like everything else.
817                if let Some(visible) = properties
818                    .attr(&Ns::Presentation, "background-visible")
819                    .and_then(crate::value::boolean)
820                {
821                    self.background_visible = Some(visible);
822                }
823                if let Some(visible) = properties
824                    .attr(&Ns::Presentation, "background-objects-visible")
825                    .and_then(crate::value::boolean)
826                {
827                    self.background_objects_visible = Some(visible);
828                }
829            } else if properties.is(&Ns::Style, "table-column-properties") {
830                if let Some(width) = properties
831                    .attr(&Ns::Style, "column-width")
832                    .and_then(Length::parse)
833                {
834                    self.column_width = Some(width);
835                }
836            } else if properties.is(&Ns::Style, "table-row-properties")
837                && let Some(height) = properties
838                    .attr(&Ns::Style, "row-height")
839                    .and_then(Length::parse)
840            {
841                self.row_height = Some(height);
842            }
843        }
844    }
845}
846
847impl TextProperties {
848    fn apply(&mut self, p: &Element, font_faces: &HashMap<String, String>) {
849        // `style:font-name` points at a font face declaration and `fo:font-family`
850        // names a family directly. A style may carry both, and the declaration is
851        // the more specific of the two.
852        if let Some(name) = p.attr(&Ns::Fo, "font-family") {
853            self.font_family = Some(name.trim_matches('\'').to_owned());
854        }
855        if let Some(name) = p.attr(&Ns::Style, "font-name") {
856            self.font_family = Some(
857                font_faces
858                    .get(name)
859                    .cloned()
860                    .unwrap_or_else(|| name.to_owned()),
861            );
862        }
863        if let Some(size) = p.attr(&Ns::Fo, "font-size").and_then(Measure::parse) {
864            self.size = Some(size);
865        }
866        if let Some(weight) = p.attr(&Ns::Fo, "font-weight") {
867            // A numeric weight is the CSS scale, where 600 and above reads as
868            // bold to anything that has two faces to choose between.
869            self.bold = Some(match weight {
870                "normal" => false,
871                "bold" => true,
872                other => other.parse::<u32>().is_ok_and(|n| n >= 600),
873            });
874        }
875        if let Some(style) = p.attr(&Ns::Fo, "font-style") {
876            self.italic = Some(style != "normal");
877        }
878        if let Some(line) = p.attr(&Ns::Style, "text-underline-style") {
879            self.underline = Some(line != "none");
880        }
881        if let Some(line) = p.attr(&Ns::Style, "text-line-through-style") {
882            self.strike = Some(line != "none");
883        }
884        if let Some(color) = p.attr(&Ns::Fo, "color") {
885            self.color = Color::parse(color);
886        }
887        if let Some(color) = p.attr(&Ns::Fo, "background-color") {
888            self.background = Color::parse(color);
889        }
890        if let Some(position) = p.attr(&Ns::Style, "text-position") {
891            // The attribute is a vertical offset and optionally a size, as in
892            // `super 58%` or `-33% 58%`. Only the direction is read: a renderer
893            // that placed the glyph at the stated offset and scaled it by the
894            // stated amount would be doing typesetting, and what is wanted here
895            // is the distinction between superscript and subscript.
896            let first = position.split_whitespace().next().unwrap_or_default();
897            self.position = Some(match first {
898                "super" => Position::Super,
899                "sub" => Position::Sub,
900                _ => match Percent::parse(first) {
901                    Some(percent) if percent.0 > 0.0 => Position::Super,
902                    Some(percent) if percent.0 < 0.0 => Position::Sub,
903                    _ => Position::Baseline,
904                },
905            });
906        }
907        if let Some(transform) = p.attr(&Ns::Fo, "text-transform") {
908            self.uppercase = Some(transform == "uppercase");
909        }
910    }
911}
912
913impl ParagraphProperties {
914    fn apply(&mut self, p: &Element) {
915        if let Some(align) = p.attr(&Ns::Fo, "text-align") {
916            self.align = match align {
917                // `left` and `right` are the writing-direction-independent
918                // spellings' siblings, and for a left-to-right document they are
919                // the same thing. A right-to-left document would need the
920                // direction to tell them apart, which is what §6 of DESIGN.md
921                // says this release does not do.
922                "start" | "left" => Some(TextAlign::Start),
923                "end" | "right" => Some(TextAlign::End),
924                "center" => Some(TextAlign::Center),
925                "justify" => Some(TextAlign::Justify),
926                _ => self.align,
927            };
928        }
929        read_edges(p, "margin", &mut self.margin, Length::parse);
930        read_edges(p, "padding", &mut self.padding, Length::parse);
931        read_edges(p, "border", &mut self.border, Border::parse);
932        if let Some(indent) = p.attr(&Ns::Fo, "text-indent").and_then(Length::parse) {
933            self.text_indent = Some(indent);
934        }
935        if let Some(height) = p.attr(&Ns::Fo, "line-height") {
936            self.line_height = Measure::parse(height);
937        }
938        if let Some(color) = p.attr(&Ns::Fo, "background-color") {
939            self.background = Color::parse(color);
940        }
941        if let Some(before) = p.attr(&Ns::Fo, "break-before") {
942            self.break_before = Some(parse_break(before));
943        }
944        if let Some(after) = p.attr(&Ns::Fo, "break-after") {
945            self.break_after = Some(parse_break(after));
946        }
947    }
948}
949
950fn parse_break(text: &str) -> Break {
951    match text {
952        "page" => Break::Page,
953        "column" => Break::Column,
954        _ => Break::Auto,
955    }
956}
957
958impl CellProperties {
959    fn apply(&mut self, p: &Element) {
960        if let Some(color) = p.attr(&Ns::Fo, "background-color") {
961            self.background = Color::parse(color);
962        }
963        if let Some(align) = p.attr(&Ns::Style, "vertical-align") {
964            self.vertical_align = match align {
965                "top" => Some(VerticalAlign::Top),
966                "middle" => Some(VerticalAlign::Middle),
967                // `bottom`, and `automatic`, which is the fourth value ODF
968                // defines and means bottom for a cell: it is what a spreadsheet
969                // shows for a cell nobody has set.
970                _ => Some(VerticalAlign::Bottom),
971            };
972        }
973        read_edges(p, "border", &mut self.border, Border::parse);
974        read_edges(p, "padding", &mut self.padding, Length::parse);
975        if let Some(wrap) = p.attr(&Ns::Fo, "wrap-option") {
976            self.wrap = Some(wrap == "wrap");
977        }
978    }
979}
980
981impl GraphicProperties {
982    fn apply(&mut self, p: &Element) {
983        // Each of these is its own property and each inherits on its own. A
984        // style that changes only the shade of an already-solid shape writes the
985        // colour and nothing else; one that turns the fill off writes the kind
986        // and nothing else.
987        if let Some(kind) = p.attr(&Ns::Draw, "fill") {
988            self.kind = Some(match kind {
989                "solid" => FillKind::Solid,
990                "gradient" => FillKind::Gradient,
991                "bitmap" => FillKind::Bitmap,
992                "hatch" => FillKind::Hatch,
993                _ => FillKind::None,
994            });
995        }
996        if let Some(color) = p.attr(&Ns::Draw, "fill-color").and_then(Color::parse) {
997            self.color = Some(color);
998        }
999        if let Some(name) = p.attr(&Ns::Draw, "fill-gradient-name") {
1000            self.gradient = Some(name.to_owned());
1001        }
1002        // Only a stretched picture: see `Fill::Image`. A tiled one leaves the
1003        // name unset, so the fill resolves to nothing rather than to one tile
1004        // blown up to the size of the shape.
1005        if let Some(name) = p.attr(&Ns::Draw, "fill-image-name") {
1006            self.image = (p.attr(&Ns::Style, "repeat").unwrap_or("stretch") == "stretch")
1007                .then(|| name.to_owned());
1008        }
1009
1010        match p.attr(&Ns::Draw, "stroke") {
1011            Some("none") => self.stroke = None,
1012            _ => {
1013                if let Some(color) = p.attr(&Ns::Svg, "stroke-color").and_then(Color::parse) {
1014                    self.stroke = Some(color);
1015                }
1016            }
1017        }
1018        if let Some(width) = p.attr(&Ns::Svg, "stroke-width").and_then(Length::parse) {
1019            self.stroke_width = Some(width);
1020        }
1021        if let Some(opacity) = p.attr(&Ns::Draw, "opacity").and_then(Percent::parse) {
1022            self.opacity = Some(opacity.fraction().clamp(0.0, 1.0));
1023        }
1024    }
1025}