Skip to main content

lib3mf_core/writer/
package_writer.rs

1use crate::error::{Lib3mfError, Result};
2use crate::model::Package;
3use crate::writer::opc_writer::{write_content_types, write_relationships};
4use std::io::{Seek, Write};
5use zip::ZipWriter;
6use zip::write::FileOptions;
7
8/// A writer that orchestrates the creation of a 3MF package (ZIP archive).
9pub struct PackageWriter<W: Write + Seek> {
10    zip: ZipWriter<W>,
11    options: FileOptions<'static, ()>,
12}
13
14impl<W: Write + Seek> PackageWriter<W> {
15    /// Creates a new `PackageWriter` wrapping the given writer with Deflate compression.
16    pub fn new(writer: W) -> Self {
17        let options = FileOptions::default()
18            .compression_method(zip::CompressionMethod::Deflated)
19            .unix_permissions(0o644);
20
21        Self {
22            zip: ZipWriter::new(writer),
23            options,
24        }
25    }
26
27    /// Writes all parts of the package to the ZIP archive and finalizes it.
28    pub fn write(mut self, package: &Package) -> Result<()> {
29        // 1. Write Attachments (Textures, Thumbnails) from the main model
30        // (In a true multi-part, attachments might be shared or part-specific,
31        // but for now we aggregate them in the main model or handle them simply).
32        for (path, data) in &package.main_model.attachments {
33            let zip_path = path.trim_start_matches('/');
34            self.zip
35                .start_file(zip_path, self.options)
36                .map_err(|e| Lib3mfError::Io(e.into()))?;
37            self.zip.write_all(data).map_err(Lib3mfError::Io)?;
38        }
39
40        // 2. Prepare Relationships (Textures, Thumbnails) for 3D Model
41        // We do this BEFORE writing XML because objects need the Relationship ID for the 'thumbnail' attribute.
42        let mut model_rels = Vec::new();
43        let mut path_to_rel_id = std::collections::HashMap::new();
44
45        // A. Collect Textures from Attachments
46        for path in package.main_model.attachments.keys() {
47            if path.starts_with("3D/Textures/") || path.starts_with("/3D/Textures/") {
48                let target = if path.starts_with('/') {
49                    path.to_string()
50                } else {
51                    format!("/{}", path)
52                };
53
54                // Deduplicate? For now, we assume 1:1 path to rel or just create distinct rels per path
55                path_to_rel_id.entry(target.clone()).or_insert_with(|| {
56                    let id = format!("rel_tex_{}", model_rels.len());
57                    model_rels.push(crate::archive::opc::Relationship {
58                        id: id.clone(),
59                        rel_type: "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel/relationship/texture".to_string(),
60                        target: target.clone(),
61                        target_mode: "Internal".to_string(),
62                    });
63                    id
64                });
65            }
66        }
67
68        // B. Collect Object Thumbnails
69        for obj in package.main_model.resources.iter_objects() {
70            if let Some(thumb_path) = &obj.thumbnail {
71                let target = if thumb_path.starts_with('/') {
72                    thumb_path.clone()
73                } else {
74                    format!("/{}", thumb_path)
75                };
76
77                path_to_rel_id.entry(target.clone()).or_insert_with(|| {
78                    let id = format!("rel_thumb_{}", model_rels.len());
79                    model_rels.push(crate::archive::opc::Relationship {
80                        id: id.clone(),
81                        rel_type: "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel/relationship/thumbnail".to_string(),
82                        target: target.clone(),
83                        target_mode: "Internal".to_string(),
84                    });
85                    id
86                });
87            }
88        }
89
90        // 3. Write 3D Model parts
91        let main_path = "3D/3dmodel.model";
92        self.zip
93            .start_file(main_path, self.options)
94            .map_err(|e| Lib3mfError::Io(e.into()))?;
95
96        // Pass the relationship map to write_xml so it can write attributes
97        package
98            .main_model
99            .write_xml(&mut self.zip, Some(&path_to_rel_id))?;
100
101        for (path, model) in &package.parts {
102            self.zip
103                .start_file(path.trim_start_matches('/'), self.options)
104                .map_err(|e| Lib3mfError::Io(e.into()))?;
105            // TODO: Support relationships for other parts if they have their own thumbnails
106            model.write_xml(&mut self.zip, None)?;
107        }
108
109        // 4. Write Relationships (_rels/.rels and model relationships)
110        // Global Relationships
111        self.zip
112            .start_file("_rels/.rels", self.options)
113            .map_err(|e| Lib3mfError::Io(e.into()))?;
114
115        let package_thumb = package
116            .main_model
117            .attachments
118            .keys()
119            .find(|k| k == &"Metadata/thumbnail.png" || k == &"/Metadata/thumbnail.png")
120            .map(|k| {
121                if k.starts_with('/') {
122                    k.clone()
123                } else {
124                    format!("/{}", k)
125                }
126            });
127
128        write_relationships(
129            &mut self.zip,
130            &format!("/{}", main_path),
131            package_thumb.as_deref(),
132        )?;
133
134        // Model Relationships (e.g. 3D/_rels/3dmodel.model.rels)
135        // Merge existing relationships with new texture/thumbnail relationships
136        let model_rels_path = "3D/_rels/3dmodel.model.rels";
137
138        // Start with existing relationships if available
139        let mut all_model_rels = package
140            .main_model
141            .existing_relationships
142            .get(model_rels_path)
143            .cloned()
144            .unwrap_or_default();
145
146        // Add new texture/thumbnail relationships
147        // Use a HashSet to track existing IDs to avoid duplicates
148        let existing_ids: std::collections::HashSet<String> =
149            all_model_rels.iter().map(|r| r.id.clone()).collect();
150
151        for rel in model_rels {
152            if !existing_ids.contains(&rel.id) {
153                all_model_rels.push(rel);
154            }
155        }
156
157        // Write merged relationships if any exist
158        if !all_model_rels.is_empty() {
159            self.zip
160                .start_file(model_rels_path, self.options)
161                .map_err(|e| Lib3mfError::Io(e.into()))?;
162
163            let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
164            xml.push_str("<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n");
165            for rel in all_model_rels {
166                xml.push_str(&format!(
167                    "  <Relationship Target=\"{}\" Id=\"{}\" Type=\"{}\" />\n",
168                    rel.target, rel.id, rel.rel_type
169                ));
170            }
171            xml.push_str("</Relationships>");
172
173            self.zip
174                .write_all(xml.as_bytes())
175                .map_err(Lib3mfError::Io)?;
176        }
177
178        // 4. Write Content Types
179        self.zip
180            .start_file("[Content_Types].xml", self.options)
181            .map_err(|e| Lib3mfError::Io(e.into()))?;
182        write_content_types(&mut self.zip)?;
183
184        self.zip.finish().map_err(|e| Lib3mfError::Io(e.into()))?;
185        Ok(())
186    }
187}