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(
9 file: PathBuf,
10 list: bool,
11 extract: Option<PathBuf>,
12 inject: Option<PathBuf>,
13 oid: Option<u32>,
14) -> Result<()> {
15 if list {
16 run_list(&file)?;
17 return Ok(());
18 }
19
20 if let Some(dir) = extract {
21 run_extract(&file, dir)?;
22 return Ok(());
23 }
24
25 if let Some(img_path) = inject {
26 run_inject(&file, img_path, oid)?;
27 return Ok(());
28 }
29
30 println!("Please specify --list, --extract <DIR>, or --inject <IMG>.");
32 Ok(())
33}
34
35fn run_list(file: &PathBuf) -> Result<()> {
36 let mut archiver = crate::commands::open_archive(file)?;
37 let model_path = lib3mf_core::archive::find_model_path(&mut archiver)?;
38 let model_data = archiver.read_entry(&model_path)?;
39 let model = lib3mf_core::parser::parse_model(std::io::Cursor::new(model_data))?;
40
41 println!("Thumbnail Status for: {:?}", file);
42
43 let pkg_thumb = archiver.entry_exists("Metadata/thumbnail.png")
45 || archiver.entry_exists("/Metadata/thumbnail.png");
46 println!(
47 "Package Thumbnail: {}",
48 if pkg_thumb { "Yes" } else { "No" }
49 );
50
51 let model_rels_path = {
53 let path = std::path::Path::new(&model_path);
54 if let Some(parent) = path.parent() {
55 let fname = path.file_name().unwrap_or_default().to_string_lossy();
56 parent
57 .join("_rels")
58 .join(format!("{}.rels", fname))
59 .to_string_lossy()
60 .replace("\\", "/")
61 } else {
62 format!("_rels/{}.rels", model_path)
63 }
64 };
65
66 let model_rels_data = archiver.read_entry(&model_rels_path).unwrap_or_default();
67 let model_rels = if !model_rels_data.is_empty() {
68 lib3mf_core::archive::opc::parse_relationships(&model_rels_data).unwrap_or_default()
69 } else {
70 Vec::new()
71 };
72
73 let mut rel_map = std::collections::HashMap::new();
75 for rel in model_rels {
76 rel_map.insert(rel.id, rel.target);
77 }
78
79 if model.resources.iter_objects().count() > 0 {
81 println!("\nObjects:");
82 for obj in model.resources.iter_objects() {
83 let thumb_display = if let Some(thumb_ref) = &obj.thumbnail {
84 rel_map
86 .get(thumb_ref)
87 .map(|s| s.as_str())
88 .unwrap_or(thumb_ref) } else {
90 "None"
91 };
92 let name = obj.name.as_deref().unwrap_or("-");
93 println!(
94 " ID: {:<4} | Name: {:<20} | Thumbnail: {}",
95 obj.id.0, name, thumb_display
96 );
97 }
98 } else {
99 println!("\nNo objects found.");
100 }
101 Ok(())
102}
103
104fn run_inject(file: &PathBuf, img_path: PathBuf, oid: Option<u32>) -> Result<()> {
121 let mut archiver = crate::commands::open_archive(file)?;
123 let model_path = lib3mf_core::archive::find_model_path(&mut archiver)?;
124 let model_data = archiver.read_entry(&model_path)?;
125 let mut model = lib3mf_core::parser::parse_model(std::io::Cursor::new(model_data))?;
126
127 let all_files = archiver.list_entries()?;
130 for entry_path in all_files {
131 if entry_path == model_path
133 || entry_path == "_rels/.rels"
134 || entry_path == "[Content_Types].xml"
135 {
136 continue;
137 }
138
139 if entry_path.ends_with(".rels") {
141 if let Ok(data) = archiver.read_entry(&entry_path) {
142 if let Ok(rels) = lib3mf_core::archive::opc::parse_relationships(&data) {
143 model.existing_relationships.insert(entry_path, rels);
144 }
145 }
146 continue;
147 }
148
149 if let Ok(data) = archiver.read_entry(&entry_path) {
151 model.attachments.insert(entry_path, data);
152 }
153 }
154
155 println!("Injecting {:?} into {:?}", img_path, file);
156
157 let img_data = fs::read(&img_path)?;
158
159 if let Some(id) = oid {
160 let rid = ResourceId(id);
162
163 let mut found = false;
164 for obj in model.resources.iter_objects_mut() {
165 if obj.id == rid {
166 let path = format!("3D/Textures/thumb_{}.png", id);
168 obj.thumbnail = Some(path.clone());
169
170 model.attachments.insert(path, img_data.clone());
172 println!("Updated Object {} thumbnail.", id);
173 found = true;
174 break;
175 }
176 }
177 if !found {
178 anyhow::bail!("Object ID {} not found.", id);
179 }
180 } else {
181 let path = "Metadata/thumbnail.png".to_string();
183 model.attachments.insert(path, img_data);
184 println!("Updated Package Thumbnail.");
185 }
186
187 let f = File::create(file)?;
189 model
190 .write(f)
191 .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
192
193 println!("Done.");
194 Ok(())
195}
196
197fn run_extract(file: &PathBuf, dir: PathBuf) -> Result<()> {
198 let mut archiver = crate::commands::open_archive(file)?;
200
201 let model_path_str = lib3mf_core::archive::find_model_path(&mut archiver)?;
204 let model_data = archiver.read_entry(&model_path_str)?;
205 let model = lib3mf_core::parser::parse_model(std::io::Cursor::new(model_data))?;
206
207 fs::create_dir_all(&dir)?;
239 println!("Extracting thumbnails to {:?}...", dir);
240
241 let global_rels_data = archiver.read_entry("_rels/.rels").unwrap_or_default();
246 let global_rels = if !global_rels_data.is_empty() {
247 lib3mf_core::archive::opc::parse_relationships(&global_rels_data).unwrap_or_default()
248 } else {
249 Vec::new()
250 };
251
252 let mut pkg_thumb_path = None;
253 for rel in global_rels {
254 if rel.rel_type.ends_with("metadata/thumbnail") {
255 pkg_thumb_path = Some(rel.target);
256 break;
257 }
258 }
259 if pkg_thumb_path.is_none() && archiver.entry_exists("Metadata/thumbnail.png") {
261 pkg_thumb_path = Some("Metadata/thumbnail.png".to_string());
262 }
263
264 if let Some(path) = pkg_thumb_path {
265 if let Ok(data) = archiver.read_entry(&path) {
266 let out = dir.join("package_thumbnail.png");
267 let mut f = File::create(&out)?;
268 f.write_all(&data)?;
269 println!(" Extracted Package Thumbnail: {:?}", out);
270 }
271 }
272
273 let model_rels_path = {
279 let path = std::path::Path::new(&model_path_str);
280 if let Some(parent) = path.parent() {
281 let fname = path.file_name().unwrap_or_default().to_string_lossy();
282 parent
283 .join("_rels")
284 .join(format!("{}.rels", fname))
285 .to_string_lossy()
286 .replace("\\", "/")
287 } else {
288 format!("_rels/{}.rels", model_path_str) }
290 };
291
292 let model_rels_data = archiver.read_entry(&model_rels_path).unwrap_or_default();
293 let model_rels = if !model_rels_data.is_empty() {
294 lib3mf_core::archive::opc::parse_relationships(&model_rels_data).unwrap_or_default()
295 } else {
296 Vec::new()
297 };
298
299 let mut rel_map = std::collections::HashMap::new();
301 for rel in model_rels {
302 rel_map.insert(rel.id, rel.target);
303 }
304
305 for obj in model.resources.iter_objects() {
306 if let Some(thumb_ref) = &obj.thumbnail {
307 let target = rel_map.get(thumb_ref).cloned().or_else(|| {
309 Some(thumb_ref.clone())
311 });
312
313 if let Some(path) = target {
314 let lookup_path = path.trim_start_matches('/');
316 if let Ok(bytes) = archiver.read_entry(lookup_path) {
317 let fname = format!("obj_{}_thumbnail.png", obj.id.0);
318 let out = dir.join(fname);
319 let mut f = File::create(&out)?;
320 f.write_all(&bytes)?;
321 println!(" Extracted Object {} Thumbnail: {:?}", obj.id.0, out);
322 } else {
323 println!(
324 " Warning: Object {} thumbnail target '{}' not found in archive.",
325 obj.id.0, lookup_path
326 );
327 }
328 }
329 }
330 }
331
332 Ok(())
333}