Skip to main content

easyofd_core/composite_obj/
content.rs

1//! Content 矢量内容描述。
2//!
3//! 对应 GB/T 33190-2016 第 13.6 节中的 Content 类型。
4//! 矢量内容描述用于在矢量图形复合对象中定义具体的绘制图元,
5//! 包括路径、文字和图像等。
6
7/// 对应 Java: org.ofdrw.core.compositeObj.Content
8///
9/// 矢量内容描述。表示矢量图形中的单个绘制图元,
10/// 可以是路径、文本或图像。
11#[derive(Debug, Clone)]
12pub enum Content {
13    /// 路径绘制图元。
14    Path(PathContent),
15    /// 文本绘制图元。
16    Text(TextContent),
17    /// 图像绘制图元。
18    Image(ImageContent),
19}
20
21/// 路径图元内容。
22#[derive(Debug, Clone)]
23pub struct PathContent {
24    /// 路径数据(SVG 风格的缩略数据)。
25    pub data: String,
26    /// 描边颜色 RGB hex。
27    pub stroke_color: u32,
28    /// 线宽(mm)。
29    pub line_width: f64,
30    /// 填充颜色 RGB hex(可选)。
31    pub fill_color: Option<u32>,
32}
33
34/// 文本图元内容。
35#[derive(Debug, Clone)]
36pub struct TextContent {
37    /// 文本内容。
38    pub text: String,
39    /// X 坐标(mm)。
40    pub x: f64,
41    /// Y 坐标(mm)。
42    pub y: f64,
43    /// 字号(pt)。
44    pub font_size: f64,
45    /// 字体名称。
46    pub font: String,
47    /// 文本颜色 RGB hex。
48    pub color: u32,
49}
50
51/// 图像图元内容。
52#[derive(Debug, Clone)]
53pub struct ImageContent {
54    /// 图像数据(原始字节)。
55    pub data: Vec<u8>,
56    /// X 坐标(mm)。
57    pub x: f64,
58    /// Y 坐标(mm)。
59    pub y: f64,
60    /// 宽度(mm)。
61    pub width: f64,
62    /// 高度(mm)。
63    pub height: f64,
64}
65
66impl Content {
67    /// 创建路径内容。
68    #[must_use]
69    pub fn path(data: impl Into<String>) -> Self {
70        Self::Path(PathContent {
71            data: data.into(),
72            stroke_color: 0x00_0000,
73            line_width: 0.35,
74            fill_color: None,
75        })
76    }
77
78    /// 创建文本内容。
79    #[must_use]
80    pub fn text(text: impl Into<String>, x: f64, y: f64) -> Self {
81        Self::Text(TextContent {
82            text: text.into(),
83            x,
84            y,
85            font_size: 12.0,
86            font: "SimSun".into(),
87            color: 0x00_0000,
88        })
89    }
90
91    /// 创建图像内容。
92    #[must_use]
93    pub fn image(data: Vec<u8>, x: f64, y: f64, width: f64, height: f64) -> Self {
94        Self::Image(ImageContent {
95            data,
96            x,
97            y,
98            width,
99            height,
100        })
101    }
102
103    /// 是否为路径内容。
104    #[must_use]
105    pub fn is_path(&self) -> bool {
106        matches!(self, Self::Path(_))
107    }
108
109    /// 是否为文本内容。
110    #[must_use]
111    pub fn is_text(&self) -> bool {
112        matches!(self, Self::Text(_))
113    }
114
115    /// 是否为图像内容。
116    #[must_use]
117    pub fn is_image(&self) -> bool {
118        matches!(self, Self::Image(_))
119    }
120
121    /// 序列化为 OFD XML 字符串。
122    #[must_use]
123    pub fn to_xml_string(&self) -> String {
124        use std::fmt::Write;
125        match self {
126            Self::Path(p) => {
127                let mut xml = String::new();
128                write!(
129                    xml,
130                    "<ofd:PathObject StrokeColor=\"{}\" LineWidth=\"{}\"",
131                    p.stroke_color, p.line_width
132                )
133                .expect("写入内存缓冲区不会失败");
134                if let Some(fc) = p.fill_color {
135                    write!(xml, " FillColor=\"{fc}\"").expect("写入内存缓冲区不会失败");
136                }
137                writeln!(
138                    xml,
139                    "><ofd:AbbreviatedData>{}</ofd:AbbreviatedData></ofd:PathObject>",
140                    p.data
141                )
142                .expect("写入内存缓冲区不会失败");
143                xml
144            }
145            Self::Text(t) => {
146                format!(
147                    "<ofd:TextObject X=\"{}\" Y=\"{}\" FontSize=\"{}\" \
148                     Font=\"{}\" Color=\"{}\">{}</ofd:TextObject>\n",
149                    t.x, t.y, t.font_size, t.font, t.color, t.text
150                )
151            }
152            Self::Image(img) => {
153                format!(
154                    "<ofd:ImageObject X=\"{}\" Y=\"{}\" Width=\"{}\" Height=\"{}\" />\n",
155                    img.x, img.y, img.width, img.height
156                )
157            }
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_content_path() {
168        let c = Content::path("M0 0L10 10");
169        assert!(c.is_path());
170        assert!(!c.is_text());
171        assert!(!c.is_image());
172        if let Content::Path(p) = &c {
173            assert_eq!(p.data, "M0 0L10 10");
174            assert_eq!(p.stroke_color, 0x00_0000);
175            assert!((p.line_width - 0.35).abs() < f64::EPSILON);
176            assert!(p.fill_color.is_none());
177        }
178    }
179
180    #[test]
181    fn test_content_text() {
182        let c = Content::text("hello", 10.0, 20.0);
183        assert!(!c.is_path());
184        assert!(c.is_text());
185        assert!(!c.is_image());
186        if let Content::Text(t) = &c {
187            assert_eq!(t.text, "hello");
188            assert!((t.x - 10.0).abs() < f64::EPSILON);
189            assert!((t.y - 20.0).abs() < f64::EPSILON);
190            assert!((t.font_size - 12.0).abs() < f64::EPSILON);
191            assert_eq!(t.font, "SimSun");
192            assert_eq!(t.color, 0x00_0000);
193        }
194    }
195
196    #[test]
197    fn test_content_image() {
198        let c = Content::image(vec![0x89, 0x50], 0.0, 0.0, 50.0, 50.0);
199        assert!(!c.is_path());
200        assert!(!c.is_text());
201        assert!(c.is_image());
202        if let Content::Image(img) = &c {
203            assert_eq!(img.data, vec![0x89, 0x50]);
204            assert!((img.width - 50.0).abs() < f64::EPSILON);
205        }
206    }
207
208    #[test]
209    fn test_content_path_to_xml() {
210        let c = Content::path("M0 0Z");
211        let xml = c.to_xml_string();
212        assert!(xml.contains("ofd:PathObject"));
213        assert!(xml.contains("M0 0Z"));
214        assert!(xml.contains("StrokeColor=\"0\""));
215    }
216
217    #[test]
218    fn test_content_text_to_xml() {
219        let c = Content::text("test", 1.0, 2.0);
220        let xml = c.to_xml_string();
221        assert!(xml.contains("ofd:TextObject"));
222        assert!(xml.contains("test"));
223        assert!(xml.contains("X=\"1\""));
224        assert!(xml.contains("Y=\"2\""));
225    }
226
227    #[test]
228    fn test_content_image_to_xml() {
229        let c = Content::image(vec![1, 2, 3], 5.0, 10.0, 30.0, 40.0);
230        let xml = c.to_xml_string();
231        assert!(xml.contains("ofd:ImageObject"));
232        assert!(xml.contains("Width=\"30\""));
233        assert!(xml.contains("Height=\"40\""));
234    }
235
236    #[test]
237    fn test_content_clone_debug() {
238        let c = Content::path("M0 0");
239        let c2 = c.clone();
240        assert!(c2.is_path());
241        assert!(format!("{c:?}").contains("Path"));
242    }
243}