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