easyofd_core/annotation/
annot.rs1use std::fmt::Write;
4
5use super::annot_type::AnnotType;
6use super::appearance::Appearance;
7
8#[derive(Debug, Clone)]
13pub struct Annot {
14 pub id: String,
16 pub creator: Option<String>,
18 pub annot_type: AnnotType,
20 pub flags: u32,
22 pub last_mod_date: Option<String>,
24 pub location: [f64; 4],
26 pub appearances: Vec<Appearance>,
28}
29
30impl Annot {
31 #[must_use]
33 pub fn new(id: impl Into<String>, annot_type: AnnotType) -> Self {
34 Self {
35 id: id.into(),
36 creator: None,
37 annot_type,
38 flags: 0,
39 last_mod_date: None,
40 location: [0.0, 0.0, 0.0, 0.0],
41 appearances: Vec::new(),
42 }
43 }
44
45 #[must_use]
47 pub fn creator(mut self, creator: impl Into<String>) -> Self {
48 self.creator = Some(creator.into());
49 self
50 }
51
52 #[must_use]
54 pub fn flags(mut self, flags: u32) -> Self {
55 self.flags = flags;
56 self
57 }
58
59 #[must_use]
61 pub fn last_mod_date(mut self, date: impl Into<String>) -> Self {
62 self.last_mod_date = Some(date.into());
63 self
64 }
65
66 #[must_use]
68 pub fn location(mut self, x: f64, y: f64, w: f64, h: f64) -> Self {
69 self.location = [x, y, w, h];
70 self
71 }
72
73 #[must_use]
75 pub fn add_appearance(mut self, appearance: Appearance) -> Self {
76 self.appearances.push(appearance);
77 self
78 }
79
80 #[must_use]
82 pub fn to_xml_string(&self) -> String {
83 let mut xml = format!(
84 r#"<ofd:Annot ID="{}" Type="{}" Flags="{}" "#,
85 self.id,
86 self.annot_type.as_str(),
87 self.flags
88 );
89
90 if let Some(ref creator) = self.creator {
91 let _ = write!(xml, r#"Creator="{creator}" "#);
92 }
93
94 if let Some(ref date) = self.last_mod_date {
95 let _ = write!(xml, r#"LastModDate="{date}" "#);
96 }
97
98 let [x, y, w, h] = self.location;
99 let _ = write!(xml, r#"Location="{x} {y} {w} {h}""#);
100
101 if self.appearances.is_empty() {
102 xml.push_str(" />");
103 } else {
104 xml.push('>');
105 for app in &self.appearances {
106 xml.push('\n');
107 xml.push_str(&app.to_xml_string());
108 }
109 xml.push_str("\n</ofd:Annot>");
110 }
111 xml
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_annot_new() {
121 let a = Annot::new("ann1", AnnotType::Text);
122 assert_eq!(a.id, "ann1");
123 assert_eq!(a.annot_type, AnnotType::Text);
124 assert_eq!(a.flags, 0);
125 assert!(a.creator.is_none());
126 assert!(a.last_mod_date.is_none());
127 assert!((a.location[0]).abs() < f64::EPSILON);
128 assert!((a.location[1]).abs() < f64::EPSILON);
129 assert!((a.location[2]).abs() < f64::EPSILON);
130 assert!((a.location[3]).abs() < f64::EPSILON);
131 assert!(a.appearances.is_empty());
132 }
133
134 #[test]
135 fn test_annot_builder() {
136 let a = Annot::new("ann2", AnnotType::Highlight)
137 .creator("user1")
138 .flags(4)
139 .last_mod_date("2025-01-01T00:00:00")
140 .location(10.0, 20.0, 30.0, 40.0)
141 .add_appearance(Appearance::new("app1", "Normal"));
142 assert_eq!(a.creator.as_deref(), Some("user1"));
143 assert_eq!(a.flags, 4);
144 assert_eq!(a.last_mod_date.as_deref(), Some("2025-01-01T00:00:00"));
145 assert!((a.location[0] - 10.0).abs() < f64::EPSILON);
146 assert!((a.location[1] - 20.0).abs() < f64::EPSILON);
147 assert!((a.location[2] - 30.0).abs() < f64::EPSILON);
148 assert!((a.location[3] - 40.0).abs() < f64::EPSILON);
149 assert_eq!(a.appearances.len(), 1);
150 }
151
152 #[test]
153 fn test_annot_to_xml_string_basic() {
154 let a = Annot::new("a1", AnnotType::Link);
155 let xml = a.to_xml_string();
156 assert!(xml.contains(r#"ID="a1""#));
157 assert!(xml.contains(r#"Type="Link""#));
158 assert!(xml.contains(r#"Flags="0""#));
159 assert!(xml.contains(" />"));
160 }
161
162 #[test]
163 fn test_annot_to_xml_string_full() {
164 let a = Annot::new("a2", AnnotType::Stamp)
165 .creator("admin")
166 .last_mod_date("2025-06-01")
167 .location(1.0, 2.0, 3.0, 4.0)
168 .add_appearance(Appearance::new("app1", "Normal"));
169 let xml = a.to_xml_string();
170 assert!(xml.contains(r#"Creator="admin""#));
171 assert!(xml.contains(r#"LastModDate="2025-06-01""#));
172 assert!(xml.contains(r#"Location="1 2 3 4""#));
173 assert!(xml.contains("ofd:Appearance"));
174 assert!(xml.contains("</ofd:Annot>"));
175 }
176}