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    /// Where the shape's own label sits between its top and bottom edges.
365    pub text_anchor: Option<Anchor>,
366}
367
368/// Where a shape's label sits between its top and bottom edges.
369///
370/// `draw:textarea-vertical-align`. A drawing shape is a box a label is centred
371/// in far more often than it is a box a label starts at the top of, so the two
372/// look nothing alike and the attribute cannot be ignored: the numeral in a
373/// circle is the ordinary case.
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
375pub enum Anchor {
376    /// Against the top edge.
377    Top,
378    /// Centred between the two.
379    Middle,
380    /// Against the bottom edge.
381    Bottom,
382}
383
384impl Anchor {
385    /// How much of the room left over above and below the label goes above it.
386    #[must_use]
387    pub fn share(self) -> f32 {
388        match self {
389            Self::Top => 0.0,
390            Self::Middle => 0.5,
391            Self::Bottom => 1.0,
392        }
393    }
394}
395
396impl GraphicProperties {
397    /// What actually fills the shape, once the kind and its value are put
398    /// together.
399    ///
400    /// A kind whose value the style never gave — `draw:fill="gradient"` with no
401    /// gradient named — fills nothing, which is the honest answer and not a
402    /// guess at which gradient was meant.
403    pub fn fill(&self) -> Fill {
404        match self.kind {
405            None | Some(FillKind::None | FillKind::Hatch) => Fill::None,
406            Some(FillKind::Solid) => self.color.map_or(Fill::None, Fill::Solid),
407            Some(FillKind::Gradient) => self.gradient.clone().map_or(Fill::None, Fill::Gradient),
408            Some(FillKind::Bitmap) => self.image.clone().map_or(Fill::None, Fill::Image),
409        }
410    }
411}
412
413/// Everything a resolved style says, across every family.
414///
415/// One type rather than one per family, because a paragraph carries character
416/// properties, a cell carries paragraph properties, and a renderer asking for a
417/// cell's font would otherwise have to resolve three styles and merge them
418/// itself.
419#[derive(Debug, Clone, Default, PartialEq)]
420pub struct Properties {
421    /// Character formatting.
422    pub text: TextProperties,
423    /// Paragraph formatting.
424    pub paragraph: ParagraphProperties,
425    /// Cell formatting.
426    pub cell: CellProperties,
427    /// Shape formatting.
428    pub graphic: GraphicProperties,
429    /// Whether the master page's own background shows through, from a
430    /// `drawing-page` style. A presentation's, and `None` where nothing said.
431    pub background_visible: Option<bool>,
432    /// Whether the master page's decorations are drawn.
433    pub background_objects_visible: Option<bool>,
434    /// A column's width, from a `table-column` style.
435    pub column_width: Option<Length>,
436    /// A row's height, from a `table-row` style.
437    pub row_height: Option<Length>,
438}
439
440/// A page's dimensions and margins, from a `style:page-layout`.
441#[derive(Debug, Clone, PartialEq)]
442pub struct PageLayout {
443    /// The page's width.
444    pub width: Length,
445    /// Its height.
446    pub height: Length,
447    /// The margins inside it.
448    pub margin: Edges<Length>,
449}
450
451impl Default for PageLayout {
452    /// US Letter portrait with one-inch margins.
453    ///
454    /// Reached only by a document that declares no page layout at all, which no
455    /// office application produces: the answer normally comes from the document's
456    /// own master page and this is never consulted. The size still has to be
457    /// something, and it is the paper the producer on this machine writes —
458    /// `LibreOffice` 25.2 puts `8.5in` by `11in` in every document it creates here.
459    /// A4 would be the other choice and neither is more right.
460    fn default() -> Self {
461        Self {
462            width: Length(612.0),
463            height: Length(792.0),
464            margin: Edges {
465                left: Some(Length(72.0)),
466                right: Some(Length(72.0)),
467                top: Some(Length(72.0)),
468                bottom: Some(Length(72.0)),
469            },
470        }
471    }
472}
473
474/// One style as the document declares it.
475pub struct Style {
476    /// Its name, as everything else refers to it.
477    pub name: String,
478    /// The name a person sees in an application's style list, where it differs
479    /// from the name in the file. ODF encodes a space as `_20_` in a style name
480    /// and carries the readable spelling in this attribute.
481    pub display_name: Option<String>,
482    /// Its family.
483    pub family: Family,
484    /// The style it inherits from.
485    pub parent: Option<String>,
486    /// The `style:style` element itself, so that a property this crate does not
487    /// model is still reachable.
488    pub element: Element,
489}
490
491/// Every style in a document, from both of the places they live.
492pub struct Styles {
493    by_name: HashMap<(Family, String), Style>,
494    defaults: HashMap<Family, Element>,
495    page_layouts: HashMap<String, PageLayout>,
496    master_pages: HashMap<String, Element>,
497    lists: HashMap<String, Element>,
498    gradients: HashMap<String, Gradient>,
499    fill_images: HashMap<String, String>,
500    font_faces: HashMap<String, String>,
501    cache: RefCell<HashMap<(Family, String), Rc<Properties>>>,
502}
503
504impl Styles {
505    /// Collect the styles of a document from its two parts.
506    ///
507    /// Both are optional because both are optional in the format: a package may
508    /// carry all its formatting in automatic styles in `content.xml` and no
509    /// `styles.xml` at all.
510    pub fn collect(content: Option<&Element>, styles: Option<&Element>) -> Self {
511        let mut this = Self {
512            by_name: HashMap::new(),
513            defaults: HashMap::new(),
514            page_layouts: HashMap::new(),
515            master_pages: HashMap::new(),
516            lists: HashMap::new(),
517            gradients: HashMap::new(),
518            fill_images: HashMap::new(),
519            font_faces: HashMap::new(),
520            cache: RefCell::new(HashMap::new()),
521        };
522        // styles.xml first, so that an automatic style in content.xml with the
523        // same family and name as a named one wins. They do not collide in
524        // practice, and the order is what decides if they ever do.
525        for root in [styles, content].into_iter().flatten() {
526            this.collect_from(root);
527        }
528        this
529    }
530
531    fn collect_from(&mut self, root: &Element) {
532        for container in root.elements() {
533            match () {
534                () if container.is(&Ns::Office, "font-face-decls") => {
535                    for face in container.elements() {
536                        let Some(name) = face.attr(&Ns::Style, "name") else {
537                            continue;
538                        };
539                        // `svg:font-family` is the family a renderer asks the
540                        // system for; the declaration's own name is only a
541                        // handle that the styles refer to it by, and the two
542                        // usually but not always agree.
543                        let family = face
544                            .attr(&Ns::Svg, "font-family")
545                            .unwrap_or(name)
546                            .trim_matches('\'')
547                            .to_owned();
548                        self.font_faces.insert(name.to_owned(), family);
549                    }
550                }
551                () if container.is(&Ns::Office, "styles")
552                    || container.is(&Ns::Office, "automatic-styles") =>
553                {
554                    for element in container.elements() {
555                        self.collect_style(element);
556                    }
557                }
558                () if container.is(&Ns::Office, "master-styles") => {
559                    for master in container.elements() {
560                        if let Some(name) = master.attr(&Ns::Style, "name") {
561                            self.master_pages.insert(name.to_owned(), master.clone());
562                        }
563                    }
564                }
565                () => {}
566            }
567        }
568    }
569
570    fn collect_style(&mut self, element: &Element) {
571        if element.is(&Ns::Style, "style") {
572            let Some(name) = element.attr(&Ns::Style, "name") else {
573                return;
574            };
575            let family = Family::parse(element.attr(&Ns::Style, "family").unwrap_or_default());
576            let style = Style {
577                name: name.to_owned(),
578                display_name: element
579                    .attr(&Ns::Style, "display-name")
580                    .map(ToOwned::to_owned),
581                family: family.clone(),
582                parent: element
583                    .attr(&Ns::Style, "parent-style-name")
584                    .map(ToOwned::to_owned),
585                element: element.clone(),
586            };
587            self.by_name.insert((family, name.to_owned()), style);
588        } else if element.is(&Ns::Style, "default-style") {
589            let family = Family::parse(element.attr(&Ns::Style, "family").unwrap_or_default());
590            self.defaults.insert(family, element.clone());
591        } else if element.is(&Ns::Style, "page-layout") {
592            if let Some(name) = element.attr(&Ns::Style, "name") {
593                self.page_layouts
594                    .insert(name.to_owned(), page_layout(element));
595            }
596        } else if element.is(&Ns::Draw, "fill-image") {
597            if let (Some(name), Some(href)) = (
598                element.attr(&Ns::Draw, "name"),
599                element.attr(&Ns::Xlink, "href"),
600            ) {
601                self.fill_images.insert(name.to_owned(), href.to_owned());
602            }
603        } else if element.is(&Ns::Draw, "gradient") {
604            if let Some(name) = element.attr(&Ns::Draw, "name") {
605                self.gradients.insert(name.to_owned(), gradient(element));
606            }
607        } else if element.is(&Ns::Text, "list-style")
608            && let Some(name) = element.attr(&Ns::Style, "name")
609        {
610            self.lists.insert(name.to_owned(), element.clone());
611        }
612    }
613
614    /// Take in a style an edit has written into the document, so that it
615    /// resolves from now on.
616    ///
617    /// Nothing is ever taken out: a style an undo removed from the tree stays
618    /// here, which keeps its name from being given to a different style while
619    /// a resolution of it may still be cached.
620    pub fn add(&mut self, element: &Element) {
621        if let (Some(name), Some(family)) = (
622            element.attr(&Ns::Style, "name"),
623            element.attr(&Ns::Style, "family"),
624        ) {
625            let key = (Family::parse(family), name.to_owned());
626            self.cache.borrow_mut().remove(&key);
627        }
628        self.collect_style(element);
629    }
630
631    /// A style by family and name.
632    pub fn style(&self, family: &Family, name: &str) -> Option<&Style> {
633        self.by_name.get(&(family.clone(), name.to_owned()))
634    }
635
636    /// A gradient by name, as a fill refers to one.
637    pub fn gradient(&self, name: &str) -> Option<&Gradient> {
638        self.gradients.get(name)
639    }
640
641    /// Where the picture of a named fill image lives inside the package.
642    pub fn fill_image(&self, name: &str) -> Option<&str> {
643        self.fill_images.get(name).map(String::as_str)
644    }
645
646    /// A list style by name, as the `text:list-style-name` of a list refers to
647    /// one.
648    pub fn list_style(&self, name: &str) -> Option<&Element> {
649        self.lists.get(name)
650    }
651
652    /// A master page by name.
653    pub fn master_page(&self, name: &str) -> Option<&Element> {
654        self.master_pages.get(name)
655    }
656
657    /// The page layout a master page points at.
658    pub fn page_layout_of(&self, master_page: &str) -> Option<&PageLayout> {
659        let master = self.master_pages.get(master_page)?;
660        let layout = master.attr(&Ns::Style, "page-layout-name")?;
661        self.page_layouts.get(layout)
662    }
663
664    /// The resolved properties of a style, with its whole inheritance chain and
665    /// its family's default applied.
666    ///
667    /// A name that is not in the document resolves to the family's default,
668    /// which is what an application does with a dangling style reference: the
669    /// paragraph is shown rather than refused.
670    pub fn resolve(&self, family: &Family, name: &str) -> Rc<Properties> {
671        let key = (family.clone(), name.to_owned());
672        if let Some(cached) = self.cache.borrow().get(&key) {
673            return Rc::clone(cached);
674        }
675
676        let mut properties = Properties::default();
677        if let Some(default) = self.defaults.get(family) {
678            properties.apply(default, &self.font_faces);
679        }
680        // Root first, so that the style asked for is applied last and wins.
681        for style in self.chain(family, name).into_iter().rev() {
682            properties.apply(&style.element, &self.font_faces);
683        }
684
685        let properties = Rc::new(properties);
686        self.cache.borrow_mut().insert(key, Rc::clone(&properties));
687        properties
688    }
689
690    /// The style and its ancestors, nearest first.
691    ///
692    /// A cycle in the chain — which no writer produces and a hand-edited file
693    /// can — stops at the style it returns to rather than hanging.
694    fn chain(&self, family: &Family, name: &str) -> Vec<&Style> {
695        let mut chain: Vec<&Style> = Vec::new();
696        let mut next: Option<&str> = Some(name);
697        while let Some(current) = next {
698            if chain.iter().any(|s| s.name == current) {
699                break;
700            }
701            let Some(style) = self.style(family, current) else {
702                break;
703            };
704            next = style.parent.as_deref();
705            chain.push(style);
706        }
707        chain
708    }
709
710    /// The family the properties of a cell's text come from: a cell style's
711    /// `style:parent-style-name` chain carries the paragraph and text
712    /// properties, so a cell resolves in one call.
713    pub fn font_family(&self, declared: &str) -> String {
714        self.font_faces
715            .get(declared)
716            .cloned()
717            .unwrap_or_else(|| declared.to_owned())
718    }
719}
720
721fn gradient(element: &Element) -> Gradient {
722    let color = |local: &str, fallback: Color| {
723        element
724            .attr(&Ns::Draw, local)
725            .and_then(Color::parse)
726            .unwrap_or(fallback)
727    };
728    let proportion = |local: &str| {
729        element
730            .attr(&Ns::Draw, local)
731            .and_then(Percent::parse)
732            .map_or(0.0, Percent::fraction)
733    };
734    Gradient {
735        style: match element.attr(&Ns::Draw, "style") {
736            Some("axial") => GradientStyle::Axial,
737            Some("radial") => GradientStyle::Radial,
738            Some("ellipsoid") => GradientStyle::Ellipsoidal,
739            Some("square") => GradientStyle::Square,
740            Some("rectangular") => GradientStyle::Rectangular,
741            _ => GradientStyle::Linear,
742        },
743        start: color("start-color", Color { r: 0, g: 0, b: 0 }),
744        end: color(
745            "end-color",
746            Color {
747                r: 0xff,
748                g: 0xff,
749                b: 0xff,
750            },
751        ),
752        // Written as `270deg`, and occasionally as a bare tenth of a degree by
753        // producers older than the unit.
754        angle: element.attr(&Ns::Draw, "angle").map_or(0.0, parse_angle),
755        border: proportion("border"),
756        center: (proportion("cx"), proportion("cy")),
757    }
758}
759
760/// An ODF angle in degrees.
761///
762/// `270deg` is the spelling ODF 1.2 introduced. Before it the attribute was a
763/// plain number in tenths of a degree, which some producers still write, so a
764/// value with no unit is read that way.
765fn parse_angle(text: &str) -> f32 {
766    let text = text.trim();
767    match text.strip_suffix("deg") {
768        Some(degrees) => degrees.trim().parse().unwrap_or(0.0),
769        None => text.parse::<f32>().unwrap_or(0.0) / 10.0,
770    }
771}
772
773fn page_layout(element: &Element) -> PageLayout {
774    let mut layout = PageLayout::default();
775    if let Some(properties) = element.child(&Ns::Style, "page-layout-properties") {
776        if let Some(width) = properties
777            .attr(&Ns::Fo, "page-width")
778            .and_then(Length::parse)
779        {
780            layout.width = width;
781        }
782        if let Some(height) = properties
783            .attr(&Ns::Fo, "page-height")
784            .and_then(Length::parse)
785        {
786            layout.height = height;
787        }
788        let mut margin = Edges::default();
789        read_edges(properties, "margin", &mut margin, Length::parse);
790        // An edge the layout does not name keeps the default rather than
791        // becoming nothing, because a page layout that sets only its top margin
792        // is not asking for the other three to be zero.
793        layout.margin.left = margin.left.or(layout.margin.left);
794        layout.margin.right = margin.right.or(layout.margin.right);
795        layout.margin.top = margin.top.or(layout.margin.top);
796        layout.margin.bottom = margin.bottom.or(layout.margin.bottom);
797    }
798    layout
799}
800
801/// Read ODF's edge shorthand: `fo:margin` sets all four, and
802/// `fo:margin-left` and its siblings override one each.
803fn read_edges<T: Copy>(
804    properties: &Element,
805    base: &str,
806    into: &mut Edges<T>,
807    parse: impl Fn(&str) -> Option<T>,
808) {
809    if let Some(all) = properties.attr(&Ns::Fo, base).and_then(&parse) {
810        *into = Edges {
811            left: Some(all),
812            right: Some(all),
813            top: Some(all),
814            bottom: Some(all),
815        };
816    }
817    if let Some(v) = properties
818        .attr(&Ns::Fo, &format!("{base}-left"))
819        .and_then(&parse)
820    {
821        into.left = Some(v);
822    }
823    if let Some(v) = properties
824        .attr(&Ns::Fo, &format!("{base}-right"))
825        .and_then(&parse)
826    {
827        into.right = Some(v);
828    }
829    if let Some(v) = properties
830        .attr(&Ns::Fo, &format!("{base}-top"))
831        .and_then(&parse)
832    {
833        into.top = Some(v);
834    }
835    if let Some(v) = properties
836        .attr(&Ns::Fo, &format!("{base}-bottom"))
837        .and_then(&parse)
838    {
839        into.bottom = Some(v);
840    }
841}
842
843impl Properties {
844    /// Apply one style's property elements over what is already here.
845    ///
846    /// Only a property the element states is changed; everything else keeps the
847    /// value it inherited, which is what makes walking a chain root first give
848    /// the right answer.
849    fn apply(&mut self, style: &Element, font_faces: &HashMap<String, String>) {
850        for properties in style.elements() {
851            if properties.is(&Ns::Style, "text-properties") {
852                self.text.apply(properties, font_faces);
853            } else if properties.is(&Ns::Style, "paragraph-properties") {
854                self.paragraph.apply(properties);
855            } else if properties.is(&Ns::Style, "table-cell-properties") {
856                self.cell.apply(properties);
857            } else if properties.is(&Ns::Style, "graphic-properties")
858                || properties.is(&Ns::Style, "drawing-page-properties")
859            {
860                self.graphic.apply(properties);
861                // Two of a slide's own switches over what its master gives it.
862                // They live on the same element as the fill and are read here so
863                // that they inherit through the style chain like everything else.
864                if let Some(visible) = properties
865                    .attr(&Ns::Presentation, "background-visible")
866                    .and_then(crate::value::boolean)
867                {
868                    self.background_visible = Some(visible);
869                }
870                if let Some(visible) = properties
871                    .attr(&Ns::Presentation, "background-objects-visible")
872                    .and_then(crate::value::boolean)
873                {
874                    self.background_objects_visible = Some(visible);
875                }
876            } else if properties.is(&Ns::Style, "table-column-properties") {
877                if let Some(width) = properties
878                    .attr(&Ns::Style, "column-width")
879                    .and_then(Length::parse)
880                {
881                    self.column_width = Some(width);
882                }
883            } else if properties.is(&Ns::Style, "table-row-properties")
884                && let Some(height) = properties
885                    .attr(&Ns::Style, "row-height")
886                    .and_then(Length::parse)
887            {
888                self.row_height = Some(height);
889            }
890        }
891    }
892}
893
894impl TextProperties {
895    fn apply(&mut self, p: &Element, font_faces: &HashMap<String, String>) {
896        // `style:font-name` points at a font face declaration and `fo:font-family`
897        // names a family directly. A style may carry both, and the declaration is
898        // the more specific of the two.
899        if let Some(name) = p.attr(&Ns::Fo, "font-family") {
900            self.font_family = Some(name.trim_matches('\'').to_owned());
901        }
902        if let Some(name) = p.attr(&Ns::Style, "font-name") {
903            self.font_family = Some(
904                font_faces
905                    .get(name)
906                    .cloned()
907                    .unwrap_or_else(|| name.to_owned()),
908            );
909        }
910        if let Some(size) = p.attr(&Ns::Fo, "font-size").and_then(Measure::parse) {
911            self.size = Some(size);
912        }
913        if let Some(weight) = p.attr(&Ns::Fo, "font-weight") {
914            // A numeric weight is the CSS scale, where 600 and above reads as
915            // bold to anything that has two faces to choose between.
916            self.bold = Some(match weight {
917                "normal" => false,
918                "bold" => true,
919                other => other.parse::<u32>().is_ok_and(|n| n >= 600),
920            });
921        }
922        if let Some(style) = p.attr(&Ns::Fo, "font-style") {
923            self.italic = Some(style != "normal");
924        }
925        if let Some(line) = p.attr(&Ns::Style, "text-underline-style") {
926            self.underline = Some(line != "none");
927        }
928        if let Some(line) = p.attr(&Ns::Style, "text-line-through-style") {
929            self.strike = Some(line != "none");
930        }
931        if let Some(color) = p.attr(&Ns::Fo, "color") {
932            self.color = Color::parse(color);
933        }
934        if let Some(color) = p.attr(&Ns::Fo, "background-color") {
935            self.background = Color::parse(color);
936        }
937        if let Some(position) = p.attr(&Ns::Style, "text-position") {
938            // The attribute is a vertical offset and optionally a size, as in
939            // `super 58%` or `-33% 58%`. Only the direction is read: a renderer
940            // that placed the glyph at the stated offset and scaled it by the
941            // stated amount would be doing typesetting, and what is wanted here
942            // is the distinction between superscript and subscript.
943            let first = position.split_whitespace().next().unwrap_or_default();
944            self.position = Some(match first {
945                "super" => Position::Super,
946                "sub" => Position::Sub,
947                _ => match Percent::parse(first) {
948                    Some(percent) if percent.0 > 0.0 => Position::Super,
949                    Some(percent) if percent.0 < 0.0 => Position::Sub,
950                    _ => Position::Baseline,
951                },
952            });
953        }
954        if let Some(transform) = p.attr(&Ns::Fo, "text-transform") {
955            self.uppercase = Some(transform == "uppercase");
956        }
957    }
958}
959
960impl ParagraphProperties {
961    fn apply(&mut self, p: &Element) {
962        if let Some(align) = p.attr(&Ns::Fo, "text-align") {
963            self.align = match align {
964                // `left` and `right` are the writing-direction-independent
965                // spellings' siblings, and for a left-to-right document they are
966                // the same thing. A right-to-left document would need the
967                // direction to tell them apart, which is what §6 of DESIGN.md
968                // says this release does not do.
969                "start" | "left" => Some(TextAlign::Start),
970                "end" | "right" => Some(TextAlign::End),
971                "center" => Some(TextAlign::Center),
972                "justify" => Some(TextAlign::Justify),
973                _ => self.align,
974            };
975        }
976        read_edges(p, "margin", &mut self.margin, Length::parse);
977        read_edges(p, "padding", &mut self.padding, Length::parse);
978        read_edges(p, "border", &mut self.border, Border::parse);
979        if let Some(indent) = p.attr(&Ns::Fo, "text-indent").and_then(Length::parse) {
980            self.text_indent = Some(indent);
981        }
982        if let Some(height) = p.attr(&Ns::Fo, "line-height") {
983            self.line_height = Measure::parse(height);
984        }
985        if let Some(color) = p.attr(&Ns::Fo, "background-color") {
986            self.background = Color::parse(color);
987        }
988        if let Some(before) = p.attr(&Ns::Fo, "break-before") {
989            self.break_before = Some(parse_break(before));
990        }
991        if let Some(after) = p.attr(&Ns::Fo, "break-after") {
992            self.break_after = Some(parse_break(after));
993        }
994    }
995}
996
997fn parse_break(text: &str) -> Break {
998    match text {
999        "page" => Break::Page,
1000        "column" => Break::Column,
1001        _ => Break::Auto,
1002    }
1003}
1004
1005impl CellProperties {
1006    fn apply(&mut self, p: &Element) {
1007        if let Some(color) = p.attr(&Ns::Fo, "background-color") {
1008            self.background = Color::parse(color);
1009        }
1010        if let Some(align) = p.attr(&Ns::Style, "vertical-align") {
1011            self.vertical_align = match align {
1012                "top" => Some(VerticalAlign::Top),
1013                "middle" => Some(VerticalAlign::Middle),
1014                // `bottom`, and `automatic`, which is the fourth value ODF
1015                // defines and means bottom for a cell: it is what a spreadsheet
1016                // shows for a cell nobody has set.
1017                _ => Some(VerticalAlign::Bottom),
1018            };
1019        }
1020        read_edges(p, "border", &mut self.border, Border::parse);
1021        read_edges(p, "padding", &mut self.padding, Length::parse);
1022        if let Some(wrap) = p.attr(&Ns::Fo, "wrap-option") {
1023            self.wrap = Some(wrap == "wrap");
1024        }
1025    }
1026}
1027
1028impl GraphicProperties {
1029    fn apply(&mut self, p: &Element) {
1030        // Each of these is its own property and each inherits on its own. A
1031        // style that changes only the shade of an already-solid shape writes the
1032        // colour and nothing else; one that turns the fill off writes the kind
1033        // and nothing else.
1034        if let Some(kind) = p.attr(&Ns::Draw, "fill") {
1035            self.kind = Some(match kind {
1036                "solid" => FillKind::Solid,
1037                "gradient" => FillKind::Gradient,
1038                "bitmap" => FillKind::Bitmap,
1039                "hatch" => FillKind::Hatch,
1040                _ => FillKind::None,
1041            });
1042        }
1043        if let Some(color) = p.attr(&Ns::Draw, "fill-color").and_then(Color::parse) {
1044            self.color = Some(color);
1045        }
1046        if let Some(name) = p.attr(&Ns::Draw, "fill-gradient-name") {
1047            self.gradient = Some(name.to_owned());
1048        }
1049        // Only a stretched picture: see `Fill::Image`. A tiled one leaves the
1050        // name unset, so the fill resolves to nothing rather than to one tile
1051        // blown up to the size of the shape.
1052        if let Some(name) = p.attr(&Ns::Draw, "fill-image-name") {
1053            self.image = (p.attr(&Ns::Style, "repeat").unwrap_or("stretch") == "stretch")
1054                .then(|| name.to_owned());
1055        }
1056
1057        match p.attr(&Ns::Draw, "stroke") {
1058            Some("none") => self.stroke = None,
1059            _ => {
1060                if let Some(color) = p.attr(&Ns::Svg, "stroke-color").and_then(Color::parse) {
1061                    self.stroke = Some(color);
1062                }
1063            }
1064        }
1065        if let Some(width) = p.attr(&Ns::Svg, "stroke-width").and_then(Length::parse) {
1066            self.stroke_width = Some(width);
1067        }
1068        if let Some(opacity) = p.attr(&Ns::Draw, "opacity").and_then(Percent::parse) {
1069            self.opacity = Some(opacity.fraction().clamp(0.0, 1.0));
1070        }
1071        // `justify` spreads the lines to fill the height, which needs a line
1072        // box this renderer does not build; it reads as the top, which is where
1073        // the first line goes either way.
1074        if let Some(anchor) = p.attr(&Ns::Draw, "textarea-vertical-align") {
1075            self.text_anchor = Some(match anchor {
1076                "middle" => Anchor::Middle,
1077                "bottom" => Anchor::Bottom,
1078                _ => Anchor::Top,
1079            });
1080        }
1081    }
1082}