lib3mf_core/writer/
package_writer.rs1use 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
8pub struct PackageWriter<W: Write + Seek> {
10 zip: ZipWriter<W>,
11 options: FileOptions<'static, ()>,
12}
13
14impl<W: Write + Seek> PackageWriter<W> {
15 pub fn new(writer: W) -> Self {
16 let options = FileOptions::default()
17 .compression_method(zip::CompressionMethod::Deflated)
18 .unix_permissions(0o644);
19
20 Self {
21 zip: ZipWriter::new(writer),
22 options,
23 }
24 }
25
26 pub fn write(mut self, package: &Package) -> Result<()> {
27 for (path, data) in &package.main_model.attachments {
31 let zip_path = path.trim_start_matches('/');
32 self.zip
33 .start_file(zip_path, self.options)
34 .map_err(|e| Lib3mfError::Io(e.into()))?;
35 self.zip.write_all(data).map_err(Lib3mfError::Io)?;
36 }
37
38 let mut model_rels = Vec::new();
41 let mut path_to_rel_id = std::collections::HashMap::new();
42
43 for path in package.main_model.attachments.keys() {
45 if path.starts_with("3D/Textures/") || path.starts_with("/3D/Textures/") {
46 let target = if path.starts_with('/') {
47 path.to_string()
48 } else {
49 format!("/{}", path)
50 };
51
52 path_to_rel_id.entry(target.clone()).or_insert_with(|| {
54 let id = format!("rel_tex_{}", model_rels.len());
55 model_rels.push(crate::archive::opc::Relationship {
56 id: id.clone(),
57 rel_type: "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel/relationship/texture".to_string(),
58 target: target.clone(),
59 target_mode: "Internal".to_string(),
60 });
61 id
62 });
63 }
64 }
65
66 for obj in package.main_model.resources.iter_objects() {
68 if let Some(thumb_path) = &obj.thumbnail {
69 let target = if thumb_path.starts_with('/') {
70 thumb_path.clone()
71 } else {
72 format!("/{}", thumb_path)
73 };
74
75 path_to_rel_id.entry(target.clone()).or_insert_with(|| {
76 let id = format!("rel_thumb_{}", model_rels.len());
77 model_rels.push(crate::archive::opc::Relationship {
78 id: id.clone(),
79 rel_type: "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel/relationship/thumbnail".to_string(),
80 target: target.clone(),
81 target_mode: "Internal".to_string(),
82 });
83 id
84 });
85 }
86 }
87
88 let main_path = "3D/3dmodel.model";
90 self.zip
91 .start_file(main_path, self.options)
92 .map_err(|e| Lib3mfError::Io(e.into()))?;
93
94 package
96 .main_model
97 .write_xml(&mut self.zip, Some(&path_to_rel_id))?;
98
99 for (path, model) in &package.parts {
100 self.zip
101 .start_file(path.trim_start_matches('/'), self.options)
102 .map_err(|e| Lib3mfError::Io(e.into()))?;
103 model.write_xml(&mut self.zip, None)?;
105 }
106
107 self.zip
110 .start_file("_rels/.rels", self.options)
111 .map_err(|e| Lib3mfError::Io(e.into()))?;
112
113 let package_thumb = package
114 .main_model
115 .attachments
116 .keys()
117 .find(|k| k == &"Metadata/thumbnail.png" || k == &"/Metadata/thumbnail.png")
118 .map(|k| {
119 if k.starts_with('/') {
120 k.clone()
121 } else {
122 format!("/{}", k)
123 }
124 });
125
126 write_relationships(
127 &mut self.zip,
128 &format!("/{}", main_path),
129 package_thumb.as_deref(),
130 )?;
131
132 let model_rels_path = "3D/_rels/3dmodel.model.rels";
135
136 let mut all_model_rels = package
138 .main_model
139 .existing_relationships
140 .get(model_rels_path)
141 .cloned()
142 .unwrap_or_default();
143
144 let existing_ids: std::collections::HashSet<String> =
147 all_model_rels.iter().map(|r| r.id.clone()).collect();
148
149 for rel in model_rels {
150 if !existing_ids.contains(&rel.id) {
151 all_model_rels.push(rel);
152 }
153 }
154
155 if !all_model_rels.is_empty() {
157 self.zip
158 .start_file(model_rels_path, self.options)
159 .map_err(|e| Lib3mfError::Io(e.into()))?;
160
161 let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
162 xml.push_str("<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n");
163 for rel in all_model_rels {
164 xml.push_str(&format!(
165 " <Relationship Target=\"{}\" Id=\"{}\" Type=\"{}\" />\n",
166 rel.target, rel.id, rel.rel_type
167 ));
168 }
169 xml.push_str("</Relationships>");
170
171 self.zip
172 .write_all(xml.as_bytes())
173 .map_err(Lib3mfError::Io)?;
174 }
175
176 self.zip
178 .start_file("[Content_Types].xml", self.options)
179 .map_err(|e| Lib3mfError::Io(e.into()))?;
180 write_content_types(&mut self.zip)?;
181
182 self.zip.finish().map_err(|e| Lib3mfError::Io(e.into()))?;
183 Ok(())
184 }
185}