use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::value::{Color, Length, Measure, Percent};
use crate::xml::{Element, Ns};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Family {
Paragraph,
Text,
Section,
Table,
TableColumn,
TableRow,
TableCell,
Graphic,
Presentation,
DrawingPage,
Chart,
Ruby,
Other(Box<str>),
}
impl Family {
pub fn parse(text: &str) -> Self {
match text {
"paragraph" => Self::Paragraph,
"text" => Self::Text,
"section" => Self::Section,
"table" => Self::Table,
"table-column" => Self::TableColumn,
"table-row" => Self::TableRow,
"table-cell" => Self::TableCell,
"graphic" => Self::Graphic,
"presentation" => Self::Presentation,
"drawing-page" => Self::DrawingPage,
"chart" => Self::Chart,
"ruby" => Self::Ruby,
other => Self::Other(other.into()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlign {
Start,
End,
Center,
Justify,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Position {
Baseline,
Super,
Sub,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerticalAlign {
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Break {
Auto,
Page,
Column,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Border {
pub width: Length,
pub color: Color,
}
impl Border {
fn parse(text: &str) -> Option<Self> {
let mut width = None;
let mut color = None;
for word in text.split_whitespace() {
match word {
"none" | "hidden" => return None,
_ => {
if let Some(length) = Length::parse(word) {
width = Some(length);
} else if let Some(parsed) = Color::parse(word) {
color = Some(parsed);
}
}
}
}
Some(Self {
width: width.unwrap_or(Length(0.5)),
color: color.unwrap_or(Color { r: 0, g: 0, b: 0 }),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Edges<T> {
pub left: Option<T>,
pub right: Option<T>,
pub top: Option<T>,
pub bottom: Option<T>,
}
impl<T> Default for Edges<T> {
fn default() -> Self {
Self {
left: None,
right: None,
top: None,
bottom: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TextProperties {
pub font_family: Option<String>,
pub size: Option<Measure>,
pub bold: Option<bool>,
pub italic: Option<bool>,
pub underline: Option<bool>,
pub strike: Option<bool>,
pub color: Option<Color>,
pub background: Option<Color>,
pub position: Option<Position>,
pub uppercase: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ParagraphProperties {
pub align: Option<TextAlign>,
pub margin: Edges<Length>,
pub text_indent: Option<Length>,
pub line_height: Option<Measure>,
pub background: Option<Color>,
pub break_before: Option<Break>,
pub break_after: Option<Break>,
pub border: Edges<Border>,
pub padding: Edges<Length>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CellProperties {
pub background: Option<Color>,
pub vertical_align: Option<VerticalAlign>,
pub border: Edges<Border>,
pub padding: Edges<Length>,
pub wrap: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub enum Fill {
#[default]
None,
Solid(Color),
Gradient(String),
Image(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GradientStyle {
Linear,
Axial,
Radial,
Ellipsoidal,
Square,
Rectangular,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Gradient {
pub style: GradientStyle,
pub start: Color,
pub end: Color,
pub angle: f32,
pub border: f32,
pub center: (f32, f32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FillKind {
None,
Solid,
Gradient,
Bitmap,
Hatch,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct GraphicProperties {
kind: Option<FillKind>,
color: Option<Color>,
gradient: Option<String>,
image: Option<String>,
pub stroke: Option<Color>,
pub stroke_width: Option<Length>,
pub opacity: Option<f32>,
}
impl GraphicProperties {
pub fn fill(&self) -> Fill {
match self.kind {
None | Some(FillKind::None | FillKind::Hatch) => Fill::None,
Some(FillKind::Solid) => self.color.map_or(Fill::None, Fill::Solid),
Some(FillKind::Gradient) => self.gradient.clone().map_or(Fill::None, Fill::Gradient),
Some(FillKind::Bitmap) => self.image.clone().map_or(Fill::None, Fill::Image),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Properties {
pub text: TextProperties,
pub paragraph: ParagraphProperties,
pub cell: CellProperties,
pub graphic: GraphicProperties,
pub background_visible: Option<bool>,
pub background_objects_visible: Option<bool>,
pub column_width: Option<Length>,
pub row_height: Option<Length>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PageLayout {
pub width: Length,
pub height: Length,
pub margin: Edges<Length>,
}
impl Default for PageLayout {
fn default() -> Self {
Self {
width: Length(612.0),
height: Length(792.0),
margin: Edges {
left: Some(Length(72.0)),
right: Some(Length(72.0)),
top: Some(Length(72.0)),
bottom: Some(Length(72.0)),
},
}
}
}
pub struct Style {
pub name: String,
pub display_name: Option<String>,
pub family: Family,
pub parent: Option<String>,
pub element: Element,
}
pub struct Styles {
by_name: HashMap<(Family, String), Style>,
defaults: HashMap<Family, Element>,
page_layouts: HashMap<String, PageLayout>,
master_pages: HashMap<String, Element>,
lists: HashMap<String, Element>,
gradients: HashMap<String, Gradient>,
fill_images: HashMap<String, String>,
font_faces: HashMap<String, String>,
cache: RefCell<HashMap<(Family, String), Rc<Properties>>>,
}
impl Styles {
pub fn collect(content: Option<&Element>, styles: Option<&Element>) -> Self {
let mut this = Self {
by_name: HashMap::new(),
defaults: HashMap::new(),
page_layouts: HashMap::new(),
master_pages: HashMap::new(),
lists: HashMap::new(),
gradients: HashMap::new(),
fill_images: HashMap::new(),
font_faces: HashMap::new(),
cache: RefCell::new(HashMap::new()),
};
for root in [styles, content].into_iter().flatten() {
this.collect_from(root);
}
this
}
fn collect_from(&mut self, root: &Element) {
for container in root.elements() {
match () {
() if container.is(&Ns::Office, "font-face-decls") => {
for face in container.elements() {
let Some(name) = face.attr(&Ns::Style, "name") else {
continue;
};
let family = face
.attr(&Ns::Svg, "font-family")
.unwrap_or(name)
.trim_matches('\'')
.to_owned();
self.font_faces.insert(name.to_owned(), family);
}
}
() if container.is(&Ns::Office, "styles")
|| container.is(&Ns::Office, "automatic-styles") =>
{
for element in container.elements() {
self.collect_style(element);
}
}
() if container.is(&Ns::Office, "master-styles") => {
for master in container.elements() {
if let Some(name) = master.attr(&Ns::Style, "name") {
self.master_pages.insert(name.to_owned(), master.clone());
}
}
}
() => {}
}
}
}
fn collect_style(&mut self, element: &Element) {
if element.is(&Ns::Style, "style") {
let Some(name) = element.attr(&Ns::Style, "name") else {
return;
};
let family = Family::parse(element.attr(&Ns::Style, "family").unwrap_or_default());
let style = Style {
name: name.to_owned(),
display_name: element
.attr(&Ns::Style, "display-name")
.map(ToOwned::to_owned),
family: family.clone(),
parent: element
.attr(&Ns::Style, "parent-style-name")
.map(ToOwned::to_owned),
element: element.clone(),
};
self.by_name.insert((family, name.to_owned()), style);
} else if element.is(&Ns::Style, "default-style") {
let family = Family::parse(element.attr(&Ns::Style, "family").unwrap_or_default());
self.defaults.insert(family, element.clone());
} else if element.is(&Ns::Style, "page-layout") {
if let Some(name) = element.attr(&Ns::Style, "name") {
self.page_layouts
.insert(name.to_owned(), page_layout(element));
}
} else if element.is(&Ns::Draw, "fill-image") {
if let (Some(name), Some(href)) = (
element.attr(&Ns::Draw, "name"),
element.attr(&Ns::Xlink, "href"),
) {
self.fill_images.insert(name.to_owned(), href.to_owned());
}
} else if element.is(&Ns::Draw, "gradient") {
if let Some(name) = element.attr(&Ns::Draw, "name") {
self.gradients.insert(name.to_owned(), gradient(element));
}
} else if element.is(&Ns::Text, "list-style")
&& let Some(name) = element.attr(&Ns::Style, "name")
{
self.lists.insert(name.to_owned(), element.clone());
}
}
pub fn style(&self, family: &Family, name: &str) -> Option<&Style> {
self.by_name.get(&(family.clone(), name.to_owned()))
}
pub fn gradient(&self, name: &str) -> Option<&Gradient> {
self.gradients.get(name)
}
pub fn fill_image(&self, name: &str) -> Option<&str> {
self.fill_images.get(name).map(String::as_str)
}
pub fn list_style(&self, name: &str) -> Option<&Element> {
self.lists.get(name)
}
pub fn master_page(&self, name: &str) -> Option<&Element> {
self.master_pages.get(name)
}
pub fn page_layout_of(&self, master_page: &str) -> Option<&PageLayout> {
let master = self.master_pages.get(master_page)?;
let layout = master.attr(&Ns::Style, "page-layout-name")?;
self.page_layouts.get(layout)
}
pub fn resolve(&self, family: &Family, name: &str) -> Rc<Properties> {
let key = (family.clone(), name.to_owned());
if let Some(cached) = self.cache.borrow().get(&key) {
return Rc::clone(cached);
}
let mut properties = Properties::default();
if let Some(default) = self.defaults.get(family) {
properties.apply(default, &self.font_faces);
}
for style in self.chain(family, name).into_iter().rev() {
properties.apply(&style.element, &self.font_faces);
}
let properties = Rc::new(properties);
self.cache.borrow_mut().insert(key, Rc::clone(&properties));
properties
}
fn chain(&self, family: &Family, name: &str) -> Vec<&Style> {
let mut chain: Vec<&Style> = Vec::new();
let mut next: Option<&str> = Some(name);
while let Some(current) = next {
if chain.iter().any(|s| s.name == current) {
break;
}
let Some(style) = self.style(family, current) else {
break;
};
next = style.parent.as_deref();
chain.push(style);
}
chain
}
pub fn font_family(&self, declared: &str) -> String {
self.font_faces
.get(declared)
.cloned()
.unwrap_or_else(|| declared.to_owned())
}
}
fn gradient(element: &Element) -> Gradient {
let color = |local: &str, fallback: Color| {
element
.attr(&Ns::Draw, local)
.and_then(Color::parse)
.unwrap_or(fallback)
};
let proportion = |local: &str| {
element
.attr(&Ns::Draw, local)
.and_then(Percent::parse)
.map_or(0.0, Percent::fraction)
};
Gradient {
style: match element.attr(&Ns::Draw, "style") {
Some("axial") => GradientStyle::Axial,
Some("radial") => GradientStyle::Radial,
Some("ellipsoid") => GradientStyle::Ellipsoidal,
Some("square") => GradientStyle::Square,
Some("rectangular") => GradientStyle::Rectangular,
_ => GradientStyle::Linear,
},
start: color("start-color", Color { r: 0, g: 0, b: 0 }),
end: color(
"end-color",
Color {
r: 0xff,
g: 0xff,
b: 0xff,
},
),
angle: element.attr(&Ns::Draw, "angle").map_or(0.0, parse_angle),
border: proportion("border"),
center: (proportion("cx"), proportion("cy")),
}
}
fn parse_angle(text: &str) -> f32 {
let text = text.trim();
match text.strip_suffix("deg") {
Some(degrees) => degrees.trim().parse().unwrap_or(0.0),
None => text.parse::<f32>().unwrap_or(0.0) / 10.0,
}
}
fn page_layout(element: &Element) -> PageLayout {
let mut layout = PageLayout::default();
if let Some(properties) = element.child(&Ns::Style, "page-layout-properties") {
if let Some(width) = properties
.attr(&Ns::Fo, "page-width")
.and_then(Length::parse)
{
layout.width = width;
}
if let Some(height) = properties
.attr(&Ns::Fo, "page-height")
.and_then(Length::parse)
{
layout.height = height;
}
let mut margin = Edges::default();
read_edges(properties, "margin", &mut margin, Length::parse);
layout.margin.left = margin.left.or(layout.margin.left);
layout.margin.right = margin.right.or(layout.margin.right);
layout.margin.top = margin.top.or(layout.margin.top);
layout.margin.bottom = margin.bottom.or(layout.margin.bottom);
}
layout
}
fn read_edges<T: Copy>(
properties: &Element,
base: &str,
into: &mut Edges<T>,
parse: impl Fn(&str) -> Option<T>,
) {
if let Some(all) = properties.attr(&Ns::Fo, base).and_then(&parse) {
*into = Edges {
left: Some(all),
right: Some(all),
top: Some(all),
bottom: Some(all),
};
}
if let Some(v) = properties
.attr(&Ns::Fo, &format!("{base}-left"))
.and_then(&parse)
{
into.left = Some(v);
}
if let Some(v) = properties
.attr(&Ns::Fo, &format!("{base}-right"))
.and_then(&parse)
{
into.right = Some(v);
}
if let Some(v) = properties
.attr(&Ns::Fo, &format!("{base}-top"))
.and_then(&parse)
{
into.top = Some(v);
}
if let Some(v) = properties
.attr(&Ns::Fo, &format!("{base}-bottom"))
.and_then(&parse)
{
into.bottom = Some(v);
}
}
impl Properties {
fn apply(&mut self, style: &Element, font_faces: &HashMap<String, String>) {
for properties in style.elements() {
if properties.is(&Ns::Style, "text-properties") {
self.text.apply(properties, font_faces);
} else if properties.is(&Ns::Style, "paragraph-properties") {
self.paragraph.apply(properties);
} else if properties.is(&Ns::Style, "table-cell-properties") {
self.cell.apply(properties);
} else if properties.is(&Ns::Style, "graphic-properties")
|| properties.is(&Ns::Style, "drawing-page-properties")
{
self.graphic.apply(properties);
if let Some(visible) = properties
.attr(&Ns::Presentation, "background-visible")
.and_then(crate::value::boolean)
{
self.background_visible = Some(visible);
}
if let Some(visible) = properties
.attr(&Ns::Presentation, "background-objects-visible")
.and_then(crate::value::boolean)
{
self.background_objects_visible = Some(visible);
}
} else if properties.is(&Ns::Style, "table-column-properties") {
if let Some(width) = properties
.attr(&Ns::Style, "column-width")
.and_then(Length::parse)
{
self.column_width = Some(width);
}
} else if properties.is(&Ns::Style, "table-row-properties")
&& let Some(height) = properties
.attr(&Ns::Style, "row-height")
.and_then(Length::parse)
{
self.row_height = Some(height);
}
}
}
}
impl TextProperties {
fn apply(&mut self, p: &Element, font_faces: &HashMap<String, String>) {
if let Some(name) = p.attr(&Ns::Fo, "font-family") {
self.font_family = Some(name.trim_matches('\'').to_owned());
}
if let Some(name) = p.attr(&Ns::Style, "font-name") {
self.font_family = Some(
font_faces
.get(name)
.cloned()
.unwrap_or_else(|| name.to_owned()),
);
}
if let Some(size) = p.attr(&Ns::Fo, "font-size").and_then(Measure::parse) {
self.size = Some(size);
}
if let Some(weight) = p.attr(&Ns::Fo, "font-weight") {
self.bold = Some(match weight {
"normal" => false,
"bold" => true,
other => other.parse::<u32>().is_ok_and(|n| n >= 600),
});
}
if let Some(style) = p.attr(&Ns::Fo, "font-style") {
self.italic = Some(style != "normal");
}
if let Some(line) = p.attr(&Ns::Style, "text-underline-style") {
self.underline = Some(line != "none");
}
if let Some(line) = p.attr(&Ns::Style, "text-line-through-style") {
self.strike = Some(line != "none");
}
if let Some(color) = p.attr(&Ns::Fo, "color") {
self.color = Color::parse(color);
}
if let Some(color) = p.attr(&Ns::Fo, "background-color") {
self.background = Color::parse(color);
}
if let Some(position) = p.attr(&Ns::Style, "text-position") {
let first = position.split_whitespace().next().unwrap_or_default();
self.position = Some(match first {
"super" => Position::Super,
"sub" => Position::Sub,
_ => match Percent::parse(first) {
Some(percent) if percent.0 > 0.0 => Position::Super,
Some(percent) if percent.0 < 0.0 => Position::Sub,
_ => Position::Baseline,
},
});
}
if let Some(transform) = p.attr(&Ns::Fo, "text-transform") {
self.uppercase = Some(transform == "uppercase");
}
}
}
impl ParagraphProperties {
fn apply(&mut self, p: &Element) {
if let Some(align) = p.attr(&Ns::Fo, "text-align") {
self.align = match align {
"start" | "left" => Some(TextAlign::Start),
"end" | "right" => Some(TextAlign::End),
"center" => Some(TextAlign::Center),
"justify" => Some(TextAlign::Justify),
_ => self.align,
};
}
read_edges(p, "margin", &mut self.margin, Length::parse);
read_edges(p, "padding", &mut self.padding, Length::parse);
read_edges(p, "border", &mut self.border, Border::parse);
if let Some(indent) = p.attr(&Ns::Fo, "text-indent").and_then(Length::parse) {
self.text_indent = Some(indent);
}
if let Some(height) = p.attr(&Ns::Fo, "line-height") {
self.line_height = Measure::parse(height);
}
if let Some(color) = p.attr(&Ns::Fo, "background-color") {
self.background = Color::parse(color);
}
if let Some(before) = p.attr(&Ns::Fo, "break-before") {
self.break_before = Some(parse_break(before));
}
if let Some(after) = p.attr(&Ns::Fo, "break-after") {
self.break_after = Some(parse_break(after));
}
}
}
fn parse_break(text: &str) -> Break {
match text {
"page" => Break::Page,
"column" => Break::Column,
_ => Break::Auto,
}
}
impl CellProperties {
fn apply(&mut self, p: &Element) {
if let Some(color) = p.attr(&Ns::Fo, "background-color") {
self.background = Color::parse(color);
}
if let Some(align) = p.attr(&Ns::Style, "vertical-align") {
self.vertical_align = match align {
"top" => Some(VerticalAlign::Top),
"middle" => Some(VerticalAlign::Middle),
_ => Some(VerticalAlign::Bottom),
};
}
read_edges(p, "border", &mut self.border, Border::parse);
read_edges(p, "padding", &mut self.padding, Length::parse);
if let Some(wrap) = p.attr(&Ns::Fo, "wrap-option") {
self.wrap = Some(wrap == "wrap");
}
}
}
impl GraphicProperties {
fn apply(&mut self, p: &Element) {
if let Some(kind) = p.attr(&Ns::Draw, "fill") {
self.kind = Some(match kind {
"solid" => FillKind::Solid,
"gradient" => FillKind::Gradient,
"bitmap" => FillKind::Bitmap,
"hatch" => FillKind::Hatch,
_ => FillKind::None,
});
}
if let Some(color) = p.attr(&Ns::Draw, "fill-color").and_then(Color::parse) {
self.color = Some(color);
}
if let Some(name) = p.attr(&Ns::Draw, "fill-gradient-name") {
self.gradient = Some(name.to_owned());
}
if let Some(name) = p.attr(&Ns::Draw, "fill-image-name") {
self.image = (p.attr(&Ns::Style, "repeat").unwrap_or("stretch") == "stretch")
.then(|| name.to_owned());
}
match p.attr(&Ns::Draw, "stroke") {
Some("none") => self.stroke = None,
_ => {
if let Some(color) = p.attr(&Ns::Svg, "stroke-color").and_then(Color::parse) {
self.stroke = Some(color);
}
}
}
if let Some(width) = p.attr(&Ns::Svg, "stroke-width").and_then(Length::parse) {
self.stroke_width = Some(width);
}
if let Some(opacity) = p.attr(&Ns::Draw, "opacity").and_then(Percent::parse) {
self.opacity = Some(opacity.fraction().clamp(0.0, 1.0));
}
}
}