Skip to main content

easyofd_core/composite_obj/
ct_composite.rs

1//! CT_Composite 复合对象容器。
2//!
3//! 对应 GB/T 33190-2016 第 13.6 节中的 CT_Composite 类型。
4//! 复合对象是页面对象的容器,可以包含多个子对象(文本、图像、路径等),
5//! 实现对象的分组和整体变换。
6
7/// 对应 Java: org.ofdrw.core.compositeObj.CT_Composite
8///
9/// 复合对象容器。用于将多个页面对象组合为一个逻辑单元,
10/// 支持统一的位置变换、裁剪等操作。
11#[allow(non_camel_case_types)]
12#[derive(Debug, Clone)]
13pub struct CT_Composite {
14    /// 对象 ID,在页面内唯一。
15    pub id: u32,
16    /// 对象边界框,格式为 "x y width height"(单位 mm)。
17    pub boundary: String,
18    /// 对象名称(可选),用于标识复合对象。
19    pub name: Option<String>,
20    /// 可见性。true 表示可见(默认),false 表示隐藏。
21    pub visible: bool,
22    /// 子对象列表,按绘制顺序排列。
23    pub children: Vec<CompositeChild>,
24}
25
26/// 复合对象中的子对象。
27///
28/// 子对象可以是文本、路径或其他复合对象。
29#[derive(Debug, Clone)]
30pub enum CompositeChild {
31    /// 文本子对象(简化表示)。
32    Text {
33        /// 文本内容。
34        content: String,
35        /// X 坐标(mm)。
36        x: f64,
37        /// Y 坐标(mm)。
38        y: f64,
39        /// 字号(pt)。
40        font_size: f64,
41    },
42    /// 路径子对象(简化表示)。
43    Path {
44        /// 路径数据(SVG 风格)。
45        data: String,
46        /// 描边颜色 RGB hex。
47        stroke_color: u32,
48    },
49    /// 嵌套复合对象。
50    Composite(CT_Composite),
51}
52
53impl CT_Composite {
54    /// 创建新的复合对象容器。
55    #[must_use]
56    pub fn new(id: u32, boundary: impl Into<String>) -> Self {
57        Self {
58            id,
59            boundary: boundary.into(),
60            name: None,
61            visible: true,
62            children: Vec::new(),
63        }
64    }
65
66    /// 设置对象名称。
67    #[must_use]
68    pub fn name(mut self, name: impl Into<String>) -> Self {
69        self.name = Some(name.into());
70        self
71    }
72
73    /// 设置可见性。
74    #[must_use]
75    pub fn visible(mut self, visible: bool) -> Self {
76        self.visible = visible;
77        self
78    }
79
80    /// 添加文本子对象。
81    pub fn add_text(&mut self, content: impl Into<String>, x: f64, y: f64, font_size: f64) {
82        self.children.push(CompositeChild::Text {
83            content: content.into(),
84            x,
85            y,
86            font_size,
87        });
88    }
89
90    /// 添加路径子对象。
91    pub fn add_path(&mut self, data: impl Into<String>, stroke_color: u32) {
92        self.children.push(CompositeChild::Path {
93            data: data.into(),
94            stroke_color,
95        });
96    }
97
98    /// 添加嵌套复合对象。
99    pub fn add_composite(&mut self, child: CT_Composite) {
100        self.children.push(CompositeChild::Composite(child));
101    }
102
103    /// 子对象数量。
104    #[must_use]
105    pub fn child_count(&self) -> usize {
106        self.children.len()
107    }
108
109    /// 序列化为 OFD XML 字符串。
110    #[must_use]
111    pub fn to_xml_string(&self) -> String {
112        use std::fmt::Write;
113        let mut xml = format!(
114            "<ofd:CT_Composite ID=\"{}\" Boundary=\"{}\"",
115            self.id, self.boundary
116        );
117        if let Some(ref name) = self.name {
118            write!(xml, " Name=\"{name}\"").expect("写入内存缓冲区不会失败");
119        }
120        if !self.visible {
121            xml.push_str(" Visible=\"false\"");
122        }
123        xml.push_str(">\n");
124        for child in &self.children {
125            xml.push_str(&child.to_xml_string());
126        }
127        xml.push_str("</ofd:CT_Composite>\n");
128        xml
129    }
130}
131
132impl CompositeChild {
133    /// 序列化子对象为 XML 字符串。
134    #[must_use]
135    pub fn to_xml_string(&self) -> String {
136        match self {
137            Self::Text {
138                content,
139                x,
140                y,
141                font_size,
142            } => {
143                format!(
144                    "  <ofd:TextObject X=\"{x}\" Y=\"{y}\" FontSize=\"{font_size}\">\
145                     {content}</ofd:TextObject>\n"
146                )
147            }
148            Self::Path { data, stroke_color } => {
149                format!(
150                    "  <ofd:PathObject StrokeColor=\"{stroke_color}\">\
151                     <ofd:AbbreviatedData>{data}</ofd:AbbreviatedData>\
152                     </ofd:PathObject>\n"
153                )
154            }
155            Self::Composite(inner) => {
156                // Indent nested composite.
157                use std::fmt::Write;
158                let inner_xml = inner.to_xml_string();
159                let mut out = String::new();
160                for line in inner_xml.lines() {
161                    writeln!(out, "  {line}").expect("写入内存缓冲区不会失败");
162                }
163                out
164            }
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_ct_composite_new() {
175        let c = CT_Composite::new(1, "0 0 100 100");
176        assert_eq!(c.id, 1);
177        assert_eq!(c.boundary, "0 0 100 100");
178        assert!(c.name.is_none());
179        assert!(c.visible);
180        assert!(c.children.is_empty());
181    }
182
183    #[test]
184    fn test_ct_composite_builder() {
185        let c = CT_Composite::new(2, "10 20 50 50")
186            .name("group1")
187            .visible(false);
188        assert_eq!(c.name.as_deref(), Some("group1"));
189        assert!(!c.visible);
190    }
191
192    #[test]
193    fn test_ct_composite_add_children() {
194        let mut c = CT_Composite::new(3, "0 0 200 200");
195        c.add_text("hello", 10.0, 20.0, 12.0);
196        c.add_path("M0 0L10 10", 0x00_0000);
197        assert_eq!(c.child_count(), 2);
198    }
199
200    #[test]
201    fn test_ct_composite_nested() {
202        let inner = CT_Composite::new(4, "5 5 10 10");
203        let mut outer = CT_Composite::new(5, "0 0 100 100");
204        outer.add_composite(inner);
205        assert_eq!(outer.child_count(), 1);
206    }
207
208    #[test]
209    fn test_ct_composite_to_xml_string() {
210        let c = CT_Composite::new(10, "0 0 50 50").name("myGroup");
211        let xml = c.to_xml_string();
212        assert!(xml.contains("ID=\"10\""));
213        assert!(xml.contains("Boundary=\"0 0 50 50\""));
214        assert!(xml.contains("Name=\"myGroup\""));
215        assert!(xml.contains("<ofd:CT_Composite"));
216        assert!(xml.contains("</ofd:CT_Composite>"));
217    }
218
219    #[test]
220    fn test_ct_composite_to_xml_with_children() {
221        let mut c = CT_Composite::new(11, "0 0 100 100");
222        c.add_text("test", 1.0, 2.0, 14.0);
223        c.add_path("M0 0", 0xFF_0000);
224        let xml = c.to_xml_string();
225        assert!(xml.contains("ofd:TextObject"));
226        assert!(xml.contains("ofd:PathObject"));
227        assert!(xml.contains("test"));
228    }
229
230    #[test]
231    fn test_ct_composite_to_xml_hidden() {
232        let c = CT_Composite::new(12, "0 0 10 10").visible(false);
233        let xml = c.to_xml_string();
234        assert!(xml.contains("Visible=\"false\""));
235    }
236
237    #[test]
238    fn test_ct_composite_clone_debug() {
239        let c = CT_Composite::new(1, "0 0 1 1");
240        let c2 = c.clone();
241        assert_eq!(c2.id, 1);
242        assert!(format!("{c:?}").contains("CT_Composite"));
243    }
244}