Skip to main content

easyofd_core/text/
text_code.rs

1//! TextCode 文字定位。
2
3/// 对应 Java: org.ofdrw.core.text.TextCode
4///
5/// 文字定位信息,使用严格的定位数据来确定文字在页面上的位置。
6/// 文字对象使用 TextCode 列表来存储文字内容和每个字符的精确定位。
7/// 对应 GB/T 33190-2016 第 11.3 节图 61 表 46。
8#[derive(Debug, Clone)]
9pub struct TextCode {
10    /// 文本内容(字形编码序列)。
11    pub content: String,
12    /// 文本起始 X 坐标(mm)。None 表示使用上一个 TextCode 的结束位置。
13    pub x: Option<f64>,
14    /// 文本起始 Y 坐标(mm)。None 表示使用上一个 TextCode 的 Y 坐标。
15    pub y: Option<f64>,
16    /// X 方向偏移量列表(每个字形的增量偏移,单位 mm)。
17    pub delta_x: Vec<f64>,
18    /// Y 方向偏移量列表(每个字形的增量偏移,单位 mm)。
19    pub delta_y: Vec<f64>,
20}
21
22impl TextCode {
23    /// 创建空的文字定位。
24    #[must_use]
25    pub fn new() -> Self {
26        Self {
27            content: String::new(),
28            x: None,
29            y: None,
30            delta_x: Vec::new(),
31            delta_y: Vec::new(),
32        }
33    }
34
35    /// 使用内容创建文字定位。
36    #[must_use]
37    pub fn with_content(content: impl Into<String>) -> Self {
38        Self {
39            content: content.into(),
40            ..Self::new()
41        }
42    }
43
44    /// 设置文本内容。
45    #[must_use]
46    pub fn content(mut self, content: impl Into<String>) -> Self {
47        self.content = content.into();
48        self
49    }
50
51    /// 设置坐标。
52    #[must_use]
53    pub fn coordinate(mut self, x: f64, y: f64) -> Self {
54        self.x = Some(x);
55        self.y = Some(y);
56        self
57    }
58
59    /// 设置 X 坐标。
60    #[must_use]
61    pub fn x(mut self, x: f64) -> Self {
62        self.x = Some(x);
63        self
64    }
65
66    /// 设置 Y 坐标。
67    #[must_use]
68    pub fn y(mut self, y: f64) -> Self {
69        self.y = Some(y);
70        self
71    }
72
73    /// 设置 X 方向偏移量列表。
74    #[must_use]
75    pub fn delta_x(mut self, deltas: Vec<f64>) -> Self {
76        self.delta_x = deltas;
77        self
78    }
79
80    /// 设置 Y 方向偏移量列表。
81    #[must_use]
82    pub fn delta_y(mut self, deltas: Vec<f64>) -> Self {
83        self.delta_y = deltas;
84        self
85    }
86
87    /// 获取内容。
88    #[must_use]
89    pub fn get_content(&self) -> &str {
90        &self.content
91    }
92
93    /// 获取 X 坐标。
94    #[must_use]
95    pub fn get_x(&self) -> Option<f64> {
96        self.x
97    }
98
99    /// 获取 Y 坐标。
100    #[must_use]
101    pub fn get_y(&self) -> Option<f64> {
102        self.y
103    }
104
105    /// 序列化为 OFD XML 字符串。
106    #[must_use]
107    pub fn to_xml_string(&self) -> String {
108        use std::fmt::Write;
109        let mut xml = String::from("<ofd:TextCode");
110        if let Some(x) = self.x {
111            write!(xml, " X=\"{x}\"").expect("写入内存缓冲区不会失败");
112        }
113        if let Some(y) = self.y {
114            write!(xml, " Y=\"{y}\"").expect("写入内存缓冲区不会失败");
115        }
116        if !self.delta_x.is_empty() {
117            xml.push_str(" DeltaX=\"");
118            for (i, d) in self.delta_x.iter().enumerate() {
119                if i > 0 {
120                    xml.push(' ');
121                }
122                write!(xml, "{d}").expect("写入内存缓冲区不会失败");
123            }
124            xml.push('"');
125        }
126        if !self.delta_y.is_empty() {
127            xml.push_str(" DeltaY=\"");
128            for (i, d) in self.delta_y.iter().enumerate() {
129                if i > 0 {
130                    xml.push(' ');
131                }
132                write!(xml, "{d}").expect("写入内存缓冲区不会失败");
133            }
134            xml.push('"');
135        }
136        xml.push('>');
137        xml.push_str(&self.content);
138        xml.push_str("</ofd:TextCode>");
139        xml
140    }
141}
142
143impl Default for TextCode {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_text_code_new() {
155        let tc = TextCode::new();
156        assert!(tc.content.is_empty());
157        assert!(tc.x.is_none());
158        assert!(tc.y.is_none());
159        assert!(tc.delta_x.is_empty());
160        assert!(tc.delta_y.is_empty());
161    }
162
163    #[test]
164    fn test_text_code_with_content() {
165        let tc = TextCode::with_content("Hello");
166        assert_eq!(tc.get_content(), "Hello");
167        assert!(tc.x.is_none());
168    }
169
170    #[test]
171    fn test_text_code_builder() {
172        let tc = TextCode::new()
173            .content("World")
174            .coordinate(10.0, 20.0)
175            .delta_x(vec![6.0, 6.0, 6.0]);
176        assert_eq!(tc.get_content(), "World");
177        assert!((tc.get_x().unwrap() - 10.0).abs() < f64::EPSILON);
178        assert!((tc.get_y().unwrap() - 20.0).abs() < f64::EPSILON);
179        assert_eq!(tc.delta_x.len(), 3);
180    }
181
182    #[test]
183    fn test_text_code_to_xml_minimal() {
184        let tc = TextCode::with_content("A");
185        let xml = tc.to_xml_string();
186        assert!(xml.contains("<ofd:TextCode"));
187        assert!(xml.contains(">A</ofd:TextCode>"));
188    }
189
190    #[test]
191    fn test_text_code_to_xml_with_position() {
192        let tc = TextCode::with_content("Hi").coordinate(5.0, 10.0);
193        let xml = tc.to_xml_string();
194        assert!(xml.contains("X=\"5\""));
195        assert!(xml.contains("Y=\"10\""));
196        assert!(xml.contains(">Hi</ofd:TextCode>"));
197    }
198
199    #[test]
200    fn test_text_code_to_xml_with_deltas() {
201        let tc = TextCode::with_content("ABC")
202            .x(0.0)
203            .delta_x(vec![6.0, 6.0, 6.0])
204            .delta_y(vec![0.0, 0.0]);
205        let xml = tc.to_xml_string();
206        assert!(xml.contains("DeltaX=\"6 6 6\""));
207        assert!(xml.contains("DeltaY=\"0 0\""));
208    }
209
210    #[test]
211    fn test_text_code_clone_debug() {
212        let tc = TextCode::with_content("x");
213        let tc2 = tc.clone();
214        assert_eq!(tc2.get_content(), "x");
215        assert!(format!("{tc:?}").contains("TextCode"));
216    }
217}