Skip to main content

easyofd_core/page_obj/
ct_graphic_unit.rs

1//! CT_GraphicUnit 图形单元基类。
2
3/// 对应 Java: org.ofdrw.core.pageDescription.CT_GraphicUnit
4///
5/// 图元对象是版式文档中页面上呈现内容的最基本单元。
6/// 所有页面显示内容(文字、图形、图像等)都属于图元对象,
7/// 或是图元对象的组合。对应 GB/T 33190-2016 第 8.5 节图 45 表 34。
8#[allow(non_camel_case_types)]
9#[derive(Debug, Clone)]
10pub struct CT_GraphicUnit {
11    /// 对象 ID,在页面内唯一。
12    pub id: u32,
13    /// 对象边界框 "topLeftX topLeftY width height"(单位 mm)。
14    pub boundary: String,
15    /// 对象名称(可选),用于标识图元。
16    pub name: Option<String>,
17    /// 可见性。true 表示可见(默认),false 表示隐藏。
18    pub visible: bool,
19    /// 变换矩阵(可选),6 个元素的仿射变换矩阵。
20    pub ctm: Option<[f64; 6]>,
21    /// 绘制参数引用 ID(可选)。
22    pub draw_param: Option<u32>,
23    /// 线宽(mm)。
24    pub line_width: Option<f64>,
25    /// 线端帽类型。
26    pub cap: Option<LineCapType>,
27    /// 线连接类型。
28    pub join: Option<LineJoinType>,
29    /// 斜接限制。
30    pub miter_limit: Option<f64>,
31    /// 虚线偏移。
32    pub dash_offset: Option<f64>,
33    /// 虚线模式(如 "4 2" 表示 4mm 实线 2mm 间隔)。
34    pub dash_pattern: Option<String>,
35    /// 透明度 (0-255),255 表示完全不透明。
36    pub alpha: Option<u8>,
37}
38
39/// 线端帽类型。
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum LineCapType {
42    /// 平头(默认)。
43    Butt,
44    /// 圆头。
45    Round,
46    /// 方头。
47    Square,
48}
49
50impl LineCapType {
51    /// 转为 OFD XML 属性值。
52    #[must_use]
53    pub fn as_str(&self) -> &'static str {
54        match self {
55            Self::Butt => "Butt",
56            Self::Round => "Round",
57            Self::Square => "Square",
58        }
59    }
60}
61
62impl std::fmt::Display for LineCapType {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.write_str(self.as_str())
65    }
66}
67
68/// 线连接类型。
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum LineJoinType {
71    /// 尖角(默认)。
72    Miter,
73    /// 圆角。
74    Round,
75    /// 平角。
76    Bevel,
77}
78
79impl LineJoinType {
80    /// 转为 OFD XML 属性值。
81    #[must_use]
82    pub fn as_str(&self) -> &'static str {
83        match self {
84            Self::Miter => "Miter",
85            Self::Round => "Round",
86            Self::Bevel => "Bevel",
87        }
88    }
89}
90
91impl std::fmt::Display for LineJoinType {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        f.write_str(self.as_str())
94    }
95}
96
97impl CT_GraphicUnit {
98    /// 创建新的图形单元。
99    #[must_use]
100    pub fn new(id: u32, boundary: impl Into<String>) -> Self {
101        Self {
102            id,
103            boundary: boundary.into(),
104            name: None,
105            visible: true,
106            ctm: None,
107            draw_param: None,
108            line_width: None,
109            cap: None,
110            join: None,
111            miter_limit: None,
112            dash_offset: None,
113            dash_pattern: None,
114            alpha: None,
115        }
116    }
117
118    /// 设置对象名称。
119    #[must_use]
120    pub fn name(mut self, name: impl Into<String>) -> Self {
121        self.name = Some(name.into());
122        self
123    }
124
125    /// 设置可见性。
126    #[must_use]
127    pub fn visible(mut self, visible: bool) -> Self {
128        self.visible = visible;
129        self
130    }
131
132    /// 设置变换矩阵。
133    #[must_use]
134    pub fn ctm(mut self, ctm: [f64; 6]) -> Self {
135        self.ctm = Some(ctm);
136        self
137    }
138
139    /// 设置绘制参数引用。
140    #[must_use]
141    pub fn draw_param(mut self, id: u32) -> Self {
142        self.draw_param = Some(id);
143        self
144    }
145
146    /// 设置线宽。
147    #[must_use]
148    pub fn line_width(mut self, width: f64) -> Self {
149        self.line_width = Some(width);
150        self
151    }
152
153    /// 设置线端帽类型。
154    #[must_use]
155    pub fn cap(mut self, cap: LineCapType) -> Self {
156        self.cap = Some(cap);
157        self
158    }
159
160    /// 设置线连接类型。
161    #[must_use]
162    pub fn join(mut self, join: LineJoinType) -> Self {
163        self.join = Some(join);
164        self
165    }
166
167    /// 设置斜接限制。
168    #[must_use]
169    pub fn miter_limit(mut self, limit: f64) -> Self {
170        self.miter_limit = Some(limit);
171        self
172    }
173
174    /// 设置虚线偏移。
175    #[must_use]
176    pub fn dash_offset(mut self, offset: f64) -> Self {
177        self.dash_offset = Some(offset);
178        self
179    }
180
181    /// 设置虚线模式。
182    #[must_use]
183    pub fn dash_pattern(mut self, pattern: impl Into<String>) -> Self {
184        self.dash_pattern = Some(pattern.into());
185        self
186    }
187
188    /// 设置透明度 (0-255)。
189    #[must_use]
190    pub fn alpha(mut self, alpha: u8) -> Self {
191        self.alpha = Some(alpha);
192        self
193    }
194
195    /// 获取对象 ID。
196    #[must_use]
197    pub fn get_id(&self) -> u32 {
198        self.id
199    }
200
201    /// 获取边界框。
202    #[must_use]
203    pub fn get_boundary(&self) -> &str {
204        &self.boundary
205    }
206
207    /// 获取对象名称。
208    #[must_use]
209    pub fn get_name(&self) -> Option<&str> {
210        self.name.as_deref()
211    }
212
213    /// 获取可见性。
214    #[must_use]
215    pub fn get_visible(&self) -> bool {
216        self.visible
217    }
218
219    /// 获取变换矩阵。
220    #[must_use]
221    pub fn get_ctm(&self) -> Option<[f64; 6]> {
222        self.ctm
223    }
224
225    /// 获取透明度。
226    #[must_use]
227    pub fn get_alpha(&self) -> Option<u8> {
228        self.alpha
229    }
230
231    /// 序列化为 OFD XML 字符串。
232    #[must_use]
233    pub fn to_xml_string(&self) -> String {
234        use std::fmt::Write;
235        let mut xml = format!(
236            "<ofd:CT_GraphicUnit ID=\"{}\" Boundary=\"{}\"",
237            self.id, self.boundary
238        );
239        if let Some(ref name) = self.name {
240            write!(xml, " Name=\"{name}\"").expect("写入内存缓冲区不会失败");
241        }
242        if !self.visible {
243            xml.push_str(" Visible=\"false\"");
244        }
245        if let Some(ctm) = self.ctm {
246            write!(
247                xml,
248                " CTM=\"{} {} {} {} {} {}\"",
249                ctm[0], ctm[1], ctm[2], ctm[3], ctm[4], ctm[5]
250            )
251            .expect("写入内存缓冲区不会失败");
252        }
253        if let Some(dp) = self.draw_param {
254            write!(xml, " DrawParam=\"{dp}\"").expect("写入内存缓冲区不会失败");
255        }
256        if let Some(lw) = self.line_width {
257            write!(xml, " LineWidth=\"{lw}\"").expect("写入内存缓冲区不会失败");
258        }
259        if let Some(cap) = self.cap {
260            write!(xml, " Cap=\"{}\"", cap.as_str()).expect("写入内存缓冲区不会失败");
261        }
262        if let Some(join) = self.join {
263            write!(xml, " Join=\"{}\"", join.as_str()).expect("写入内存缓冲区不会失败");
264        }
265        if let Some(ml) = self.miter_limit {
266            write!(xml, " MiterLimit=\"{ml}\"").expect("写入内存缓冲区不会失败");
267        }
268        if let Some(doff) = self.dash_offset {
269            write!(xml, " DashOffset=\"{doff}\"").expect("写入内存缓冲区不会失败");
270        }
271        if let Some(ref dp) = self.dash_pattern {
272            write!(xml, " DashPattern=\"{dp}\"").expect("写入内存缓冲区不会失败");
273        }
274        if let Some(a) = self.alpha {
275            write!(xml, " Alpha=\"{a}\"").expect("写入内存缓冲区不会失败");
276        }
277        xml.push_str(" />");
278        xml
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_ct_graphic_unit_new() {
288        let gu = CT_GraphicUnit::new(1, "0 0 100 50");
289        assert_eq!(gu.id, 1);
290        assert_eq!(gu.boundary, "0 0 100 50");
291        assert!(gu.name.is_none());
292        assert!(gu.visible);
293        assert!(gu.ctm.is_none());
294        assert!(gu.alpha.is_none());
295    }
296
297    #[test]
298    fn test_ct_graphic_unit_builder_chaining() {
299        let gu = CT_GraphicUnit::new(2, "10 20 50 50")
300            .name("rect1")
301            .visible(false)
302            .line_width(1.5)
303            .cap(LineCapType::Round)
304            .join(LineJoinType::Bevel)
305            .alpha(128);
306        assert_eq!(gu.get_name(), Some("rect1"));
307        assert!(!gu.get_visible());
308        assert!((gu.line_width.unwrap() - 1.5).abs() < f64::EPSILON);
309        assert_eq!(gu.cap, Some(LineCapType::Round));
310        assert_eq!(gu.join, Some(LineJoinType::Bevel));
311        assert_eq!(gu.get_alpha(), Some(128));
312    }
313
314    #[test]
315    fn test_ct_graphic_unit_ctm() {
316        let gu = CT_GraphicUnit::new(3, "0 0 10 10").ctm([1.0, 0.0, 0.0, 1.0, 5.0, 5.0]);
317        let ctm = gu.get_ctm().unwrap();
318        assert!((ctm[4] - 5.0).abs() < f64::EPSILON);
319    }
320
321    #[test]
322    fn test_line_cap_type_display() {
323        assert_eq!(LineCapType::Butt.to_string(), "Butt");
324        assert_eq!(LineCapType::Round.to_string(), "Round");
325        assert_eq!(LineCapType::Square.to_string(), "Square");
326    }
327
328    #[test]
329    fn test_line_join_type_display() {
330        assert_eq!(LineJoinType::Miter.to_string(), "Miter");
331        assert_eq!(LineJoinType::Round.to_string(), "Round");
332        assert_eq!(LineJoinType::Bevel.to_string(), "Bevel");
333    }
334
335    #[test]
336    fn test_ct_graphic_unit_to_xml_minimal() {
337        let gu = CT_GraphicUnit::new(1, "0 0 100 50");
338        let xml = gu.to_xml_string();
339        assert!(xml.contains("ID=\"1\""));
340        assert!(xml.contains("Boundary=\"0 0 100 50\""));
341        assert!(xml.contains("<ofd:CT_GraphicUnit"));
342        assert!(xml.ends_with(" />"));
343    }
344
345    #[test]
346    fn test_ct_graphic_unit_to_xml_full() {
347        let gu = CT_GraphicUnit::new(5, "0 0 200 100")
348            .name("myUnit")
349            .visible(false)
350            .ctm([2.0, 0.0, 0.0, 2.0, 10.0, 20.0])
351            .line_width(0.5)
352            .cap(LineCapType::Square)
353            .join(LineJoinType::Miter)
354            .miter_limit(4.0)
355            .dash_offset(1.0)
356            .dash_pattern("4 2")
357            .alpha(200);
358        let xml = gu.to_xml_string();
359        assert!(xml.contains("Name=\"myUnit\""));
360        assert!(xml.contains("Visible=\"false\""));
361        assert!(xml.contains("CTM=\"2 0 0 2 10 20\""));
362        assert!(xml.contains("LineWidth=\"0.5\""));
363        assert!(xml.contains("Cap=\"Square\""));
364        assert!(xml.contains("Join=\"Miter\""));
365        assert!(xml.contains("MiterLimit=\"4\""));
366        assert!(xml.contains("DashOffset=\"1\""));
367        assert!(xml.contains("DashPattern=\"4 2\""));
368        assert!(xml.contains("Alpha=\"200\""));
369    }
370
371    #[test]
372    fn test_ct_graphic_unit_clone_debug() {
373        let gu = CT_GraphicUnit::new(1, "0 0 1 1");
374        let gu2 = gu.clone();
375        assert_eq!(gu2.id, 1);
376        assert!(format!("{gu:?}").contains("CT_GraphicUnit"));
377    }
378}