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}