lib3mf_cli/commands/
thumbnails.rs1use anyhow::Result;
2use lib3mf_core::archive::ArchiveReader; use lib3mf_core::model::ResourceId;
4use std::fs::{self, File};
5use std::io::Write; use std::path::PathBuf;
7
8pub fn run(
14 file: PathBuf,
15 list: bool,
16 extract: Option<PathBuf>,
17 inject: Option<PathBuf>,
18 oid: Option<u32>,
19) -> Result<()> {
20 if list {
21 run_list(&file)?;
22 return Ok(());
23 }
24
25 if let Some(dir) = extract {
26 run_extract(&file, dir)?;
27 return Ok(());
28 }
29
30 if let Some(img_path) = inject {
31 run_inject(&file, img_path, oid)?;
32 return Ok(());
33 }
34
35 println!("Please specify --list, --extract <DIR>, or --inject <IMG>.");
37 Ok(())
38}
39
40fn run_list(file: &PathBuf) -> Result<()> {
41 let mut archiver = crate::commands::open_archive(file)?;
42 let model_path = lib3mf_core::archive::find_model_path(&mut archiver)?;
43 let model_data = archiver.read_entry(&model_path)?;
44 let model = lib3mf_core::parser::parse_model(std::io::Cursor::new(model_data))?;
45
46 println!("Thumbnail Status for: {:?}", file);
47
48 let pkg_thumb = archiver.entry_exists("Metadata/thumbnail.png")
50 || archiver.entry_exists("/Metadata/thumbnail.png");
51 println!(
52 "Package Thumbnail: {}",
53 if pkg_thumb { "Yes" } else { "No" }
54 );
55
56 let model_rels_path = {
58 let path = std::path::Path::new(&model_path);
59 if let Some(parent) = path.parent() {
60 let fname = path.file_name().unwrap_or_default().to_string_lossy();
61 parent
62 .join("_rels")
63 .join(format!("{}.rels", fname))
64 .to_string_lossy()
65 .replace("\\", "/")
66 } else {
67 format!("_rels/{}.rels", model_path)
68 }
69 };
70
71 let model_rels_data = archiver.read_entry(&model_rels_path).unwrap_or_default();
72 let model_rels = if !model_rels_data.is_empty() {
73 lib3mf_core::archive::opc::parse_relationships(&model_rels_data).unwrap_or_default()
74 } else {
75 Vec::new()
76 };
77
78 let mut rel_map = std::collections::HashMap::new();
80 for rel in model_rels {
81 rel_map.insert(rel.id, rel.target);
82 }
83
84 if model.resources.iter_objects().count() > 0 {
86 println!("\nObjects:");
87 for obj in model.resources.iter_objects() {
88 let thumb_display = if let Some(thumb_ref) = &obj.thumbnail {
89 rel_map
91 .get(thumb_ref)
92 .map(|s| s.as_str())
93 .unwrap_or(thumb_ref) } else {
95 "None"
96 };
97 let name = obj.name.as_deref().unwrap_or("-");
98 println!(
99 " ID: {:<4} | Name: {:<20} | Thumbnail: {}",
100 obj.id.0, name, thumb_display
101 );
102 }
103 } else {
104 println!("\nNo objects found.");
105 }
106 Ok(())
107}
108
109fn run_inject(file: &PathBuf, img_path: PathBuf, oid: Option<u32>) -> Result<()> {
126 let mut archiver = crate::commands::open_archive(file)?;
128 let model_path = lib3mf_core::archive::find_model_path(&mut archiver)?;
129 let model_data = archiver.read_entry(&model_path)?;
130 let mut model = lib3mf_core::parser::parse_model(std::io::Cursor::new(model_data))?;
131
132 let all_files = archiver.list_entries()?;
135 for entry_path in all_files {
136 if entry_path == model_path
138 || entry_path == "_rels/.rels"
139 || entry_path == "[Content_Types].xml"
140 {
141 continue;
142 }
143
144 if entry_path.ends_with(".rels") {
146 if let Ok(data) = archiver.read_entry(&entry_path)
147 && let Ok(rels) = lib3mf_core::archive::opc::parse_relationships(&data)
148 {
149 model.existing_relationships.insert(entry_path, rels);
150 }
151 continue;
152 }
153
154 if let Ok(data) = archiver.read_entry(&entry_path) {
156 model.attachments.insert(entry_path, data);
157 }
158 }
159
160 println!("Injecting {:?} into {:?}", img_path, file);
161
162 let img_data = fs::read(&img_path)?;
163
164 if let Some(id) = oid {
165 let rid = ResourceId(id);
167
168 let mut found = false;
169 for obj in model.resources.iter_objects_mut() {
170 if obj.id == rid {
171 let path = format!("3D/Textures/thumb_{}.png", id);
173 obj.thumbnail = Some(path.clone());
174
175 model.attachments.insert(path, img_data.clone());
177 println!("Updated Object {} thumbnail.", id);
178 found = true;
179 break;
180 }
181 }
182 if !found {
183 anyhow::bail!("Object ID {} not found.", id);
184 }
185 } else {
186 let path = "Metadata/thumbnail.png".to_string();
188 model.attachments.insert(path, img_data);
189 println!("Updated Package Thumbnail.");
190 }
191
192 let f = File::create(file)?;
194 model
195 .write(f)
196 .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
197
198 println!("Done.");
199 Ok(())
200}
201
202fn run_extract(file: &PathBuf, dir: PathBuf) -> Result<()> {
203 let mut archiver = crate::commands::open_archive(file)?;
205
206 let model_path_str = lib3mf_core::archive::find_model_path(&mut archiver)?;
209 let model_data = archiver.read_entry(&model_path_str)?;
210 let model = lib3mf_core::parser::parse_model(std::io::Cursor::new(model_data))?;
211
212 fs::create_dir_all(&dir)?;
244 println!("Extracting thumbnails to {:?}...", dir);
245
246 let global_rels_data = archiver.read_entry("_rels/.rels").unwrap_or_default();
251 let global_rels = if !global_rels_data.is_empty() {
252 lib3mf_core::archive::opc::parse_relationships(&global_rels_data).unwrap_or_default()
253 } else {
254 Vec::new()
255 };
256
257 let mut pkg_thumb_path = None;
258 for rel in global_rels {
259 if rel.rel_type.ends_with("metadata/thumbnail") {
260 pkg_thumb_path = Some(rel.target);
261 break;
262 }
263 }
264 if pkg_thumb_path.is_none() && archiver.entry_exists("Metadata/thumbnail.png") {
266 pkg_thumb_path = Some("Metadata/thumbnail.png".to_string());
267 }
268
269 if let Some(path) = pkg_thumb_path
270 && let Ok(data) = archiver.read_entry(&path)
271 {
272 let out = dir.join("package_thumbnail.png");
273 let mut f = File::create(&out)?;
274 f.write_all(&data)?;
275 println!(" Extracted Package Thumbnail: {:?}", out);
276 }
277
278 let model_rels_path = {
284 let path = std::path::Path::new(&model_path_str);
285 if let Some(parent) = path.parent() {
286 let fname = path.file_name().unwrap_or_default().to_string_lossy();
287 parent
288 .join("_rels")
289 .join(format!("{}.rels", fname))
290 .to_string_lossy()
291 .replace("\\", "/")
292 } else {
293 format!("_rels/{}.rels", model_path_str) }
295 };
296
297 let model_rels_data = archiver.read_entry(&model_rels_path).unwrap_or_default();
298 let model_rels = if !model_rels_data.is_empty() {
299 lib3mf_core::archive::opc::parse_relationships(&model_rels_data).unwrap_or_default()
300 } else {
301 Vec::new()
302 };
303
304 let mut rel_map = std::collections::HashMap::new();
306 for rel in model_rels {
307 rel_map.insert(rel.id, rel.target);
308 }
309
310 for obj in model.resources.iter_objects() {
311 if let Some(thumb_ref) = &obj.thumbnail {
312 let target = rel_map.get(thumb_ref).cloned().or_else(|| {
314 Some(thumb_ref.clone())
316 });
317
318 if let Some(path) = target {
319 let lookup_path = path.trim_start_matches('/');
321 if let Ok(bytes) = archiver.read_entry(lookup_path) {
322 let fname = format!("obj_{}_thumbnail.png", obj.id.0);
323 let out = dir.join(fname);
324 let mut f = File::create(&out)?;
325 f.write_all(&bytes)?;
326 println!(" Extracted Object {} Thumbnail: {:?}", obj.id.0, out);
327 } else {
328 println!(
329 " Warning: Object {} thumbnail target '{}' not found in archive.",
330 obj.id.0, lookup_path
331 );
332 }
333 }
334 }
335 }
336
337 Ok(())
338}