#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RgbColor {
pub red: u8,
pub green: u8,
pub blue: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RgbaColor {
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: u8, }
impl RgbColor {
pub fn new(red: u8, green: u8, blue: u8) -> Self {
Self { red, green, blue }
}
pub fn from_hex(hex: &str) -> Result<Self, String> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err("Hex color must be 6 characters long".to_string());
}
let red = u8
::from_str_radix(&hex[0..2], 16)
.map_err(|_| "Invalid red component".to_string())?;
let green = u8
::from_str_radix(&hex[2..4], 16)
.map_err(|_| "Invalid green component".to_string())?;
let blue = u8
::from_str_radix(&hex[4..6], 16)
.map_err(|_| "Invalid blue component".to_string())?;
Ok(Self::new(red, green, blue))
}
pub fn to_hex(&self) -> String {
format!("{:02X}{:02X}{:02X}", self.red, self.green, self.blue)
}
pub fn to_rgba(&self) -> RgbaColor {
RgbaColor::new(self.red, self.green, self.blue, 255)
}
pub const BLACK: Self = Self {
red: 0,
green: 0,
blue: 0,
};
pub const WHITE: Self = Self {
red: 255,
green: 255,
blue: 255,
};
pub const RED: Self = Self {
red: 255,
green: 0,
blue: 0,
};
pub const GREEN: Self = Self {
red: 0,
green: 255,
blue: 0,
};
pub const BLUE: Self = Self {
red: 0,
green: 0,
blue: 255,
};
}
impl RgbaColor {
pub fn new(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
Self {
red,
green,
blue,
alpha,
}
}
pub fn from_hex(hex: &str) -> Result<Self, String> {
let hex = hex.trim_start_matches('#');
if hex.len() != 8 {
return Err("Hex RGBA color must be 8 characters long".to_string());
}
let red = u8
::from_str_radix(&hex[0..2], 16)
.map_err(|_| "Invalid red component".to_string())?;
let green = u8
::from_str_radix(&hex[2..4], 16)
.map_err(|_| "Invalid green component".to_string())?;
let blue = u8
::from_str_radix(&hex[4..6], 16)
.map_err(|_| "Invalid blue component".to_string())?;
let alpha = u8
::from_str_radix(&hex[6..8], 16)
.map_err(|_| "Invalid alpha component".to_string())?;
Ok(Self::new(red, green, blue, alpha))
}
pub fn to_hex(&self) -> String {
format!("{:02X}{:02X}{:02X}{:02X}", self.red, self.green, self.blue, self.alpha)
}
pub fn alpha_percent(&self) -> f32 {
(self.alpha as f32) / 255.0
}
pub fn with_alpha_percent(mut self, alpha: f32) -> Self {
self.alpha = (alpha.clamp(0.0, 1.0) * 255.0) as u8;
self
}
pub fn to_rgb(&self) -> RgbColor {
RgbColor::new(self.red, self.green, self.blue)
}
pub const TRANSPARENT: Self = Self {
red: 0,
green: 0,
blue: 0,
alpha: 0,
};
pub const BLACK: Self = Self {
red: 0,
green: 0,
blue: 0,
alpha: 255,
};
pub const WHITE: Self = Self {
red: 255,
green: 255,
blue: 255,
alpha: 255,
};
pub const RED: Self = Self {
red: 255,
green: 0,
blue: 0,
alpha: 255,
};
pub const GREEN: Self = Self {
red: 0,
green: 255,
blue: 0,
alpha: 255,
};
pub const BLUE: Self = Self {
red: 0,
green: 0,
blue: 255,
alpha: 255,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ThemeColor {
Dark1,
Light1,
Dark2,
Light2,
Accent1,
Accent2,
Accent3,
Accent4,
Accent5,
Accent6,
Hyperlink,
FollowedHyperlink,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Color {
Rgb(RgbColor),
Rgba(RgbaColor),
Theme(ThemeColor),
Indexed(u8),
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct FontSize(pub f32);
impl FontSize {
pub fn new(size: f32) -> Self {
Self(size.max(0.0))
}
pub fn value(&self) -> f32 {
self.0
}
pub const SMALL: Self = Self(8.0);
pub const NORMAL: Self = Self(11.0);
pub const MEDIUM: Self = Self(12.0);
pub const LARGE: Self = Self(14.0);
pub const EXTRA_LARGE: Self = Self(18.0);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnderlineStyle {
None,
Single,
Double,
Thick,
Dotted,
DottedHeavy,
Dashed,
DashedHeavy,
DashLong,
DashLongHeavy,
DotDash,
DotDashHeavy,
DotDotDash,
DotDotDashHeavy,
Wave,
WaveHeavy,
WaveDouble,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FontStyle {
pub bold: bool,
pub italic: bool,
pub underline: UnderlineStyle,
pub strikethrough: bool,
pub superscript: bool,
pub subscript: bool,
pub small_caps: bool,
pub all_caps: bool,
}
impl Default for FontStyle {
fn default() -> Self {
Self {
bold: false,
italic: false,
underline: UnderlineStyle::None,
strikethrough: false,
superscript: false,
subscript: false,
small_caps: false,
all_caps: false,
}
}
}
impl FontStyle {
pub fn bold() -> Self {
Self {
bold: true,
italic: false,
underline: UnderlineStyle::None,
strikethrough: false,
superscript: false,
subscript: false,
small_caps: false,
all_caps: false,
}
}
pub fn italic() -> Self {
Self {
bold: false,
italic: true,
underline: UnderlineStyle::None,
strikethrough: false,
superscript: false,
subscript: false,
small_caps: false,
all_caps: false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Font {
pub name: String,
pub size: FontSize,
pub style: FontStyle,
pub color: Color,
}
impl Default for Font {
fn default() -> Self {
Self {
name: "Calibri".to_string(),
size: FontSize::NORMAL,
style: FontStyle::default(),
color: Color::Auto,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LengthUnit {
Points(f32),
Pixels(f32),
Inches(f32),
Centimeters(f32),
Millimeters(f32),
Emu(i32),
}
impl LengthUnit {
pub fn to_points(&self) -> f32 {
match self {
LengthUnit::Points(p) => *p,
LengthUnit::Pixels(px) => *px * 0.75, LengthUnit::Inches(inch) => *inch * 72.0,
LengthUnit::Centimeters(cm) => *cm * 28.35,
LengthUnit::Millimeters(mm) => *mm * 2.835,
LengthUnit::Emu(emu) => (*emu as f32) / 12700.0,
}
}
pub fn to_emu(&self) -> i32 {
(self.to_points() * 12700.0) as i32
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Margin {
pub top: LengthUnit,
pub right: LengthUnit,
pub bottom: LengthUnit,
pub left: LengthUnit,
}
impl Margin {
pub fn uniform(margin: LengthUnit) -> Self {
Self {
top: margin,
right: margin,
bottom: margin,
left: margin,
}
}
pub fn zero() -> Self {
Self::uniform(LengthUnit::Points(0.0))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HorizontalAlignment {
Left,
Center,
Right,
Justify,
Distributed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VerticalAlignment {
Top,
Middle,
Bottom,
Justify,
Distributed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BorderStyle {
None,
Thin,
Medium,
Thick,
Double,
Dotted,
Dashed,
DashDot,
DashDotDot,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Border {
pub style: BorderStyle,
pub color: Color,
pub width: LengthUnit,
}
impl Default for Border {
fn default() -> Self {
Self {
style: BorderStyle::None,
color: Color::Auto,
width: LengthUnit::Points(0.0),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Borders {
pub top: Border,
pub right: Border,
pub bottom: Border,
pub left: Border,
}
impl Default for Borders {
fn default() -> Self {
Self {
top: Border::default(),
right: Border::default(),
bottom: Border::default(),
left: Border::default(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FillPattern {
None,
Solid(Color),
Gradient {
start_color: Color,
end_color: Color,
angle: f32, },
Pattern {
pattern_type: PatternType,
foreground_color: Color,
background_color: Color,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PatternType {
Solid,
Gray75,
Gray50,
Gray25,
Gray125,
Gray0625,
HorizontalStripe,
VerticalStripe,
ReverseDiagonalStripe,
DiagonalStripe,
DiagonalCrosshatch,
ThickDiagonalCrosshatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PageOrientation {
Portrait,
Landscape,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PageSize {
pub width: LengthUnit,
pub height: LengthUnit,
pub orientation: PageOrientation,
}
impl PageSize {
pub fn a4() -> Self {
Self {
width: LengthUnit::Millimeters(210.0),
height: LengthUnit::Millimeters(297.0),
orientation: PageOrientation::Portrait,
}
}
pub fn letter() -> Self {
Self {
width: LengthUnit::Inches(8.5),
height: LengthUnit::Inches(11.0),
orientation: PageOrientation::Portrait,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DocumentProperties {
pub title: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub keywords: Option<String>,
pub description: Option<String>,
pub category: Option<String>,
pub created: Option<chrono::DateTime<chrono::Utc>>,
pub modified: Option<chrono::DateTime<chrono::Utc>>,
}
impl Default for DocumentProperties {
fn default() -> Self {
Self {
title: None,
author: None,
subject: None,
keywords: None,
description: None,
category: None,
created: None,
modified: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: LengthUnit,
pub y: LengthUnit,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Size {
pub width: LengthUnit,
pub height: LengthUnit,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rectangle {
pub position: Point,
pub size: Size,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ListType {
Bullet,
Number,
LowerAlpha,
UpperAlpha,
LowerRoman,
UpperRoman,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ListStyle {
pub list_type: ListType,
pub level: u8,
pub indent: LengthUnit,
pub bullet_char: Option<char>,
pub number_format: Option<String>,
}
impl Default for ListStyle {
fn default() -> Self {
Self {
list_type: ListType::Bullet,
level: 0,
indent: LengthUnit::Points(18.0),
bullet_char: Some('•'),
number_format: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableBorderStyle {
pub outer: Borders,
pub inner_horizontal: Border,
pub inner_vertical: Border,
}
impl Default for TableBorderStyle {
fn default() -> Self {
Self {
outer: Borders::default(),
inner_horizontal: Border::default(),
inner_vertical: Border::default(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableStyle {
pub borders: TableBorderStyle,
pub cell_padding: Margin,
pub cell_spacing: LengthUnit,
pub background_color: Option<Color>,
pub stripe_rows: bool,
pub stripe_columns: bool,
}
impl Default for TableStyle {
fn default() -> Self {
Self {
borders: TableBorderStyle::default(),
cell_padding: Margin::uniform(LengthUnit::Points(2.0)),
cell_spacing: LengthUnit::Points(0.0),
background_color: None,
stripe_rows: false,
stripe_columns: false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextDecoration {
pub shadow: bool,
pub emboss: bool,
pub imprint: bool,
pub outline: bool,
pub glow: Option<Color>,
pub reflection: bool,
}
impl Default for TextDecoration {
fn default() -> Self {
Self {
shadow: false,
emboss: false,
imprint: false,
outline: false,
glow: None,
reflection: false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParagraphStyle {
pub alignment: HorizontalAlignment,
pub line_spacing: LineSpacing,
pub space_before: LengthUnit,
pub space_after: LengthUnit,
pub first_line_indent: LengthUnit,
pub left_indent: LengthUnit,
pub right_indent: LengthUnit,
pub keep_together: bool,
pub keep_with_next: bool,
pub page_break_before: bool,
}
impl Default for ParagraphStyle {
fn default() -> Self {
Self {
alignment: HorizontalAlignment::Left,
line_spacing: LineSpacing::Single,
space_before: LengthUnit::Points(0.0),
space_after: LengthUnit::Points(0.0),
first_line_indent: LengthUnit::Points(0.0),
left_indent: LengthUnit::Points(0.0),
right_indent: LengthUnit::Points(0.0),
keep_together: false,
keep_with_next: false,
page_break_before: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LineSpacing {
Single,
OneAndHalf,
Double,
Multiple(f32),
Exact(f32),
AtLeast(f32),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rgb_color() {
let color = RgbColor::new(255, 128, 64);
assert_eq!(color.to_hex(), "FF8040");
let parsed = RgbColor::from_hex("FF8040").unwrap();
assert_eq!(parsed, color);
let parsed_with_hash = RgbColor::from_hex("#FF8040").unwrap();
assert_eq!(parsed_with_hash, color);
}
#[test]
fn test_font_size() {
let size = FontSize::new(12.0);
assert_eq!(size.value(), 12.0);
let negative_size = FontSize::new(-5.0);
assert_eq!(negative_size.value(), 0.0);
}
#[test]
fn test_length_unit_conversion() {
let points = LengthUnit::Points(72.0);
assert_eq!(points.to_points(), 72.0);
let inches = LengthUnit::Inches(1.0);
assert_eq!(inches.to_points(), 72.0);
let cm = LengthUnit::Centimeters(2.54);
assert!((cm.to_points() - 72.009).abs() < 0.1);
}
#[test]
fn test_margin() {
let uniform = Margin::uniform(LengthUnit::Points(10.0));
assert_eq!(uniform.top.to_points(), 10.0);
assert_eq!(uniform.right.to_points(), 10.0);
assert_eq!(uniform.bottom.to_points(), 10.0);
assert_eq!(uniform.left.to_points(), 10.0);
let zero = Margin::zero();
assert_eq!(zero.top.to_points(), 0.0);
}
#[test]
fn test_page_size() {
let a4 = PageSize::a4();
assert_eq!(a4.orientation, PageOrientation::Portrait);
assert!((a4.width.to_points() - 595.35).abs() < 0.1);
let letter = PageSize::letter();
assert_eq!(letter.width.to_points(), 612.0); }
#[test]
fn test_font_style() {
let default_style = FontStyle::default();
assert!(!default_style.bold);
assert!(!default_style.italic);
assert_eq!(default_style.underline, UnderlineStyle::None);
assert!(!default_style.strikethrough);
assert!(!default_style.superscript);
assert!(!default_style.subscript);
let bold_style = FontStyle::bold();
assert!(bold_style.bold);
assert!(!bold_style.italic);
let italic_style = FontStyle::italic();
assert!(!italic_style.bold);
assert!(italic_style.italic);
}
#[test]
fn test_rgba_color() {
let rgba = RgbaColor::new(255, 0, 0, 128);
assert_eq!(rgba.red, 255);
assert_eq!(rgba.green, 0);
assert_eq!(rgba.blue, 0);
assert_eq!(rgba.alpha, 128);
assert_eq!(rgba.alpha_percent(), 0.5019608);
assert_eq!(rgba.to_hex(), "FF000080");
let rgba_from_hex = RgbaColor::from_hex("FF000080").unwrap();
assert_eq!(rgba_from_hex, rgba);
let rgb = rgba.to_rgb();
assert_eq!(rgb.red, 255);
assert_eq!(rgb.green, 0);
assert_eq!(rgb.blue, 0);
let semi_transparent = RgbaColor::RED.with_alpha_percent(0.5);
assert_eq!(semi_transparent.alpha, 127); }
#[test]
fn test_color_enum() {
let rgb_color = Color::Rgb(RgbColor::RED);
let rgba_color = Color::Rgba(RgbaColor::RED);
let theme_color = Color::Theme(ThemeColor::Accent1);
let auto_color = Color::Auto;
assert_ne!(rgb_color, rgba_color);
assert_ne!(rgb_color, theme_color);
assert_ne!(rgb_color, auto_color);
}
#[test]
fn test_underline_style() {
let mut style = FontStyle::default();
assert_eq!(style.underline, UnderlineStyle::None);
style.underline = UnderlineStyle::Single;
assert_eq!(style.underline, UnderlineStyle::Single);
style.underline = UnderlineStyle::Double;
assert_eq!(style.underline, UnderlineStyle::Double);
}
#[test]
fn test_list_style() {
let default_list = ListStyle::default();
assert_eq!(default_list.list_type, ListType::Bullet);
assert_eq!(default_list.level, 0);
assert_eq!(default_list.bullet_char, Some('•'));
let numbered_list = ListStyle {
list_type: ListType::Number,
level: 1,
indent: LengthUnit::Points(36.0),
bullet_char: None,
number_format: Some("1.".to_string()),
};
assert_eq!(numbered_list.list_type, ListType::Number);
assert_eq!(numbered_list.level, 1);
}
#[test]
fn test_line_spacing() {
let single = LineSpacing::Single;
let double = LineSpacing::Double;
let multiple = LineSpacing::Multiple(1.5);
let exact = LineSpacing::Exact(12.0);
assert_ne!(single, double);
if let LineSpacing::Multiple(factor) = multiple {
assert_eq!(factor, 1.5);
} else {
panic!("Expected Multiple variant");
}
if let LineSpacing::Exact(points) = exact {
assert_eq!(points, 12.0);
} else {
panic!("Expected Exact variant");
}
}
}