Skip to main content

easyofd_core/annotation/
annot.rs

1//! 单个注释。
2
3use std::fmt::Write;
4
5use super::annot_type::AnnotType;
6use super::appearance::Appearance;
7
8/// 对应 Java: org.ofdrw.core.annotation.Annot
9///
10/// 表示一个单独的注释对象,包含注释的标识、创建者、类型、
11/// 标志位、最后修改时间、位置和外观等信息。
12#[derive(Debug, Clone)]
13pub struct Annot {
14    /// 注释 ID。
15    pub id: String,
16    /// 注释创建者。
17    pub creator: Option<String>,
18    /// 注释类型。
19    pub annot_type: AnnotType,
20    /// 注释标志位(如是否可打印、是否锁定等)。
21    pub flags: u32,
22    /// 最后修改日期(ISO 8601 格式字符串)。
23    pub last_mod_date: Option<String>,
24    /// 注释在页面上的矩形区域 [x, y, width, height](单位 mm)。
25    pub location: [f64; 4],
26    /// 注释外观列表。
27    pub appearances: Vec<Appearance>,
28}
29
30impl Annot {
31    /// 创建一个新的注释。
32    #[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    /// 设置创建者。
46    #[must_use]
47    pub fn creator(mut self, creator: impl Into<String>) -> Self {
48        self.creator = Some(creator.into());
49        self
50    }
51
52    /// 设置标志位。
53    #[must_use]
54    pub fn flags(mut self, flags: u32) -> Self {
55        self.flags = flags;
56        self
57    }
58
59    /// 设置最后修改日期。
60    #[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    /// 设置位置。
67    #[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    /// 添加外观。
74    #[must_use]
75    pub fn add_appearance(mut self, appearance: Appearance) -> Self {
76        self.appearances.push(appearance);
77        self
78    }
79
80    /// 序列化为 XML 字符串。
81    #[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}