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 {
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 pub fn write(mut self, package: &Package) -> Result<()> {
29 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 let mut model_rels = Vec::new();
43 let mut path_to_rel_id = std::collections::HashMap::new();
44
45 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 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 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 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 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 model.write_xml(&mut self.zip, None)?;
107 }
108
109 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 let model_rels_path = "3D/_rels/3dmodel.model.rels";
137
138 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 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 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 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}