1use super::{CT_CGTransform, TextCode};
4
5#[allow(non_camel_case_types)]
10#[derive(Debug, Clone)]
11pub struct CT_Text {
12 pub id: u32,
14 pub boundary: String,
16 pub font_ref: Option<u32>,
18 pub size: Option<f64>,
20 pub stroke: bool,
22 pub fill: bool,
24 pub h_scale: Option<f64>,
26 pub read_direction: Option<u32>,
28 pub char_direction: Option<u32>,
30 pub weight: Option<u32>,
32 pub italic: bool,
34 pub fill_color: Option<u32>,
36 pub stroke_color: Option<u32>,
38 pub cg_transforms: Vec<CT_CGTransform>,
40 pub text_codes: Vec<TextCode>,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Direction {
47 LeftToRight,
49 TopToBottom,
51 RightToLeft,
53 BottomToTop,
55}
56
57impl Direction {
58 #[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 #[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 #[must_use]
95 pub fn font(mut self, ref_id: u32) -> Self {
96 self.font_ref = Some(ref_id);
97 self
98 }
99
100 #[must_use]
102 pub fn size(mut self, size: f64) -> Self {
103 self.size = Some(size);
104 self
105 }
106
107 #[must_use]
109 pub fn stroke(mut self, stroke: bool) -> Self {
110 self.stroke = stroke;
111 self
112 }
113
114 #[must_use]
116 pub fn fill(mut self, fill: bool) -> Self {
117 self.fill = fill;
118 self
119 }
120
121 #[must_use]
123 pub fn h_scale(mut self, scale: f64) -> Self {
124 self.h_scale = Some(scale);
125 self
126 }
127
128 #[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 #[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 #[must_use]
144 pub fn weight(mut self, weight: u32) -> Self {
145 self.weight = Some(weight);
146 self
147 }
148
149 #[must_use]
151 pub fn italic(mut self, italic: bool) -> Self {
152 self.italic = italic;
153 self
154 }
155
156 #[must_use]
158 pub fn fill_color(mut self, color: u32) -> Self {
159 self.fill_color = Some(color);
160 self
161 }
162
163 #[must_use]
165 pub fn stroke_color(mut self, color: u32) -> Self {
166 self.stroke_color = Some(color);
167 self
168 }
169
170 pub fn add_cg_transform(&mut self, cg: CT_CGTransform) {
172 self.cg_transforms.push(cg);
173 }
174
175 pub fn add_text_code(&mut self, tc: TextCode) {
177 self.text_codes.push(tc);
178 }
179
180 #[must_use]
182 pub fn get_font(&self) -> Option<u32> {
183 self.font_ref
184 }
185
186 #[must_use]
188 pub fn get_size(&self) -> Option<f64> {
189 self.size
190 }
191
192 #[must_use]
194 pub fn get_weight(&self) -> Option<u32> {
195 self.weight
196 }
197
198 #[must_use]
200 pub fn get_cg_transforms(&self) -> &[CT_CGTransform] {
201 &self.cg_transforms
202 }
203
204 #[must_use]
206 pub fn get_text_codes(&self) -> &[TextCode] {
207 &self.text_codes
208 }
209
210 #[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}