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    /// Unique identifier for this relationship within the package.
43    pub id: String,
44    /// The relationship type URI (e.g., `http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel`).
45    pub rel_type: String,
46    /// Target part path (e.g., `/3D/3dmodel.model`).
47    pub target: String,
48    /// Target mode: `"Internal"` for package-relative paths, `"External"` for absolute URIs.
49    pub target_mode: String,
50}
51
52/// Represents an OPC Content Type override or default.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum ContentType {
55    /// A default content type mapped to a file extension.
56    Default {
57        /// File extension (without the leading dot, e.g., `"model"`).
58        extension: String,
59        /// MIME content type string (e.g., `"application/vnd.ms-package.3dmanufacturing-3dmodel+xml"`).
60        content_type: String,
61    },
62    /// An explicit content type override for a specific part path.
63    Override {
64        /// Package-relative path of the part (e.g., `/3D/3dmodel.model`).
65        part_name: String,
66        /// MIME content type string for this specific part.
67        content_type: String,
68    },
69}
70
71/// Parses relationship file (e.g., _rels/.rels).
72pub fn parse_relationships(xml_content: &[u8]) -> Result<Vec<Relationship>> {
73    let mut reader = Reader::from_reader(xml_content);
74    reader.config_mut().trim_text(true);
75
76    let mut rels = Vec::new();
77    let mut buf = Vec::new();
78
79    loop {
80        match reader.read_event_into(&mut buf) {
81            Ok(Event::Empty(e)) | Ok(Event::Start(e)) => {
82                if e.name().as_ref() == b"Relationship" {
83                    let mut id = String::new();
84                    let mut rel_type = String::new();
85                    let mut target = String::new();
86                    let mut target_mode = "Internal".to_string(); // Default
87
88                    for attr in e.attributes() {
89                        let attr = attr.map_err(|e| Lib3mfError::Validation(e.to_string()))?;
90                        match attr.key.as_ref() {
91                            b"Id" => id = String::from_utf8_lossy(&attr.value).to_string(),
92                            b"Type" => rel_type = String::from_utf8_lossy(&attr.value).to_string(),
93                            b"Target" => target = String::from_utf8_lossy(&attr.value).to_string(),
94                            b"TargetMode" => {
95                                target_mode = String::from_utf8_lossy(&attr.value).to_string()
96                            }
97                            _ => {}
98                        }
99                    }
100                    rels.push(Relationship {
101                        id,
102                        rel_type,
103                        target,
104                        target_mode,
105                    });
106                }
107            }
108            Ok(Event::Eof) => break,
109            Err(e) => return Err(Lib3mfError::Validation(e.to_string())),
110            _ => {}
111        }
112        buf.clear();
113    }
114
115    Ok(rels)
116}
117
118/// Parses `[Content_Types].xml`.
119pub fn parse_content_types(xml_content: &[u8]) -> Result<Vec<ContentType>> {
120    let mut reader = Reader::from_reader(xml_content);
121    reader.config_mut().trim_text(true);
122
123    let mut types = Vec::new();
124    let mut buf = Vec::new();
125
126    loop {
127        match reader.read_event_into(&mut buf) {
128            Ok(Event::Empty(e)) | Ok(Event::Start(e)) => match e.name().as_ref() {
129                b"Default" => {
130                    let mut extension = String::new();
131                    let mut content_type = String::new();
132                    for attr in e.attributes() {
133                        let attr = attr.map_err(|e| Lib3mfError::Validation(e.to_string()))?;
134                        match attr.key.as_ref() {
135                            b"Extension" => {
136                                extension = String::from_utf8_lossy(&attr.value).to_string()
137                            }
138                            b"ContentType" => {
139                                content_type = String::from_utf8_lossy(&attr.value).to_string()
140                            }
141                            _ => {}
142                        }
143                    }
144                    types.push(ContentType::Default {
145                        extension,
146                        content_type,
147                    });
148                }
149                b"Override" => {
150                    let mut part_name = String::new();
151                    let mut content_type = String::new();
152                    for attr in e.attributes() {
153                        let attr = attr.map_err(|e| Lib3mfError::Validation(e.to_string()))?;
154                        match attr.key.as_ref() {
155                            b"PartName" => {
156                                part_name = String::from_utf8_lossy(&attr.value).to_string()
157                            }
158                            b"ContentType" => {
159                                content_type = String::from_utf8_lossy(&attr.value).to_string()
160                            }
161                            _ => {}
162                        }
163                    }
164                    types.push(ContentType::Override {
165                        part_name,
166                        content_type,
167                    });
168                }
169                _ => {}
170            },
171            Ok(Event::Eof) => break,
172            Err(e) => return Err(Lib3mfError::Validation(e.to_string())),
173            _ => {}
174        }
175        buf.clear();
176    }
177
178    Ok(types)
179}