Skip to main content

lib3mf_core/archive/
opc.rs

1use crate::error::{Lib3mfError, Result};
2use quick_xml::events::Event;
3use quick_xml::reader::Reader;
4use serde::{Deserialize, Serialize};
5
6/// Bambu Studio OPC relationship type constants.
7///
8/// These URIs appear in `_rels/*.rels` files within Bambu Studio 3MF archives
9/// to identify vendor-specific relationships to thumbnails and embedded G-code.
10///
11/// # Example
12///
13/// ```ignore
14/// use lib3mf_core::archive::opc::bambu_rel_types;
15///
16/// let is_bambu_thumbnail = rel.rel_type == bambu_rel_types::COVER_THUMBNAIL_MIDDLE;
17/// ```
18pub mod bambu_rel_types {
19    /// Relationship type for the medium-size cover thumbnail image.
20    ///
21    /// Targets a PNG or similar image file used as the model's display thumbnail
22    /// in Bambu Studio's file browser.
23    pub const COVER_THUMBNAIL_MIDDLE: &str =
24        "http://schemas.bambulab.com/package/2021/cover-thumbnail-middle";
25
26    /// Relationship type for the small cover thumbnail image.
27    ///
28    /// Targets a small PNG image suitable for grid/icon views in Bambu Studio.
29    pub const COVER_THUMBNAIL_SMALL: &str =
30        "http://schemas.bambulab.com/package/2021/cover-thumbnail-small";
31
32    /// Relationship type for embedded G-code.
33    ///
34    /// Targets a `.gcode` file embedded in the archive. When present, the file
35    /// contains pre-sliced G-code that can be sent directly to a Bambu printer.
36    pub const GCODE: &str = "http://schemas.bambulab.com/package/2021/gcode";
37}
38
39/// Represents an OPC Relationship.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Relationship {
42    pub id: String,
43    pub rel_type: String,
44    pub target: String,
45    pub target_mode: String,
46}
47
48/// Represents an OPC Content Type override or default.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub enum ContentType {
51    Default {
52        extension: String,
53        content_type: String,
54    },
55    Override {
56        part_name: String,
57        content_type: String,
58    },
59}
60
61/// Parses relationship file (e.g., _rels/.rels).
62pub fn parse_relationships(xml_content: &[u8]) -> Result<Vec<Relationship>> {
63    let mut reader = Reader::from_reader(xml_content);
64    reader.config_mut().trim_text(true);
65
66    let mut rels = Vec::new();
67    let mut buf = Vec::new();
68
69    loop {
70        match reader.read_event_into(&mut buf) {
71            Ok(Event::Empty(e)) | Ok(Event::Start(e)) => {
72                if e.name().as_ref() == b"Relationship" {
73                    let mut id = String::new();
74                    let mut rel_type = String::new();
75                    let mut target = String::new();
76                    let mut target_mode = "Internal".to_string(); // Default
77
78                    for attr in e.attributes() {
79                        let attr = attr.map_err(|e| Lib3mfError::Validation(e.to_string()))?;
80                        match attr.key.as_ref() {
81                            b"Id" => id = String::from_utf8_lossy(&attr.value).to_string(),
82                            b"Type" => rel_type = String::from_utf8_lossy(&attr.value).to_string(),
83                            b"Target" => target = String::from_utf8_lossy(&attr.value).to_string(),
84                            b"TargetMode" => {
85                                target_mode = String::from_utf8_lossy(&attr.value).to_string()
86                            }
87                            _ => {}
88                        }
89                    }
90                    rels.push(Relationship {
91                        id,
92                        rel_type,
93                        target,
94                        target_mode,
95                    });
96                }
97            }
98            Ok(Event::Eof) => break,
99            Err(e) => return Err(Lib3mfError::Validation(e.to_string())),
100            _ => {}
101        }
102        buf.clear();
103    }
104
105    Ok(rels)
106}
107
108/// Parses `[Content_Types].xml`.
109pub fn parse_content_types(xml_content: &[u8]) -> Result<Vec<ContentType>> {
110    let mut reader = Reader::from_reader(xml_content);
111    reader.config_mut().trim_text(true);
112
113    let mut types = Vec::new();
114    let mut buf = Vec::new();
115
116    loop {
117        match reader.read_event_into(&mut buf) {
118            Ok(Event::Empty(e)) | Ok(Event::Start(e)) => match e.name().as_ref() {
119                b"Default" => {
120                    let mut extension = String::new();
121                    let mut content_type = String::new();
122                    for attr in e.attributes() {
123                        let attr = attr.map_err(|e| Lib3mfError::Validation(e.to_string()))?;
124                        match attr.key.as_ref() {
125                            b"Extension" => {
126                                extension = String::from_utf8_lossy(&attr.value).to_string()
127                            }
128                            b"ContentType" => {
129                                content_type = String::from_utf8_lossy(&attr.value).to_string()
130                            }
131                            _ => {}
132                        }
133                    }
134                    types.push(ContentType::Default {
135                        extension,
136                        content_type,
137                    });
138                }
139                b"Override" => {
140                    let mut part_name = String::new();
141                    let mut content_type = String::new();
142                    for attr in e.attributes() {
143                        let attr = attr.map_err(|e| Lib3mfError::Validation(e.to_string()))?;
144                        match attr.key.as_ref() {
145                            b"PartName" => {
146                                part_name = String::from_utf8_lossy(&attr.value).to_string()
147                            }
148                            b"ContentType" => {
149                                content_type = String::from_utf8_lossy(&attr.value).to_string()
150                            }
151                            _ => {}
152                        }
153                    }
154                    types.push(ContentType::Override {
155                        part_name,
156                        content_type,
157                    });
158                }
159                _ => {}
160            },
161            Ok(Event::Eof) => break,
162            Err(e) => return Err(Lib3mfError::Validation(e.to_string())),
163            _ => {}
164        }
165        buf.clear();
166    }
167
168    Ok(types)
169}