1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct RgbColor {
15 pub red: u8,
16 pub green: u8,
17 pub blue: u8,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct RgbaColor {
23 pub red: u8,
24 pub green: u8,
25 pub blue: u8,
26 pub alpha: u8, }
28
29impl RgbColor {
30 pub fn new(red: u8, green: u8, blue: u8) -> Self {
32 Self { red, green, blue }
33 }
34
35 pub fn from_hex(hex: &str) -> Result<Self, String> {
37 let hex = hex.trim_start_matches('#');
38 if hex.len() != 6 {
39 return Err("Hex color must be 6 characters long".to_string());
40 }
41
42 let red = u8
43 ::from_str_radix(&hex[0..2], 16)
44 .map_err(|_| "Invalid red component".to_string())?;
45 let green = u8
46 ::from_str_radix(&hex[2..4], 16)
47 .map_err(|_| "Invalid green component".to_string())?;
48 let blue = u8
49 ::from_str_radix(&hex[4..6], 16)
50 .map_err(|_| "Invalid blue component".to_string())?;
51
52 Ok(Self::new(red, green, blue))
53 }
54
55 pub fn to_hex(&self) -> String {
57 format!("{:02X}{:02X}{:02X}", self.red, self.green, self.blue)
58 }
59
60 pub fn to_rgba(&self) -> RgbaColor {
62 RgbaColor::new(self.red, self.green, self.blue, 255)
63 }
64
65 pub const BLACK: Self = Self {
67 red: 0,
68 green: 0,
69 blue: 0,
70 };
71 pub const WHITE: Self = Self {
72 red: 255,
73 green: 255,
74 blue: 255,
75 };
76 pub const RED: Self = Self {
77 red: 255,
78 green: 0,
79 blue: 0,
80 };
81 pub const GREEN: Self = Self {
82 red: 0,
83 green: 255,
84 blue: 0,
85 };
86 pub const BLUE: Self = Self {
87 red: 0,
88 green: 0,
89 blue: 255,
90 };
91}
92
93impl RgbaColor {
94 pub fn new(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
96 Self {
97 red,
98 green,
99 blue,
100 alpha,
101 }
102 }
103
104 pub fn from_hex(hex: &str) -> Result<Self, String> {
106 let hex = hex.trim_start_matches('#');
107 if hex.len() != 8 {
108 return Err("Hex RGBA color must be 8 characters long".to_string());
109 }
110
111 let red = u8
112 ::from_str_radix(&hex[0..2], 16)
113 .map_err(|_| "Invalid red component".to_string())?;
114 let green = u8
115 ::from_str_radix(&hex[2..4], 16)
116 .map_err(|_| "Invalid green component".to_string())?;
117 let blue = u8
118 ::from_str_radix(&hex[4..6], 16)
119 .map_err(|_| "Invalid blue component".to_string())?;
120 let alpha = u8
121 ::from_str_radix(&hex[6..8], 16)
122 .map_err(|_| "Invalid alpha component".to_string())?;
123
124 Ok(Self::new(red, green, blue, alpha))
125 }
126
127 pub fn to_hex(&self) -> String {
129 format!("{:02X}{:02X}{:02X}{:02X}", self.red, self.green, self.blue, self.alpha)
130 }
131
132 pub fn alpha_percent(&self) -> f32 {
134 (self.alpha as f32) / 255.0
135 }
136
137 pub fn with_alpha_percent(mut self, alpha: f32) -> Self {
139 self.alpha = (alpha.clamp(0.0, 1.0) * 255.0) as u8;
140 self
141 }
142
143 pub fn to_rgb(&self) -> RgbColor {
145 RgbColor::new(self.red, self.green, self.blue)
146 }
147
148 pub const TRANSPARENT: Self = Self {
150 red: 0,
151 green: 0,
152 blue: 0,
153 alpha: 0,
154 };
155 pub const BLACK: Self = Self {
156 red: 0,
157 green: 0,
158 blue: 0,
159 alpha: 255,
160 };
161 pub const WHITE: Self = Self {
162 red: 255,
163 green: 255,
164 blue: 255,
165 alpha: 255,
166 };
167 pub const RED: Self = Self {
168 red: 255,
169 green: 0,
170 blue: 0,
171 alpha: 255,
172 };
173 pub const GREEN: Self = Self {
174 red: 0,
175 green: 255,
176 blue: 0,
177 alpha: 255,
178 };
179 pub const BLUE: Self = Self {
180 red: 0,
181 green: 0,
182 blue: 255,
183 alpha: 255,
184 };
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189pub enum ThemeColor {
190 Dark1,
192 Light1,
194 Dark2,
196 Light2,
198 Accent1,
200 Accent2,
201 Accent3,
202 Accent4,
203 Accent5,
204 Accent6,
205 Hyperlink,
207 FollowedHyperlink,
209}
210
211#[derive(Debug, Clone, PartialEq)]
213pub enum Color {
214 Rgb(RgbColor),
216 Rgba(RgbaColor),
218 Theme(ThemeColor),
220 Indexed(u8),
222 Auto,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
228pub struct FontSize(pub f32);
229
230impl FontSize {
231 pub fn new(size: f32) -> Self {
233 Self(size.max(0.0))
234 }
235
236 pub fn value(&self) -> f32 {
238 self.0
239 }
240
241 pub const SMALL: Self = Self(8.0);
243 pub const NORMAL: Self = Self(11.0);
244 pub const MEDIUM: Self = Self(12.0);
245 pub const LARGE: Self = Self(14.0);
246 pub const EXTRA_LARGE: Self = Self(18.0);
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
251pub enum UnderlineStyle {
252 None,
253 Single,
254 Double,
255 Thick,
256 Dotted,
257 DottedHeavy,
258 Dashed,
259 DashedHeavy,
260 DashLong,
261 DashLongHeavy,
262 DotDash,
263 DotDashHeavy,
264 DotDotDash,
265 DotDotDashHeavy,
266 Wave,
267 WaveHeavy,
268 WaveDouble,
269}
270
271#[derive(Debug, Clone, PartialEq)]
273pub struct FontStyle {
274 pub bold: bool,
275 pub italic: bool,
276 pub underline: UnderlineStyle,
277 pub strikethrough: bool,
278 pub superscript: bool,
279 pub subscript: bool,
280 pub small_caps: bool,
281 pub all_caps: bool,
282}
283
284impl Default for FontStyle {
285 fn default() -> Self {
286 Self {
287 bold: false,
288 italic: false,
289 underline: UnderlineStyle::None,
290 strikethrough: false,
291 superscript: false,
292 subscript: false,
293 small_caps: false,
294 all_caps: false,
295 }
296 }
297}
298
299impl FontStyle {
300 pub fn bold() -> Self {
302 Self {
303 bold: true,
304 italic: false,
305 underline: UnderlineStyle::None,
306 strikethrough: false,
307 superscript: false,
308 subscript: false,
309 small_caps: false,
310 all_caps: false,
311 }
312 }
313
314 pub fn italic() -> Self {
316 Self {
317 bold: false,
318 italic: true,
319 underline: UnderlineStyle::None,
320 strikethrough: false,
321 superscript: false,
322 subscript: false,
323 small_caps: false,
324 all_caps: false,
325 }
326 }
327}
328
329#[derive(Debug, Clone, PartialEq)]
331pub struct Font {
332 pub name: String,
333 pub size: FontSize,
334 pub style: FontStyle,
335 pub color: Color,
336}
337
338impl Default for Font {
339 fn default() -> Self {
340 Self {
341 name: "Calibri".to_string(),
342 size: FontSize::NORMAL,
343 style: FontStyle::default(),
344 color: Color::Auto,
345 }
346 }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq)]
351pub enum LengthUnit {
352 Points(f32),
354 Pixels(f32),
356 Inches(f32),
358 Centimeters(f32),
360 Millimeters(f32),
362 Emu(i32),
364}
365
366impl LengthUnit {
367 pub fn to_points(&self) -> f32 {
369 match self {
370 LengthUnit::Points(p) => *p,
371 LengthUnit::Pixels(px) => *px * 0.75, LengthUnit::Inches(inch) => *inch * 72.0,
373 LengthUnit::Centimeters(cm) => *cm * 28.35,
374 LengthUnit::Millimeters(mm) => *mm * 2.835,
375 LengthUnit::Emu(emu) => (*emu as f32) / 12700.0,
376 }
377 }
378
379 pub fn to_emu(&self) -> i32 {
381 (self.to_points() * 12700.0) as i32
382 }
383}
384
385#[derive(Debug, Clone, Copy, PartialEq)]
387pub struct Margin {
388 pub top: LengthUnit,
389 pub right: LengthUnit,
390 pub bottom: LengthUnit,
391 pub left: LengthUnit,
392}
393
394impl Margin {
395 pub fn uniform(margin: LengthUnit) -> Self {
397 Self {
398 top: margin,
399 right: margin,
400 bottom: margin,
401 left: margin,
402 }
403 }
404
405 pub fn zero() -> Self {
407 Self::uniform(LengthUnit::Points(0.0))
408 }
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
413pub enum HorizontalAlignment {
414 Left,
415 Center,
416 Right,
417 Justify,
418 Distributed,
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
423pub enum VerticalAlignment {
424 Top,
425 Middle,
426 Bottom,
427 Justify,
428 Distributed,
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
433pub enum BorderStyle {
434 None,
435 Thin,
436 Medium,
437 Thick,
438 Double,
439 Dotted,
440 Dashed,
441 DashDot,
442 DashDotDot,
443}
444
445#[derive(Debug, Clone, PartialEq)]
447pub struct Border {
448 pub style: BorderStyle,
449 pub color: Color,
450 pub width: LengthUnit,
451}
452
453impl Default for Border {
454 fn default() -> Self {
455 Self {
456 style: BorderStyle::None,
457 color: Color::Auto,
458 width: LengthUnit::Points(0.0),
459 }
460 }
461}
462
463#[derive(Debug, Clone, PartialEq)]
465pub struct Borders {
466 pub top: Border,
467 pub right: Border,
468 pub bottom: Border,
469 pub left: Border,
470}
471
472impl Default for Borders {
473 fn default() -> Self {
474 Self {
475 top: Border::default(),
476 right: Border::default(),
477 bottom: Border::default(),
478 left: Border::default(),
479 }
480 }
481}
482
483#[derive(Debug, Clone, PartialEq)]
485pub enum FillPattern {
486 None,
488 Solid(Color),
490 Gradient {
492 start_color: Color,
493 end_color: Color,
494 angle: f32, },
496 Pattern {
498 pattern_type: PatternType,
499 foreground_color: Color,
500 background_color: Color,
501 },
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
506pub enum PatternType {
507 Solid,
508 Gray75,
509 Gray50,
510 Gray25,
511 Gray125,
512 Gray0625,
513 HorizontalStripe,
514 VerticalStripe,
515 ReverseDiagonalStripe,
516 DiagonalStripe,
517 DiagonalCrosshatch,
518 ThickDiagonalCrosshatch,
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
523pub enum PageOrientation {
524 Portrait,
525 Landscape,
526}
527
528#[derive(Debug, Clone, Copy, PartialEq)]
530pub struct PageSize {
531 pub width: LengthUnit,
532 pub height: LengthUnit,
533 pub orientation: PageOrientation,
534}
535
536impl PageSize {
537 pub fn a4() -> Self {
539 Self {
540 width: LengthUnit::Millimeters(210.0),
541 height: LengthUnit::Millimeters(297.0),
542 orientation: PageOrientation::Portrait,
543 }
544 }
545
546 pub fn letter() -> Self {
548 Self {
549 width: LengthUnit::Inches(8.5),
550 height: LengthUnit::Inches(11.0),
551 orientation: PageOrientation::Portrait,
552 }
553 }
554}
555
556#[derive(Debug, Clone, PartialEq)]
558pub struct DocumentProperties {
559 pub title: Option<String>,
560 pub author: Option<String>,
561 pub subject: Option<String>,
562 pub keywords: Option<String>,
563 pub description: Option<String>,
564 pub category: Option<String>,
565 pub created: Option<chrono::DateTime<chrono::Utc>>,
566 pub modified: Option<chrono::DateTime<chrono::Utc>>,
567}
568
569impl Default for DocumentProperties {
570 fn default() -> Self {
571 Self {
572 title: None,
573 author: None,
574 subject: None,
575 keywords: None,
576 description: None,
577 category: None,
578 created: None,
579 modified: None,
580 }
581 }
582}
583
584#[derive(Debug, Clone, Copy, PartialEq)]
586pub struct Point {
587 pub x: LengthUnit,
588 pub y: LengthUnit,
589}
590
591#[derive(Debug, Clone, Copy, PartialEq)]
593pub struct Size {
594 pub width: LengthUnit,
595 pub height: LengthUnit,
596}
597
598#[derive(Debug, Clone, Copy, PartialEq)]
600pub struct Rectangle {
601 pub position: Point,
602 pub size: Size,
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
607pub enum ListType {
608 Bullet,
610 Number,
612 LowerAlpha,
614 UpperAlpha,
616 LowerRoman,
618 UpperRoman,
620}
621
622#[derive(Debug, Clone, PartialEq)]
624pub struct ListStyle {
625 pub list_type: ListType,
626 pub level: u8,
627 pub indent: LengthUnit,
628 pub bullet_char: Option<char>,
629 pub number_format: Option<String>,
630}
631
632impl Default for ListStyle {
633 fn default() -> Self {
634 Self {
635 list_type: ListType::Bullet,
636 level: 0,
637 indent: LengthUnit::Points(18.0),
638 bullet_char: Some('•'),
639 number_format: None,
640 }
641 }
642}
643
644#[derive(Debug, Clone, PartialEq)]
646pub struct TableBorderStyle {
647 pub outer: Borders,
648 pub inner_horizontal: Border,
649 pub inner_vertical: Border,
650}
651
652impl Default for TableBorderStyle {
653 fn default() -> Self {
654 Self {
655 outer: Borders::default(),
656 inner_horizontal: Border::default(),
657 inner_vertical: Border::default(),
658 }
659 }
660}
661
662#[derive(Debug, Clone, PartialEq)]
664pub struct TableStyle {
665 pub borders: TableBorderStyle,
666 pub cell_padding: Margin,
667 pub cell_spacing: LengthUnit,
668 pub background_color: Option<Color>,
669 pub stripe_rows: bool,
670 pub stripe_columns: bool,
671}
672
673impl Default for TableStyle {
674 fn default() -> Self {
675 Self {
676 borders: TableBorderStyle::default(),
677 cell_padding: Margin::uniform(LengthUnit::Points(2.0)),
678 cell_spacing: LengthUnit::Points(0.0),
679 background_color: None,
680 stripe_rows: false,
681 stripe_columns: false,
682 }
683 }
684}
685
686#[derive(Debug, Clone, PartialEq)]
688pub struct TextDecoration {
689 pub shadow: bool,
690 pub emboss: bool,
691 pub imprint: bool,
692 pub outline: bool,
693 pub glow: Option<Color>,
694 pub reflection: bool,
695}
696
697impl Default for TextDecoration {
698 fn default() -> Self {
699 Self {
700 shadow: false,
701 emboss: false,
702 imprint: false,
703 outline: false,
704 glow: None,
705 reflection: false,
706 }
707 }
708}
709
710#[derive(Debug, Clone, PartialEq)]
712pub struct ParagraphStyle {
713 pub alignment: HorizontalAlignment,
714 pub line_spacing: LineSpacing,
715 pub space_before: LengthUnit,
716 pub space_after: LengthUnit,
717 pub first_line_indent: LengthUnit,
718 pub left_indent: LengthUnit,
719 pub right_indent: LengthUnit,
720 pub keep_together: bool,
721 pub keep_with_next: bool,
722 pub page_break_before: bool,
723}
724
725impl Default for ParagraphStyle {
726 fn default() -> Self {
727 Self {
728 alignment: HorizontalAlignment::Left,
729 line_spacing: LineSpacing::Single,
730 space_before: LengthUnit::Points(0.0),
731 space_after: LengthUnit::Points(0.0),
732 first_line_indent: LengthUnit::Points(0.0),
733 left_indent: LengthUnit::Points(0.0),
734 right_indent: LengthUnit::Points(0.0),
735 keep_together: false,
736 keep_with_next: false,
737 page_break_before: false,
738 }
739 }
740}
741
742#[derive(Debug, Clone, Copy, PartialEq)]
744pub enum LineSpacing {
745 Single,
747 OneAndHalf,
749 Double,
751 Multiple(f32),
753 Exact(f32),
755 AtLeast(f32),
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762
763 #[test]
764 fn test_rgb_color() {
765 let color = RgbColor::new(255, 128, 64);
766 assert_eq!(color.to_hex(), "FF8040");
767
768 let parsed = RgbColor::from_hex("FF8040").unwrap();
769 assert_eq!(parsed, color);
770
771 let parsed_with_hash = RgbColor::from_hex("#FF8040").unwrap();
772 assert_eq!(parsed_with_hash, color);
773 }
774
775 #[test]
776 fn test_font_size() {
777 let size = FontSize::new(12.0);
778 assert_eq!(size.value(), 12.0);
779
780 let negative_size = FontSize::new(-5.0);
782 assert_eq!(negative_size.value(), 0.0);
783 }
784
785 #[test]
786 fn test_length_unit_conversion() {
787 let points = LengthUnit::Points(72.0);
788 assert_eq!(points.to_points(), 72.0);
789
790 let inches = LengthUnit::Inches(1.0);
791 assert_eq!(inches.to_points(), 72.0);
792
793 let cm = LengthUnit::Centimeters(2.54);
794 assert!((cm.to_points() - 72.009).abs() < 0.1);
795 }
796
797 #[test]
798 fn test_margin() {
799 let uniform = Margin::uniform(LengthUnit::Points(10.0));
800 assert_eq!(uniform.top.to_points(), 10.0);
801 assert_eq!(uniform.right.to_points(), 10.0);
802 assert_eq!(uniform.bottom.to_points(), 10.0);
803 assert_eq!(uniform.left.to_points(), 10.0);
804
805 let zero = Margin::zero();
806 assert_eq!(zero.top.to_points(), 0.0);
807 }
808
809 #[test]
810 fn test_page_size() {
811 let a4 = PageSize::a4();
812 assert_eq!(a4.orientation, PageOrientation::Portrait);
813 assert!((a4.width.to_points() - 595.35).abs() < 0.1);
815
816 let letter = PageSize::letter();
817 assert_eq!(letter.width.to_points(), 612.0); }
819
820 #[test]
821 fn test_font_style() {
822 let default_style = FontStyle::default();
823 assert!(!default_style.bold);
824 assert!(!default_style.italic);
825 assert_eq!(default_style.underline, UnderlineStyle::None);
826 assert!(!default_style.strikethrough);
827 assert!(!default_style.superscript);
828 assert!(!default_style.subscript);
829
830 let bold_style = FontStyle::bold();
831 assert!(bold_style.bold);
832 assert!(!bold_style.italic);
833
834 let italic_style = FontStyle::italic();
835 assert!(!italic_style.bold);
836 assert!(italic_style.italic);
837 }
838
839 #[test]
840 fn test_rgba_color() {
841 let rgba = RgbaColor::new(255, 0, 0, 128);
842 assert_eq!(rgba.red, 255);
843 assert_eq!(rgba.green, 0);
844 assert_eq!(rgba.blue, 0);
845 assert_eq!(rgba.alpha, 128);
846
847 assert_eq!(rgba.alpha_percent(), 0.5019608); assert_eq!(rgba.to_hex(), "FF000080");
852
853 let rgba_from_hex = RgbaColor::from_hex("FF000080").unwrap();
855 assert_eq!(rgba_from_hex, rgba);
856
857 let rgb = rgba.to_rgb();
859 assert_eq!(rgb.red, 255);
860 assert_eq!(rgb.green, 0);
861 assert_eq!(rgb.blue, 0);
862
863 let semi_transparent = RgbaColor::RED.with_alpha_percent(0.5);
865 assert_eq!(semi_transparent.alpha, 127); }
867
868 #[test]
869 fn test_color_enum() {
870 let rgb_color = Color::Rgb(RgbColor::RED);
871 let rgba_color = Color::Rgba(RgbaColor::RED);
872 let theme_color = Color::Theme(ThemeColor::Accent1);
873 let auto_color = Color::Auto;
874
875 assert_ne!(rgb_color, rgba_color);
877 assert_ne!(rgb_color, theme_color);
878 assert_ne!(rgb_color, auto_color);
879 }
880
881 #[test]
882 fn test_underline_style() {
883 let mut style = FontStyle::default();
884 assert_eq!(style.underline, UnderlineStyle::None);
885
886 style.underline = UnderlineStyle::Single;
887 assert_eq!(style.underline, UnderlineStyle::Single);
888
889 style.underline = UnderlineStyle::Double;
890 assert_eq!(style.underline, UnderlineStyle::Double);
891 }
892
893 #[test]
894 fn test_list_style() {
895 let default_list = ListStyle::default();
896 assert_eq!(default_list.list_type, ListType::Bullet);
897 assert_eq!(default_list.level, 0);
898 assert_eq!(default_list.bullet_char, Some('•'));
899
900 let numbered_list = ListStyle {
901 list_type: ListType::Number,
902 level: 1,
903 indent: LengthUnit::Points(36.0),
904 bullet_char: None,
905 number_format: Some("1.".to_string()),
906 };
907 assert_eq!(numbered_list.list_type, ListType::Number);
908 assert_eq!(numbered_list.level, 1);
909 }
910
911 #[test]
912 fn test_line_spacing() {
913 let single = LineSpacing::Single;
914 let double = LineSpacing::Double;
915 let multiple = LineSpacing::Multiple(1.5);
916 let exact = LineSpacing::Exact(12.0);
917
918 assert_ne!(single, double);
919
920 if let LineSpacing::Multiple(factor) = multiple {
921 assert_eq!(factor, 1.5);
922 } else {
923 panic!("Expected Multiple variant");
924 }
925
926 if let LineSpacing::Exact(points) = exact {
927 assert_eq!(points, 12.0);
928 } else {
929 panic!("Expected Exact variant");
930 }
931 }
932}