Skip to main content

easyofd_core/graph/
ct_path.rs

1//! CT_Path 路径对象。
2
3use super::AbbreviatedData;
4use crate::xml_element::{XmlElement, XmlElementError, XmlNode};
5
6/// 填充规则。
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum FillRule {
9    /// 非零绕组规则(默认)。
10    NonZero,
11    /// 奇偶规则。
12    EvenOdd,
13}
14
15impl FillRule {
16    /// 转为 OFD XML 属性值。
17    #[must_use]
18    pub fn as_str(&self) -> &'static str {
19        match self {
20            Self::NonZero => "NonZero",
21            Self::EvenOdd => "EvenOdd",
22        }
23    }
24}
25
26impl std::fmt::Display for FillRule {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.write_str(self.as_str())
29    }
30}
31
32/// 对应 Java: org.ofdrw.core.graph.pathObj.CT_Path
33///
34/// 路径对象,扩展自图形单元,包含填充规则、线宽、线帽、
35/// 填充/描边颜色和缩略路径数据等属性。
36/// 对应 GB/T 33190-2016 第 9.1 节图 46 表 35。
37#[allow(non_camel_case_types)]
38#[derive(Debug, Clone)]
39pub struct CT_Path {
40    /// 对象 ID。
41    pub id: u32,
42    /// 边界框 "x y width height"(单位 mm)。
43    pub boundary: String,
44    /// 是否描边。
45    pub stroke: bool,
46    /// 是否填充。
47    pub fill: bool,
48    /// 填充规则。
49    pub rule: FillRule,
50    /// 线宽(mm)。
51    pub line_width: Option<f64>,
52    /// 描边颜色 RGB hex。
53    pub stroke_color: Option<u32>,
54    /// 填充颜色 RGB hex。
55    pub fill_color: Option<u32>,
56    /// 路径缩写数据。
57    pub abbreviated_data: Option<AbbreviatedData>,
58}
59
60impl CT_Path {
61    /// 创建新的路径对象。
62    #[must_use]
63    pub fn new(id: u32, boundary: impl Into<String>) -> Self {
64        Self {
65            id,
66            boundary: boundary.into(),
67            stroke: true,
68            fill: false,
69            rule: FillRule::NonZero,
70            line_width: None,
71            stroke_color: None,
72            fill_color: None,
73            abbreviated_data: None,
74        }
75    }
76
77    /// 设置描边。
78    #[must_use]
79    pub fn stroke(mut self, stroke: bool) -> Self {
80        self.stroke = stroke;
81        self
82    }
83
84    /// 设置填充。
85    #[must_use]
86    pub fn fill(mut self, fill: bool) -> Self {
87        self.fill = fill;
88        self
89    }
90
91    /// 设置填充规则。
92    #[must_use]
93    pub fn rule(mut self, rule: FillRule) -> Self {
94        self.rule = rule;
95        self
96    }
97
98    /// 设置线宽。
99    #[must_use]
100    pub fn line_width(mut self, width: f64) -> Self {
101        self.line_width = Some(width);
102        self
103    }
104
105    /// 设置描边颜色。
106    #[must_use]
107    pub fn stroke_color(mut self, color: u32) -> Self {
108        self.stroke_color = Some(color);
109        self
110    }
111
112    /// 设置填充颜色。
113    #[must_use]
114    pub fn fill_color(mut self, color: u32) -> Self {
115        self.fill_color = Some(color);
116        self
117    }
118
119    /// 设置路径数据。
120    #[must_use]
121    pub fn abbreviated_data(mut self, data: AbbreviatedData) -> Self {
122        self.abbreviated_data = Some(data);
123        self
124    }
125
126    /// 获取路径数据字符串。
127    #[must_use]
128    pub fn get_abbreviated_data(&self) -> Option<String> {
129        self.abbreviated_data.as_ref().map(|d| d.to_data_string())
130    }
131
132    /// 获取描边颜色。
133    #[must_use]
134    pub fn get_stroke_color(&self) -> Option<u32> {
135        self.stroke_color
136    }
137
138    /// 获取填充颜色。
139    #[must_use]
140    pub fn get_fill_color(&self) -> Option<u32> {
141        self.fill_color
142    }
143
144    /// 序列化为 OFD XML 字符串。
145    #[must_use]
146    pub fn to_xml_string(&self) -> String {
147        use std::fmt::Write;
148        let mut xml = format!(
149            "<ofd:PathObject ID=\"{}\" Boundary=\"{}\"",
150            self.id, self.boundary
151        );
152        if !self.stroke {
153            xml.push_str(" Stroke=\"false\"");
154        }
155        if self.fill {
156            xml.push_str(" Fill=\"true\"");
157        }
158        if self.rule != FillRule::NonZero {
159            write!(xml, " Rule=\"{}\"", self.rule.as_str()).expect("写入内存缓冲区不会失败");
160        }
161        if let Some(lw) = self.line_width {
162            write!(xml, " LineWidth=\"{lw}\"").expect("写入内存缓冲区不会失败");
163        }
164        if let Some(sc) = self.stroke_color {
165            write!(xml, " StrokeColor=\"{sc}\"").expect("写入内存缓冲区不会失败");
166        }
167        if let Some(fc) = self.fill_color {
168            write!(xml, " FillColor=\"{fc}\"").expect("写入内存缓冲区不会失败");
169        }
170        if let Some(ref ad) = self.abbreviated_data {
171            xml.push_str(">\n  ");
172            xml.push_str(&ad.to_xml_string());
173            xml.push_str("\n</ofd:PathObject>\n");
174        } else {
175            xml.push_str(" />\n");
176        }
177        xml
178    }
179}
180
181impl XmlElement for CT_Path {
182    /// 对应 Java: CT_Path 元素名 "Path"。
183    fn element_name(&self) -> &'static str {
184        "Path"
185    }
186
187    fn attributes(&self) -> Vec<(String, String)> {
188        let mut attrs = Vec::new();
189        attrs.push(("ID".to_string(), self.id.to_string()));
190        attrs.push(("Boundary".to_string(), self.boundary.clone()));
191        if !self.stroke {
192            attrs.push(("Stroke".to_string(), "false".to_string()));
193        }
194        if self.fill {
195            attrs.push(("Fill".to_string(), "true".to_string()));
196        }
197        if self.rule != FillRule::NonZero {
198            attrs.push(("Rule".to_string(), self.rule.as_str().to_string()));
199        }
200        if let Some(lw) = self.line_width {
201            attrs.push(("LineWidth".to_string(), lw.to_string()));
202        }
203        if let Some(sc) = self.stroke_color {
204            attrs.push(("StrokeColor".to_string(), sc.to_string()));
205        }
206        if let Some(fc) = self.fill_color {
207            attrs.push(("FillColor".to_string(), fc.to_string()));
208        }
209        attrs
210    }
211
212    fn child_nodes(&self) -> Vec<XmlNode> {
213        let mut children = Vec::new();
214        if let Some(ref ad) = self.abbreviated_data {
215            let mut node = XmlNode::element("AbbreviatedData");
216            node.push_child(XmlNode::text_node(ad.to_data_string()));
217            children.push(node);
218        }
219        children
220    }
221
222    fn from_xml(node: &XmlNode) -> Result<Self, XmlElementError> {
223        let id = node
224            .get_attr("ID")
225            .ok_or_else(|| XmlElementError("Path 缺少 ID 属性".to_string()))?
226            .parse::<u32>()
227            .map_err(|e| XmlElementError(format!("解析 Path.ID 失败: {e}")))?;
228        let boundary = node
229            .get_attr("Boundary")
230            .ok_or_else(|| XmlElementError("Path 缺少 Boundary 属性".to_string()))?
231            .to_string();
232        let stroke = node.get_attr("Stroke").map_or(true, |s| s != "false");
233        let fill = node.get_attr("Fill").is_some_and(|s| s == "true");
234        let rule = match node.get_attr("Rule") {
235            Some("EvenOdd") => FillRule::EvenOdd,
236            _ => FillRule::NonZero,
237        };
238        let line_width = node
239            .get_attr("LineWidth")
240            .map(|s| {
241                s.parse::<f64>()
242                    .map_err(|e| XmlElementError(format!("解析 Path.LineWidth 失败: {e}")))
243            })
244            .transpose()?;
245        let stroke_color = node
246            .get_attr("StrokeColor")
247            .map(|s| {
248                s.parse::<u32>()
249                    .map_err(|e| XmlElementError(format!("解析 Path.StrokeColor 失败: {e}")))
250            })
251            .transpose()?;
252        let fill_color = node
253            .get_attr("FillColor")
254            .map(|s| {
255                s.parse::<u32>()
256                    .map_err(|e| XmlElementError(format!("解析 Path.FillColor 失败: {e}")))
257            })
258            .transpose()?;
259        let abbreviated_data = node.child("AbbreviatedData").map(|child| {
260            let text = child.text.as_deref().unwrap_or("");
261            AbbreviatedData::parse(text)
262        });
263        Ok(Self {
264            id,
265            boundary,
266            stroke,
267            fill,
268            rule,
269            line_width,
270            stroke_color,
271            fill_color,
272            abbreviated_data,
273        })
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::xml_parse::parse_xml_to_nodes;
281
282    #[test]
283    fn test_ct_path_new() {
284        let p = CT_Path::new(1, "0 0 100 50");
285        assert_eq!(p.id, 1);
286        assert!(p.stroke);
287        assert!(!p.fill);
288        assert_eq!(p.rule, FillRule::NonZero);
289        assert!(p.abbreviated_data.is_none());
290    }
291
292    #[test]
293    fn test_ct_path_builder() {
294        let p = CT_Path::new(2, "10 20 50 50")
295            .stroke(false)
296            .fill(true)
297            .rule(FillRule::EvenOdd)
298            .line_width(2.0)
299            .stroke_color(0xFF_0000)
300            .fill_color(0x00_FF00);
301        assert!(!p.stroke);
302        assert!(p.fill);
303        assert_eq!(p.rule, FillRule::EvenOdd);
304        assert!((p.line_width.unwrap() - 2.0).abs() < f64::EPSILON);
305        assert_eq!(p.stroke_color, Some(0xFF_0000));
306        assert_eq!(p.fill_color, Some(0x00_FF00));
307    }
308
309    #[test]
310    fn test_ct_path_with_data() {
311        let data = AbbreviatedData::new()
312            .move_to(0.0, 0.0)
313            .line_to(100.0, 100.0)
314            .close();
315        let p = CT_Path::new(3, "0 0 100 100").abbreviated_data(data);
316        assert!(p.abbreviated_data.is_some());
317        let data_str = p.get_abbreviated_data().unwrap();
318        assert!(data_str.contains("M 0 0"));
319        assert!(data_str.contains("L 100 100"));
320    }
321
322    #[test]
323    fn test_fill_rule_display() {
324        assert_eq!(FillRule::NonZero.to_string(), "NonZero");
325        assert_eq!(FillRule::EvenOdd.to_string(), "EvenOdd");
326    }
327
328    #[test]
329    fn test_ct_path_to_xml_basic() {
330        let p = CT_Path::new(1, "0 0 50 50");
331        let xml = p.to_xml_string();
332        assert!(xml.contains("ID=\"1\""));
333        assert!(xml.contains("Boundary=\"0 0 50 50\""));
334        assert!(xml.contains("<ofd:PathObject"));
335        assert!(xml.ends_with(" />\n"));
336    }
337
338    #[test]
339    fn test_ct_path_to_xml_with_data() {
340        let data = AbbreviatedData::new().move_to(0.0, 0.0).line_to(10.0, 10.0);
341        let p = CT_Path::new(5, "0 0 10 10")
342            .fill(true)
343            .stroke(false)
344            .fill_color(0x00_00FF)
345            .abbreviated_data(data);
346        let xml = p.to_xml_string();
347        assert!(xml.contains("Fill=\"true\""));
348        assert!(xml.contains("Stroke=\"false\""));
349        assert!(xml.contains("FillColor=\"255\""));
350        assert!(xml.contains("ofd:AbbreviatedData"));
351        assert!(xml.contains("M 0 0"));
352        assert!(xml.contains("L 10 10"));
353    }
354
355    #[test]
356    fn test_ct_path_to_xml_evenodd() {
357        let p = CT_Path::new(6, "0 0 10 10").rule(FillRule::EvenOdd);
358        let xml = p.to_xml_string();
359        assert!(xml.contains("Rule=\"EvenOdd\""));
360    }
361
362    #[test]
363    fn test_ct_path_clone_debug() {
364        let p = CT_Path::new(1, "0 0 1 1");
365        let p2 = p.clone();
366        assert_eq!(p2.id, 1);
367        assert!(format!("{p:?}").contains("CT_Path"));
368    }
369
370    #[test]
371    fn test_xml_element_name() {
372        let p = CT_Path::new(1, "0 0 1 1");
373        assert_eq!(p.element_name(), "Path");
374    }
375
376    #[test]
377    fn test_xml_element_to_xml_contains_attrs() {
378        let p = CT_Path::new(3, "10 20 50 50")
379            .stroke(false)
380            .fill(true)
381            .rule(FillRule::EvenOdd)
382            .line_width(2.5)
383            .stroke_color(0xFF_0000)
384            .fill_color(0x00_FF00);
385        let xml = p.to_xml();
386        assert!(xml.contains("ID=\"3\""));
387        assert!(xml.contains("Boundary=\"10 20 50 50\""));
388        assert!(xml.contains("Stroke=\"false\""));
389        assert!(xml.contains("Fill=\"true\""));
390        assert!(xml.contains("Rule=\"EvenOdd\""));
391        assert!(xml.contains("LineWidth=\"2.5\""));
392        assert!(xml.contains("StrokeColor=\"16711680\""));
393        assert!(xml.contains("FillColor=\"65280\""));
394    }
395
396    #[test]
397    fn test_xml_element_roundtrip_full() {
398        let data = AbbreviatedData::new()
399            .move_to(0.0, 0.0)
400            .line_to(10.0, 10.0)
401            .close();
402        let p = CT_Path::new(5, "0 0 100 100")
403            .stroke(false)
404            .fill(true)
405            .rule(FillRule::EvenOdd)
406            .line_width(2.0)
407            .stroke_color(0xFF_0000)
408            .fill_color(0x00_FF00)
409            .abbreviated_data(data);
410        let xml = p.to_xml();
411        let node = parse_xml_to_nodes(&xml).unwrap();
412        let p2 = CT_Path::from_xml(&node).unwrap();
413        assert_eq!(p.id, p2.id);
414        assert_eq!(p.boundary, p2.boundary);
415        assert_eq!(p.stroke, p2.stroke);
416        assert_eq!(p.fill, p2.fill);
417        assert_eq!(p.rule, p2.rule);
418        assert_eq!(p.line_width, p2.line_width);
419        assert_eq!(p.stroke_color, p2.stroke_color);
420        assert_eq!(p.fill_color, p2.fill_color);
421        assert_eq!(p.get_abbreviated_data(), p2.get_abbreviated_data());
422    }
423
424    #[test]
425    fn test_xml_element_roundtrip_minimal() {
426        let p = CT_Path::new(1, "0 0 10 10");
427        let xml = p.to_xml();
428        let node = parse_xml_to_nodes(&xml).unwrap();
429        let p2 = CT_Path::from_xml(&node).unwrap();
430        assert_eq!(p.id, p2.id);
431        assert_eq!(p.boundary, p2.boundary);
432        assert!(p2.stroke);
433        assert!(!p2.fill);
434    }
435}