Skip to main content

easyofd_core/text/
ct_text.rs

1//! CT_Text 文本对象。
2
3use super::{CT_CGTransform, TextCode};
4
5/// 对应 Java: org.ofdrw.core.text.text.CT_Text
6///
7/// 文本对象,扩展自图形单元,包含字体引用、文字方向、字间距等
8/// 文本特有属性。对应 GB/T 33190-2016 第 11.2 节图 59 表 45。
9#[allow(non_camel_case_types)]
10#[derive(Debug, Clone)]
11pub struct CT_Text {
12    /// 对象 ID。
13    pub id: u32,
14    /// 边界框 "x y width height"(单位 mm)。
15    pub boundary: String,
16    /// 字体引用 ID。
17    pub font_ref: Option<u32>,
18    /// 字号(pt)。
19    pub size: Option<f64>,
20    /// 是否描边。
21    pub stroke: bool,
22    /// 是否填充。
23    pub fill: bool,
24    /// 水平缩放比例。
25    pub h_scale: Option<f64>,
26    /// 阅读方向(角度,0/90/180/270)。
27    pub read_direction: Option<u32>,
28    /// 字符排列方向(角度)。
29    pub char_direction: Option<u32>,
30    /// 字重(400=正常,700=粗体)。
31    pub weight: Option<u32>,
32    /// 是否斜体。
33    pub italic: bool,
34    /// 填充颜色 RGB hex。
35    pub fill_color: Option<u32>,
36    /// 描边颜色 RGB hex。
37    pub stroke_color: Option<u32>,
38    /// 字形变换列表。
39    pub cg_transforms: Vec<CT_CGTransform>,
40    /// 文字定位列表。
41    pub text_codes: Vec<TextCode>,
42}
43
44/// 文字阅读方向。
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Direction {
47    /// 从左到右(0度)。
48    LeftToRight,
49    /// 从上到下(90度)。
50    TopToBottom,
51    /// 从右到左(180度)。
52    RightToLeft,
53    /// 从下到上(270度)。
54    BottomToTop,
55}
56
57impl Direction {
58    /// 获取角度值。
59    #[must_use]
60    pub fn as_degrees(&self) -> u32 {
61        match self {
62            Self::LeftToRight => 0,
63            Self::TopToBottom => 90,
64            Self::RightToLeft => 180,
65            Self::BottomToTop => 270,
66        }
67    }
68}
69
70impl CT_Text {
71    /// 创建新的文本对象。
72    #[must_use]
73    pub fn new(id: u32, boundary: impl Into<String>) -> Self {
74        Self {
75            id,
76            boundary: boundary.into(),
77            font_ref: None,
78            size: None,
79            stroke: false,
80            fill: true,
81            h_scale: None,
82            read_direction: None,
83            char_direction: None,
84            weight: None,
85            italic: false,
86            fill_color: None,
87            stroke_color: None,
88            cg_transforms: Vec::new(),
89            text_codes: Vec::new(),
90        }
91    }
92
93    /// 设置字体引用。
94    #[must_use]
95    pub fn font(mut self, ref_id: u32) -> Self {
96        self.font_ref = Some(ref_id);
97        self
98    }
99
100    /// 设置字号。
101    #[must_use]
102    pub fn size(mut self, size: f64) -> Self {
103        self.size = Some(size);
104        self
105    }
106
107    /// 设置描边。
108    #[must_use]
109    pub fn stroke(mut self, stroke: bool) -> Self {
110        self.stroke = stroke;
111        self
112    }
113
114    /// 设置填充。
115    #[must_use]
116    pub fn fill(mut self, fill: bool) -> Self {
117        self.fill = fill;
118        self
119    }
120
121    /// 设置水平缩放。
122    #[must_use]
123    pub fn h_scale(mut self, scale: f64) -> Self {
124        self.h_scale = Some(scale);
125        self
126    }
127
128    /// 设置阅读方向。
129    #[must_use]
130    pub fn read_direction(mut self, dir: Direction) -> Self {
131        self.read_direction = Some(dir.as_degrees());
132        self
133    }
134
135    /// 设置字符方向。
136    #[must_use]
137    pub fn char_direction(mut self, dir: Direction) -> Self {
138        self.char_direction = Some(dir.as_degrees());
139        self
140    }
141
142    /// 设置字重。
143    #[must_use]
144    pub fn weight(mut self, weight: u32) -> Self {
145        self.weight = Some(weight);
146        self
147    }
148
149    /// 设置斜体。
150    #[must_use]
151    pub fn italic(mut self, italic: bool) -> Self {
152        self.italic = italic;
153        self
154    }
155
156    /// 设置填充颜色。
157    #[must_use]
158    pub fn fill_color(mut self, color: u32) -> Self {
159        self.fill_color = Some(color);
160        self
161    }
162
163    /// 设置描边颜色。
164    #[must_use]
165    pub fn stroke_color(mut self, color: u32) -> Self {
166        self.stroke_color = Some(color);
167        self
168    }
169
170    /// 添加字形变换。
171    pub fn add_cg_transform(&mut self, cg: CT_CGTransform) {
172        self.cg_transforms.push(cg);
173    }
174
175    /// 添加文字定位。
176    pub fn add_text_code(&mut self, tc: TextCode) {
177        self.text_codes.push(tc);
178    }
179
180    /// 获取字体引用。
181    #[must_use]
182    pub fn get_font(&self) -> Option<u32> {
183        self.font_ref
184    }
185
186    /// 获取字号。
187    #[must_use]
188    pub fn get_size(&self) -> Option<f64> {
189        self.size
190    }
191
192    /// 获取字重。
193    #[must_use]
194    pub fn get_weight(&self) -> Option<u32> {
195        self.weight
196    }
197
198    /// 获取字形变换列表。
199    #[must_use]
200    pub fn get_cg_transforms(&self) -> &[CT_CGTransform] {
201        &self.cg_transforms
202    }
203
204    /// 获取文字定位列表。
205    #[must_use]
206    pub fn get_text_codes(&self) -> &[TextCode] {
207        &self.text_codes
208    }
209
210    /// 序列化为 OFD XML 字符串。
211    #[must_use]
212    pub fn to_xml_string(&self) -> String {
213        use std::fmt::Write;
214        let mut xml = format!(
215            "<ofd:TextObject ID=\"{}\" Boundary=\"{}\"",
216            self.id, self.boundary
217        );
218        if let Some(fr) = self.font_ref {
219            write!(xml, " Font=\"{fr}\"").expect("写入内存缓冲区不会失败");
220        }
221        if let Some(sz) = self.size {
222            write!(xml, " Size=\"{sz}\"").expect("写入内存缓冲区不会失败");
223        }
224        if self.stroke {
225            xml.push_str(" Stroke=\"true\"");
226        }
227        if !self.fill {
228            xml.push_str(" Fill=\"false\"");
229        }
230        if let Some(hs) = self.h_scale {
231            write!(xml, " HScale=\"{hs}\"").expect("写入内存缓冲区不会失败");
232        }
233        if let Some(rd) = self.read_direction {
234            write!(xml, " ReadDirection=\"{rd}\"").expect("写入内存缓冲区不会失败");
235        }
236        if let Some(cd) = self.char_direction {
237            write!(xml, " CharDirection=\"{cd}\"").expect("写入内存缓冲区不会失败");
238        }
239        if let Some(w) = self.weight {
240            write!(xml, " Weight=\"{w}\"").expect("写入内存缓冲区不会失败");
241        }
242        if self.italic {
243            xml.push_str(" Italic=\"true\"");
244        }
245        if let Some(fc) = self.fill_color {
246            write!(xml, " FillColor=\"{fc}\"").expect("写入内存缓冲区不会失败");
247        }
248        if let Some(sc) = self.stroke_color {
249            write!(xml, " StrokeColor=\"{sc}\"").expect("写入内存缓冲区不会失败");
250        }
251        xml.push_str(">\n");
252        for cg in &self.cg_transforms {
253            xml.push_str("  ");
254            xml.push_str(&cg.to_xml_string());
255            xml.push('\n');
256        }
257        for tc in &self.text_codes {
258            xml.push_str("  ");
259            xml.push_str(&tc.to_xml_string());
260            xml.push('\n');
261        }
262        xml.push_str("</ofd:TextObject>\n");
263        xml
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_ct_text_new() {
273        let t = CT_Text::new(1, "0 0 100 20");
274        assert_eq!(t.id, 1);
275        assert_eq!(t.boundary, "0 0 100 20");
276        assert!(t.font_ref.is_none());
277        assert!(t.fill);
278        assert!(!t.stroke);
279        assert!(!t.italic);
280    }
281
282    #[test]
283    fn test_ct_text_builder() {
284        let t = CT_Text::new(2, "10 20 50 15")
285            .font(3)
286            .size(14.0)
287            .weight(700)
288            .italic(true)
289            .fill_color(0xFF_0000)
290            .h_scale(1.2);
291        assert_eq!(t.get_font(), Some(3));
292        assert!((t.get_size().unwrap() - 14.0).abs() < f64::EPSILON);
293        assert_eq!(t.get_weight(), Some(700));
294        assert!(t.italic);
295        assert_eq!(t.fill_color, Some(0xFF_0000));
296        assert!((t.h_scale.unwrap() - 1.2).abs() < f64::EPSILON);
297    }
298
299    #[test]
300    fn test_ct_text_direction() {
301        let t = CT_Text::new(1, "0 0 10 10")
302            .read_direction(Direction::TopToBottom)
303            .char_direction(Direction::RightToLeft);
304        assert_eq!(t.read_direction, Some(90));
305        assert_eq!(t.char_direction, Some(180));
306    }
307
308    #[test]
309    fn test_ct_text_add_text_code() {
310        let mut t = CT_Text::new(1, "0 0 100 20");
311        t.add_text_code(TextCode::with_content("Hello").coordinate(0.0, 10.0));
312        t.add_text_code(TextCode::with_content("World").coordinate(30.0, 10.0));
313        assert_eq!(t.get_text_codes().len(), 2);
314    }
315
316    #[test]
317    fn test_ct_text_add_cg_transform() {
318        let mut t = CT_Text::new(1, "0 0 100 20");
319        t.add_cg_transform(
320            CT_CGTransform::new()
321                .code_position(0)
322                .code_count(1)
323                .glyph_count(1)
324                .glyphs(vec![42]),
325        );
326        assert_eq!(t.get_cg_transforms().len(), 1);
327    }
328
329    #[test]
330    fn test_direction_as_degrees() {
331        assert_eq!(Direction::LeftToRight.as_degrees(), 0);
332        assert_eq!(Direction::TopToBottom.as_degrees(), 90);
333        assert_eq!(Direction::RightToLeft.as_degrees(), 180);
334        assert_eq!(Direction::BottomToTop.as_degrees(), 270);
335    }
336
337    #[test]
338    fn test_ct_text_to_xml_basic() {
339        let t = CT_Text::new(1, "0 0 100 20");
340        let xml = t.to_xml_string();
341        assert!(xml.contains("ID=\"1\""));
342        assert!(xml.contains("Boundary=\"0 0 100 20\""));
343        assert!(xml.contains("<ofd:TextObject"));
344        assert!(xml.contains("</ofd:TextObject>"));
345    }
346
347    #[test]
348    fn test_ct_text_to_xml_full() {
349        let mut t = CT_Text::new(5, "10 20 200 30")
350            .font(3)
351            .size(12.0)
352            .weight(700)
353            .italic(true)
354            .stroke(true)
355            .fill(false)
356            .fill_color(0xFF_0000)
357            .stroke_color(0x00_FF00)
358            .read_direction(Direction::TopToBottom);
359        t.add_text_code(TextCode::with_content("test").coordinate(10.0, 30.0));
360        let xml = t.to_xml_string();
361        assert!(xml.contains("Font=\"3\""));
362        assert!(xml.contains("Size=\"12\""));
363        assert!(xml.contains("Weight=\"700\""));
364        assert!(xml.contains("Italic=\"true\""));
365        assert!(xml.contains("Stroke=\"true\""));
366        assert!(xml.contains("Fill=\"false\""));
367        assert!(xml.contains("FillColor=\"16711680\""));
368        assert!(xml.contains("StrokeColor=\"65280\""));
369        assert!(xml.contains("ReadDirection=\"90\""));
370        assert!(xml.contains("ofd:TextCode"));
371        assert!(xml.contains("test"));
372    }
373
374    #[test]
375    fn test_ct_text_clone_debug() {
376        let t = CT_Text::new(1, "0 0 1 1");
377        let t2 = t.clone();
378        assert_eq!(t2.id, 1);
379        assert!(format!("{t:?}").contains("CT_Text"));
380    }
381}