1pub mod thumbnails;
7
8use clap::ValueEnum;
9use lib3mf_core::archive::{ArchiveReader, ZipArchiver, find_model_path, opc};
10use lib3mf_core::parser::parse_model;
11use serde::Serialize;
12use std::collections::BTreeMap;
13use std::fs::File;
14use std::io::{Read, Seek, Write};
15use std::path::PathBuf;
16
17#[derive(Clone, ValueEnum, Debug, PartialEq)]
21pub enum OutputFormat {
22 Text,
24 Json,
26 Tree,
28}
29
30#[derive(Clone, ValueEnum, Debug, PartialEq, Copy)]
34pub enum RepairType {
35 Degenerate,
37 Duplicates,
39 Harmonize,
41 Islands,
43 Holes,
45 All,
47}
48
49enum ModelSource {
50 Archive(ZipArchiver<File>, lib3mf_core::model::Model),
51 Raw(lib3mf_core::model::Model),
52}
53
54fn open_model(path: &PathBuf) -> anyhow::Result<ModelSource> {
55 let mut file =
56 File::open(path).map_err(|e| anyhow::anyhow!("Failed to open file {:?}: {}", path, e))?;
57
58 let mut magic = [0u8; 4];
59 let is_zip = file.read_exact(&mut magic).is_ok() && &magic == b"PK\x03\x04";
60 file.rewind()?;
61
62 if is_zip {
63 let mut archiver = ZipArchiver::new(file)
64 .map_err(|e| anyhow::anyhow!("Failed to open zip archive: {}", e))?;
65 let model_path = find_model_path(&mut archiver)
66 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
67 let model_data = archiver
68 .read_entry(&model_path)
69 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
70 let model = parse_model(std::io::Cursor::new(model_data))
71 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
72 Ok(ModelSource::Archive(archiver, model))
73 } else {
74 let ext = path
75 .extension()
76 .and_then(|s| s.to_str())
77 .unwrap_or("")
78 .to_lowercase();
79
80 match ext.as_str() {
81 "stl" => {
82 let model = lib3mf_converters::stl::StlImporter::read(file)
83 .map_err(|e| anyhow::anyhow!("Failed to import STL: {}", e))?;
84 Ok(ModelSource::Raw(model))
85 }
86 "obj" => {
87 let model = lib3mf_converters::obj::ObjImporter::read(file)
88 .map_err(|e| anyhow::anyhow!("Failed to import OBJ: {}", e))?;
89 Ok(ModelSource::Raw(model))
90 }
91 _ => Err(anyhow::anyhow!(
92 "Unsupported format: {} (and not a ZIP/3MF archive)",
93 ext
94 )),
95 }
96 }
97}
98
99pub fn stats(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
125 let mut source = open_model(&path)?;
126 let stats = match source {
127 ModelSource::Archive(ref mut archiver, ref model) => model
128 .compute_stats(archiver)
129 .map_err(|e| anyhow::anyhow!("Failed to compute stats: {}", e))?,
130 ModelSource::Raw(ref model) => {
131 struct NoArchive;
132 impl std::io::Read for NoArchive {
133 fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
134 Ok(0)
135 }
136 }
137 impl std::io::Seek for NoArchive {
138 fn seek(&mut self, _: std::io::SeekFrom) -> std::io::Result<u64> {
139 Ok(0)
140 }
141 }
142 impl lib3mf_core::archive::ArchiveReader for NoArchive {
143 fn read_entry(&mut self, _: &str) -> lib3mf_core::error::Result<Vec<u8>> {
144 Err(lib3mf_core::error::Lib3mfError::Io(std::io::Error::new(
145 std::io::ErrorKind::NotFound,
146 "Raw format",
147 )))
148 }
149 fn entry_exists(&mut self, _: &str) -> bool {
150 false
151 }
152 fn list_entries(&mut self) -> lib3mf_core::error::Result<Vec<String>> {
153 Ok(vec![])
154 }
155 }
156 model
157 .compute_stats(&mut NoArchive)
158 .map_err(|e| anyhow::anyhow!("Failed to compute stats: {}", e))?
159 }
160 };
161
162 match format {
163 OutputFormat::Json => {
164 println!("{}", serde_json::to_string_pretty(&stats)?);
165 }
166 OutputFormat::Tree => {
167 println!("Model Hierarchy for {:?}", path);
168 match source {
169 ModelSource::Archive(mut archiver, model) => {
170 let mut resolver =
171 lib3mf_core::model::resolver::PartResolver::new(&mut archiver, model);
172 print_model_hierarchy_resolved(&mut resolver);
173 }
174 ModelSource::Raw(model) => {
175 print_model_hierarchy(&model);
176 }
177 }
178 }
179 _ => {
180 println!("Stats for {:?}", path);
181 println!(
182 "Unit: {:?} (Scale: {} m)",
183 stats.unit,
184 stats.unit.scale_factor()
185 );
186 println!("Generator: {:?}", stats.generator.unwrap_or_default());
187 println!("Geometry:");
188
189 let type_display: Vec<String> =
191 ["model", "support", "solidsupport", "surface", "other"]
192 .iter()
193 .filter_map(|&type_name| {
194 stats
195 .geometry
196 .type_counts
197 .get(type_name)
198 .and_then(|&count| {
199 if count > 0 {
200 Some(format!("{} {}", count, type_name))
201 } else {
202 None
203 }
204 })
205 })
206 .collect();
207
208 if type_display.is_empty() {
209 println!(" Objects: 0");
210 } else {
211 println!(" Objects: {}", type_display.join(", "));
212 }
213
214 println!(" Instances: {}", stats.geometry.instance_count);
215 println!(" Vertices: {}", stats.geometry.vertex_count);
216 println!(" Triangles: {}", stats.geometry.triangle_count);
217 if let Some(bbox) = stats.geometry.bounding_box {
218 println!(" Bounding Box: Min {:?}, Max {:?}", bbox.min, bbox.max);
219 }
220 let scale = stats.unit.scale_factor();
221 println!(
222 " Surface Area: {:.2} (native units^2)",
223 stats.geometry.surface_area
224 );
225 println!(
226 " {:.6} m^2",
227 stats.geometry.surface_area * scale * scale
228 );
229 println!(
230 " Volume: {:.2} (native units^3)",
231 stats.geometry.volume
232 );
233 println!(
234 " {:.6} m^3",
235 stats.geometry.volume * scale * scale * scale
236 );
237
238 println!("\nSystem Info:");
239 println!(" Architecture: {}", stats.system_info.architecture);
240 println!(" CPUs (Threads): {}", stats.system_info.num_cpus);
241 println!(
242 " SIMD Features: {}",
243 stats.system_info.simd_features.join(", ")
244 );
245
246 println!("Materials:");
247 println!(" Base Groups: {}", stats.materials.base_materials_count);
248 println!(" Color Groups: {}", stats.materials.color_groups_count);
249 println!(
250 " Texture 2D Groups: {}",
251 stats.materials.texture_2d_groups_count
252 );
253 println!(
254 " Composite Materials: {}",
255 stats.materials.composite_materials_count
256 );
257 println!(
258 " Multi Properties: {}",
259 stats.materials.multi_properties_count
260 );
261
262 let has_bambu = stats.vendor.printer_model.is_some()
264 || !stats.vendor.plates.is_empty()
265 || !stats.vendor.filaments.is_empty()
266 || stats.vendor.slicer_version.is_some();
267
268 if has_bambu {
269 println!("\nVendor Data (Bambu Studio):");
270
271 if let Some(ref version) = stats.vendor.slicer_version {
272 println!(" Slicer: {}", version);
273 }
274
275 if let Some(ref model) = stats.vendor.printer_model {
277 let nozzle_str = stats
278 .vendor
279 .nozzle_diameter
280 .map(|d| format!(" -- {}mm nozzle", d))
281 .unwrap_or_default();
282 println!(" Printer: {}{}", model, nozzle_str);
283 }
284
285 if let Some(ref ps) = stats.vendor.project_settings {
287 if let Some(ref bed) = ps.bed_type {
288 println!(" Bed Type: {}", bed);
289 }
290 if let Some(lh) = ps.layer_height {
291 println!(" Layer Height: {}mm", lh);
292 }
293 }
294
295 if let Some(ref time) = stats.vendor.print_time_estimate {
297 println!(" Print Time: {}", time);
298 }
299
300 let total_g: f32 = stats.vendor.filaments.iter().filter_map(|f| f.used_g).sum();
302 if total_g > 0.0 {
303 println!(" Total Weight: {:.2}g", total_g);
304 }
305
306 if !stats.vendor.filaments.is_empty() {
308 println!("\n Filaments:");
309 println!(
310 " {:>3} {:<6} {:<9} {:>6} {:>6}",
311 "ID", "Type", "Color", "Meters", "Grams"
312 );
313 for f in &stats.vendor.filaments {
314 println!(
315 " {:>3} {:<6} {:<9} {:>6} {:>6}",
316 f.id,
317 &f.type_,
318 f.color.as_deref().unwrap_or("-"),
319 f.used_m
320 .map(|v| format!("{:.2}", v))
321 .unwrap_or_else(|| "-".to_string()),
322 f.used_g
323 .map(|v| format!("{:.2}", v))
324 .unwrap_or_else(|| "-".to_string()),
325 );
326 }
327 }
328
329 if !stats.vendor.plates.is_empty() {
331 println!("\n Plates:");
332 for plate in &stats.vendor.plates {
333 let name = plate.name.as_deref().unwrap_or("[unnamed]");
334 let locked_str = if plate.locked { " [locked]" } else { "" };
335 println!(" Plate {}: {}{}", plate.id, name, locked_str);
336
337 if !plate.items.is_empty() {
339 let obj_ids: Vec<String> = plate
340 .items
341 .iter()
342 .map(|item| {
343 let name = stats
345 .vendor
346 .object_metadata
347 .iter()
348 .find(|o| o.id == item.object_id)
349 .and_then(|o| o.name.as_deref());
350 match name {
351 Some(n) => format!("{} (ID {})", n, item.object_id),
352 None => format!("ID {}", item.object_id),
353 }
354 })
355 .collect();
356 println!(" Objects: {}", obj_ids.join(", "));
357 }
358 }
359 }
360
361 if !stats.vendor.slicer_warnings.is_empty() {
363 println!("\n Slicer Warnings:");
364 for (i, w) in stats.vendor.slicer_warnings.iter().enumerate() {
365 let code = w.error_code.as_deref().unwrap_or("");
366 if code.is_empty() {
367 println!(" [{}] {}", i + 1, w.msg);
368 } else {
369 println!(" [{}] {} ({})", i + 1, w.msg, code);
370 }
371 }
372 }
373 }
374
375 println!("Thumbnails:");
376 println!(
377 " Package Thumbnail: {}",
378 if stats.thumbnails.package_thumbnail_present {
379 "Yes"
380 } else {
381 "No"
382 }
383 );
384 println!(
385 " Object Thumbnails: {}",
386 stats.thumbnails.object_thumbnail_count
387 );
388
389 if stats.displacement.mesh_count > 0 || stats.displacement.texture_count > 0 {
391 println!("\nDisplacement:");
392 println!(" Meshes: {}", stats.displacement.mesh_count);
393 println!(" Textures: {}", stats.displacement.texture_count);
394 if stats.displacement.normal_count > 0 {
395 println!(" Vertex Normals: {}", stats.displacement.normal_count);
396 }
397 if stats.displacement.gradient_count > 0 {
398 println!(" Gradient Vectors: {}", stats.displacement.gradient_count);
399 }
400 if stats.displacement.total_triangle_count > 0 {
401 let coverage = 100.0 * stats.displacement.displaced_triangle_count as f64
402 / stats.displacement.total_triangle_count as f64;
403 println!(
404 " Displaced Triangles: {} of {} ({:.1}%)",
405 stats.displacement.displaced_triangle_count,
406 stats.displacement.total_triangle_count,
407 coverage
408 );
409 }
410 }
411 }
412 }
413 Ok(())
414}
415
416pub fn list(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
429 let source = open_model(&path)?;
430
431 let entries = match source {
432 ModelSource::Archive(mut archiver, _) => archiver
433 .list_entries()
434 .map_err(|e| anyhow::anyhow!("Failed to list entries: {}", e))?,
435 ModelSource::Raw(_) => vec![
436 path.file_name()
437 .and_then(|n| n.to_str())
438 .unwrap_or("model")
439 .to_string(),
440 ],
441 };
442
443 match format {
444 OutputFormat::Json => {
445 let tree = build_file_tree(&entries);
446 println!("{}", serde_json::to_string_pretty(&tree)?);
447 }
448 OutputFormat::Tree => {
449 print_tree(&entries);
450 }
451 OutputFormat::Text => {
452 for entry in entries {
453 println!("{}", entry);
454 }
455 }
456 }
457 Ok(())
458}
459
460pub fn rels(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
474 let mut archiver = open_archive(&path)?;
475
476 let rels_data = archiver.read_entry("_rels/.rels").unwrap_or_default();
478 let rels = if !rels_data.is_empty() {
479 opc::parse_relationships(&rels_data).unwrap_or_default()
480 } else {
481 Vec::new()
482 };
483
484 let types_data = archiver
486 .read_entry("[Content_Types].xml")
487 .unwrap_or_default();
488 let types = if !types_data.is_empty() {
489 opc::parse_content_types(&types_data).unwrap_or_default()
490 } else {
491 Vec::new()
492 };
493
494 match format {
495 OutputFormat::Json => {
496 #[derive(Serialize)]
497 struct OpcData {
498 relationships: Vec<lib3mf_core::archive::opc::Relationship>,
499 content_types: Vec<lib3mf_core::archive::opc::ContentType>,
500 }
501 let data = OpcData {
502 relationships: rels,
503 content_types: types,
504 };
505 println!("{}", serde_json::to_string_pretty(&data)?);
506 }
507 _ => {
508 println!("Relationships:");
509 for rel in rels {
510 println!(
511 " - ID: {}, Type: {}, Target: {}",
512 rel.id, rel.rel_type, rel.target
513 );
514 }
515 println!("\nContent Types:");
516 for ct in types {
517 println!(" - {:?}", ct);
518 }
519 }
520 }
521 Ok(())
522}
523
524pub fn dump(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
537 let mut archiver = open_archive(&path)?;
538 let model_path = find_model_path(&mut archiver)
539 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
540 let model_data = archiver
541 .read_entry(&model_path)
542 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
543 let model = parse_model(std::io::Cursor::new(model_data))
544 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
545
546 match format {
547 OutputFormat::Json => {
548 println!("{}", serde_json::to_string_pretty(&model)?);
549 }
550 _ => {
551 println!("{:#?}", model);
552 }
553 }
554 Ok(())
555}
556
557pub fn extract(path: PathBuf, inner_path: String, output: Option<PathBuf>) -> anyhow::Result<()> {
571 let mut archiver = open_archive(&path)?;
572 let data = archiver
573 .read_entry(&inner_path)
574 .map_err(|e| anyhow::anyhow!("Failed to read entry '{}': {}", inner_path, e))?;
575
576 if let Some(out_path) = output {
577 let mut f = File::create(&out_path)
578 .map_err(|e| anyhow::anyhow!("Failed to create output file {:?}: {}", out_path, e))?;
579 f.write_all(&data)?;
580 println!("Extracted '{}' to {:?}", inner_path, out_path);
581 } else {
582 std::io::stdout().write_all(&data)?;
583 }
584 Ok(())
585}
586
587pub fn extract_by_resource_id(
601 path: PathBuf,
602 resource_id: u32,
603 output: Option<PathBuf>,
604) -> anyhow::Result<()> {
605 let mut archiver = open_archive(&path)?;
606 let model_path = find_model_path(&mut archiver)?;
607 let model_data = archiver.read_entry(&model_path)?;
608 let model = parse_model(std::io::Cursor::new(model_data))?;
609
610 let resource_id = lib3mf_core::model::ResourceId(resource_id);
611
612 if let Some(disp2d) = model.resources.get_displacement_2d(resource_id) {
614 let texture_path = &disp2d.path;
615 let archive_path = texture_path.trim_start_matches('/');
616 let data = archiver
617 .read_entry(archive_path)
618 .map_err(|e| anyhow::anyhow!("Failed to read texture '{}': {}", archive_path, e))?;
619
620 if let Some(out_path) = output {
621 let mut f = File::create(&out_path)?;
622 f.write_all(&data)?;
623 println!(
624 "Extracted displacement texture (ID {}) to {:?}",
625 resource_id.0, out_path
626 );
627 } else {
628 std::io::stdout().write_all(&data)?;
629 }
630 return Ok(());
631 }
632
633 Err(anyhow::anyhow!(
634 "No displacement texture resource found with ID {}",
635 resource_id.0
636 ))
637}
638
639pub fn copy(input: PathBuf, output: PathBuf) -> anyhow::Result<()> {
653 let mut archiver = open_archive(&input)?;
654 let model_path = find_model_path(&mut archiver)
655 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
656 let model_data = archiver
657 .read_entry(&model_path)
658 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
659 let mut model = parse_model(std::io::Cursor::new(model_data))
660 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
661
662 let all_files = archiver.list_entries()?;
664 for entry_path in all_files {
665 if entry_path == model_path
667 || entry_path == "_rels/.rels"
668 || entry_path == "[Content_Types].xml"
669 {
670 continue;
671 }
672
673 if entry_path.ends_with(".rels") {
675 if let Ok(data) = archiver.read_entry(&entry_path) {
676 if let Ok(rels) = lib3mf_core::archive::opc::parse_relationships(&data) {
677 model.existing_relationships.insert(entry_path, rels);
678 }
679 }
680 continue;
681 }
682
683 if let Ok(data) = archiver.read_entry(&entry_path) {
685 model.attachments.insert(entry_path, data);
686 }
687 }
688
689 let file = File::create(&output)
690 .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
691 model
692 .write(file)
693 .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
694
695 println!("Copied {:?} to {:?}", input, output);
696 Ok(())
697}
698
699fn open_archive(path: &PathBuf) -> anyhow::Result<ZipArchiver<File>> {
700 let file =
701 File::open(path).map_err(|e| anyhow::anyhow!("Failed to open file {:?}: {}", path, e))?;
702 ZipArchiver::new(file).map_err(|e| anyhow::anyhow!("Failed to open zip archive: {}", e))
703}
704
705fn build_file_tree(paths: &[String]) -> node::FileNode {
706 let mut root = node::FileNode::new_dir();
707 for path in paths {
708 let parts: Vec<&str> = path.split('/').collect();
709 root.insert(&parts);
710 }
711 root
712}
713
714fn print_tree(paths: &[String]) {
715 let mut tree: BTreeMap<String, node::Node> = BTreeMap::new();
718
719 for path in paths {
720 let parts: Vec<&str> = path.split('/').collect();
721 let mut current_level = &mut tree;
722
723 for (i, part) in parts.iter().enumerate() {
724 let _is_file = i == parts.len() - 1;
725 let node = current_level
726 .entry(part.to_string())
727 .or_insert_with(node::Node::new);
728 current_level = &mut node.children;
729 }
730 }
731
732 node::print_nodes(&tree, "");
733}
734
735fn print_model_hierarchy(model: &lib3mf_core::model::Model) {
736 let mut tree: BTreeMap<String, node::Node> = BTreeMap::new();
737
738 for (i, item) in model.build.items.iter().enumerate() {
739 let (obj_name, obj_type) = model
740 .resources
741 .get_object(item.object_id)
742 .map(|obj| {
743 (
744 obj.name
745 .clone()
746 .unwrap_or_else(|| format!("Object {}", item.object_id.0)),
747 obj.object_type,
748 )
749 })
750 .unwrap_or_else(|| {
751 (
752 format!("Object {}", item.object_id.0),
753 lib3mf_core::model::ObjectType::Model,
754 )
755 });
756
757 let name = format!(
758 "Build Item {} [{}] (type: {}, ID: {})",
759 i + 1,
760 obj_name,
761 obj_type,
762 item.object_id.0
763 );
764 let node = tree.entry(name).or_insert_with(node::Node::new);
765
766 add_object_to_tree(model, item.object_id, node);
768 }
769
770 node::print_nodes(&tree, "");
771}
772
773fn add_object_to_tree(
774 model: &lib3mf_core::model::Model,
775 id: lib3mf_core::model::ResourceId,
776 parent: &mut node::Node,
777) {
778 if let Some(obj) = model.resources.get_object(id) {
779 match &obj.geometry {
780 lib3mf_core::model::Geometry::Mesh(mesh) => {
781 let info = format!(
782 "Mesh: {} vertices, {} triangles",
783 mesh.vertices.len(),
784 mesh.triangles.len()
785 );
786 parent.children.insert(info, node::Node::new());
787 }
788 lib3mf_core::model::Geometry::Components(comps) => {
789 for (i, comp) in comps.components.iter().enumerate() {
790 let child_obj_name = model
791 .resources
792 .get_object(comp.object_id)
793 .and_then(|obj| obj.name.clone())
794 .unwrap_or_else(|| format!("Object {}", comp.object_id.0));
795
796 let name = format!(
797 "Component {} [{}] (ID: {})",
798 i + 1,
799 child_obj_name,
800 comp.object_id.0
801 );
802 let node = parent.children.entry(name).or_insert_with(node::Node::new);
803 add_object_to_tree(model, comp.object_id, node);
804 }
805 }
806 _ => {
807 parent
808 .children
809 .insert("Unknown Geometry".to_string(), node::Node::new());
810 }
811 }
812 }
813}
814
815fn print_model_hierarchy_resolved<A: ArchiveReader>(
816 resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
817) {
818 let mut tree: BTreeMap<String, node::Node> = BTreeMap::new();
819
820 let build_items = resolver.get_root_model().build.items.clone();
821
822 for (i, item) in build_items.iter().enumerate() {
823 let (obj_name, obj_id, obj_type) = {
824 let res = resolver
825 .resolve_object(item.object_id, None)
826 .unwrap_or(None);
827 match res {
828 Some((_model, obj)) => (
829 obj.name
830 .clone()
831 .unwrap_or_else(|| format!("Object {}", obj.id.0)),
832 obj.id,
833 obj.object_type,
834 ),
835 None => (
836 format!("Missing Object {}", item.object_id.0),
837 item.object_id,
838 lib3mf_core::model::ObjectType::Model,
839 ),
840 }
841 };
842
843 let name = format!(
844 "Build Item {} [{}] (type: {}, ID: {})",
845 i + 1,
846 obj_name,
847 obj_type,
848 obj_id.0
849 );
850 let node = tree.entry(name).or_insert_with(node::Node::new);
851
852 add_object_to_tree_resolved(resolver, obj_id, None, node);
854 }
855
856 node::print_nodes(&tree, "");
857}
858
859fn add_object_to_tree_resolved<A: ArchiveReader>(
860 resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
861 id: lib3mf_core::model::ResourceId,
862 path: Option<&str>,
863 parent: &mut node::Node,
864) {
865 let components = {
866 let resolved = resolver.resolve_object(id, path).unwrap_or(None);
867 if let Some((_model, obj)) = resolved {
868 match &obj.geometry {
869 lib3mf_core::model::Geometry::Mesh(mesh) => {
870 let info = format!(
871 "Mesh: {} vertices, {} triangles",
872 mesh.vertices.len(),
873 mesh.triangles.len()
874 );
875 parent.children.insert(info, node::Node::new());
876 None
877 }
878 lib3mf_core::model::Geometry::Components(comps) => Some(comps.components.clone()),
879 _ => {
880 parent
881 .children
882 .insert("Unknown Geometry".to_string(), node::Node::new());
883 None
884 }
885 }
886 } else {
887 None
888 }
889 };
890
891 if let Some(comps) = components {
892 for (i, comp) in comps.iter().enumerate() {
893 let next_path = comp.path.as_deref().or(path);
894 let (child_obj_name, child_obj_id) = {
895 let res = resolver
896 .resolve_object(comp.object_id, next_path)
897 .unwrap_or(None);
898 match res {
899 Some((_model, obj)) => (
900 obj.name
901 .clone()
902 .unwrap_or_else(|| format!("Object {}", obj.id.0)),
903 obj.id,
904 ),
905 None => (
906 format!("Missing Object {}", comp.object_id.0),
907 comp.object_id,
908 ),
909 }
910 };
911
912 let name = format!(
913 "Component {} [{}] (ID: {})",
914 i + 1,
915 child_obj_name,
916 child_obj_id.0
917 );
918 let node = parent.children.entry(name).or_insert_with(node::Node::new);
919 add_object_to_tree_resolved(resolver, child_obj_id, next_path, node);
920 }
921 }
922}
923
924mod node {
925 use serde::Serialize;
926 use std::collections::BTreeMap;
927
928 #[derive(Serialize)]
929 #[serde(untagged)]
930 pub enum FileNode {
931 File(Empty),
932 Dir(BTreeMap<String, FileNode>),
933 }
934
935 #[derive(Serialize)]
936 pub struct Empty {}
937
938 impl FileNode {
939 pub fn new_dir() -> Self {
940 FileNode::Dir(BTreeMap::new())
941 }
942
943 pub fn new_file() -> Self {
944 FileNode::File(Empty {})
945 }
946
947 pub fn insert(&mut self, path_parts: &[&str]) {
948 if let FileNode::Dir(children) = self {
949 if let Some((first, rest)) = path_parts.split_first() {
950 let entry = children
951 .entry(first.to_string())
952 .or_insert_with(FileNode::new_dir);
953
954 if rest.is_empty() {
955 if let FileNode::Dir(sub) = entry {
957 if sub.is_empty() {
958 *entry = FileNode::new_file();
959 } else {
960 }
964 }
965 } else {
966 entry.insert(rest);
968 }
969 }
970 }
971 }
972 }
973
974 #[derive(Serialize)] pub struct Node {
978 pub children: BTreeMap<String, Node>,
979 }
980
981 impl Node {
982 pub fn new() -> Self {
983 Self {
984 children: BTreeMap::new(),
985 }
986 }
987 }
988
989 pub fn print_nodes(nodes: &BTreeMap<String, Node>, prefix: &str) {
990 let count = nodes.len();
991 for (i, (name, node)) in nodes.iter().enumerate() {
992 let is_last = i == count - 1;
993 let connector = if is_last { "└── " } else { "├── " };
994 println!("{}{}{}", prefix, connector, name);
995
996 let child_prefix = if is_last { " " } else { "│ " };
997 let new_prefix = format!("{}{}", prefix, child_prefix);
998 print_nodes(&node.children, &new_prefix);
999 }
1000 }
1001}
1002
1003pub fn convert(input: PathBuf, output: PathBuf) -> anyhow::Result<()> {
1034 let output_ext = output
1035 .extension()
1036 .and_then(|e| e.to_str())
1037 .unwrap_or("")
1038 .to_lowercase();
1039
1040 if output_ext == "stl" {
1042 let file_res = File::open(&input);
1045
1046 let should_use_resolver = if let Ok(mut f) = file_res {
1047 let mut magic = [0u8; 4];
1048 f.read_exact(&mut magic).is_ok() && &magic == b"PK\x03\x04"
1049 } else {
1050 false
1051 };
1052
1053 if should_use_resolver {
1054 let mut archiver = open_archive(&input)?;
1055 let model_path = find_model_path(&mut archiver)
1056 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
1057 let model_data = archiver
1058 .read_entry(&model_path)
1059 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
1060 let model = parse_model(std::io::Cursor::new(model_data))
1061 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
1062
1063 let resolver = lib3mf_core::model::resolver::PartResolver::new(&mut archiver, model);
1064 let file = File::create(&output)
1065 .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
1066
1067 let root_model = resolver.get_root_model().clone(); lib3mf_converters::stl::StlExporter::write_with_resolver(&root_model, resolver, file)
1071 .map_err(|e| anyhow::anyhow!("Failed to export STL: {}", e))?;
1072
1073 println!("Converted {:?} to {:?}", input, output);
1074 return Ok(());
1075 }
1076 }
1077
1078 let model = load_model(&input)?;
1081
1082 let file = File::create(&output)
1084 .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
1085
1086 match output_ext.as_str() {
1087 "3mf" => {
1088 model
1089 .write(file)
1090 .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
1091 }
1092 "stl" => {
1093 lib3mf_converters::stl::StlExporter::write(&model, file)
1094 .map_err(|e| anyhow::anyhow!("Failed to export STL: {}", e))?;
1095 }
1096 "obj" => {
1097 lib3mf_converters::obj::ObjExporter::write(&model, file)
1098 .map_err(|e| anyhow::anyhow!("Failed to export OBJ: {}", e))?;
1099 }
1100 _ => return Err(anyhow::anyhow!("Unsupported output format: {}", output_ext)),
1101 }
1102
1103 println!("Converted {:?} to {:?}", input, output);
1104 Ok(())
1105}
1106
1107pub fn validate(path: PathBuf, level: String) -> anyhow::Result<()> {
1128 use lib3mf_core::validation::{ValidationLevel, ValidationSeverity};
1129
1130 let level_enum = match level.to_lowercase().as_str() {
1131 "minimal" => ValidationLevel::Minimal,
1132 "standard" => ValidationLevel::Standard,
1133 "strict" => ValidationLevel::Strict,
1134 "paranoid" => ValidationLevel::Paranoid,
1135 _ => ValidationLevel::Standard,
1136 };
1137
1138 println!("Validating {:?} at {:?} level...", path, level_enum);
1139
1140 let model = load_model(&path)?;
1141
1142 let report = model.validate(level_enum);
1144
1145 let errors: Vec<_> = report
1146 .items
1147 .iter()
1148 .filter(|i| i.severity == ValidationSeverity::Error)
1149 .collect();
1150 let warnings: Vec<_> = report
1151 .items
1152 .iter()
1153 .filter(|i| i.severity == ValidationSeverity::Warning)
1154 .collect();
1155
1156 if !errors.is_empty() {
1157 println!("Validation Failed with {} error(s):", errors.len());
1158 for item in &errors {
1159 println!(" [ERROR {}] {}", item.code, item.message);
1160 }
1161 std::process::exit(1);
1162 } else if !warnings.is_empty() {
1163 println!("Validation Passed with {} warning(s):", warnings.len());
1164 for item in &warnings {
1165 println!(" [WARN {}] {}", item.code, item.message);
1166 }
1167 } else {
1168 println!("Validation Passed.");
1169 }
1170
1171 Ok(())
1172}
1173
1174pub fn repair(
1195 input: PathBuf,
1196 output: PathBuf,
1197 epsilon: f32,
1198 fixes: Vec<RepairType>,
1199) -> anyhow::Result<()> {
1200 use lib3mf_core::model::{Geometry, MeshRepair, RepairOptions};
1201
1202 println!("Repairing {:?} -> {:?}", input, output);
1203
1204 let mut archiver = open_archive(&input)?;
1205 let model_path = find_model_path(&mut archiver)
1206 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
1207 let model_data = archiver
1208 .read_entry(&model_path)
1209 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
1210 let mut model = parse_model(std::io::Cursor::new(model_data))
1211 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
1212
1213 let mut options = RepairOptions {
1214 stitch_epsilon: epsilon,
1215 remove_degenerate: false,
1216 remove_duplicate_faces: false,
1217 harmonize_orientations: false,
1218 remove_islands: false,
1219 fill_holes: false,
1220 };
1221
1222 let has_all = fixes.contains(&RepairType::All);
1223 for fix in fixes {
1224 match fix {
1225 RepairType::Degenerate => options.remove_degenerate = true,
1226 RepairType::Duplicates => options.remove_duplicate_faces = true,
1227 RepairType::Harmonize => options.harmonize_orientations = true,
1228 RepairType::Islands => options.remove_islands = true,
1229 RepairType::Holes => options.fill_holes = true,
1230 RepairType::All => {
1231 options.remove_degenerate = true;
1232 options.remove_duplicate_faces = true;
1233 options.harmonize_orientations = true;
1234 options.remove_islands = true;
1235 options.fill_holes = true;
1236 }
1237 }
1238 }
1239
1240 if has_all {
1241 options.remove_degenerate = true;
1242 options.remove_duplicate_faces = true;
1243 options.harmonize_orientations = true;
1244 options.remove_islands = true;
1245 options.fill_holes = true;
1246 }
1247
1248 println!("Repair Options: {:?}", options);
1249
1250 let mut total_vertices_removed = 0;
1251 let mut total_triangles_removed = 0;
1252 let mut total_triangles_flipped = 0;
1253 let mut total_triangles_added = 0;
1254
1255 for object in model.resources.iter_objects_mut() {
1256 if let Geometry::Mesh(mesh) = &mut object.geometry {
1257 let stats = mesh.repair(options);
1258 if stats.vertices_removed > 0
1259 || stats.triangles_removed > 0
1260 || stats.triangles_flipped > 0
1261 || stats.triangles_added > 0
1262 {
1263 println!(
1264 "Repaired Object {}: Removed {} vertices, {} triangles. Flipped {}. Added {}.",
1265 object.id.0,
1266 stats.vertices_removed,
1267 stats.triangles_removed,
1268 stats.triangles_flipped,
1269 stats.triangles_added
1270 );
1271 total_vertices_removed += stats.vertices_removed;
1272 total_triangles_removed += stats.triangles_removed;
1273 total_triangles_flipped += stats.triangles_flipped;
1274 total_triangles_added += stats.triangles_added;
1275 }
1276 }
1277 }
1278
1279 println!("Total Repair Stats:");
1280 println!(" Vertices Removed: {}", total_vertices_removed);
1281 println!(" Triangles Removed: {}", total_triangles_removed);
1282 println!(" Triangles Flipped: {}", total_triangles_flipped);
1283 println!(" Triangles Added: {}", total_triangles_added);
1284
1285 let file = File::create(&output)
1287 .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
1288 model
1289 .write(file)
1290 .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
1291
1292 Ok(())
1293}
1294
1295pub fn benchmark(path: PathBuf) -> anyhow::Result<()> {
1308 use std::time::Instant;
1309
1310 println!("Benchmarking {:?}...", path);
1311
1312 let start = Instant::now();
1313 let mut archiver = open_archive(&path)?;
1314 let t_zip = start.elapsed();
1315
1316 let start_parse = Instant::now();
1317 let model_path = find_model_path(&mut archiver)
1318 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
1319 let model_data = archiver
1320 .read_entry(&model_path)
1321 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
1322 let model = parse_model(std::io::Cursor::new(model_data))
1323 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
1324 let t_parse = start_parse.elapsed();
1325
1326 let start_stats = Instant::now();
1327 let stats = model
1328 .compute_stats(&mut archiver)
1329 .map_err(|e| anyhow::anyhow!("Failed to compute stats: {}", e))?;
1330 let t_stats = start_stats.elapsed();
1331
1332 let total = start.elapsed();
1333
1334 println!("Results:");
1335 println!(
1336 " System: {} ({} CPUs), SIMD: {}",
1337 stats.system_info.architecture,
1338 stats.system_info.num_cpus,
1339 stats.system_info.simd_features.join(", ")
1340 );
1341 println!(" Zip Open: {:?}", t_zip);
1342 println!(" XML Parse: {:?}", t_parse);
1343 println!(" Stats Calc: {:?}", t_stats);
1344 println!(" Total: {:?}", total);
1345 println!(" Triangles: {}", stats.geometry.triangle_count);
1346 println!(
1347 " Area: {:.2}, Volume: {:.2}",
1348 stats.geometry.surface_area, stats.geometry.volume
1349 );
1350
1351 Ok(())
1352}
1353
1354pub fn diff(file1: PathBuf, file2: PathBuf, format: &str) -> anyhow::Result<()> {
1369 println!("Comparing {:?} and {:?}...", file1, file2);
1370
1371 let model_a = load_model(&file1)?;
1372 let model_b = load_model(&file2)?;
1373
1374 let diff = lib3mf_core::utils::diff::compare_models(&model_a, &model_b);
1375
1376 if format == "json" {
1377 println!("{}", serde_json::to_string_pretty(&diff)?);
1378 } else if diff.is_empty() {
1379 println!("Models are identical.");
1380 } else {
1381 println!("Differences found:");
1382 if !diff.metadata_diffs.is_empty() {
1383 println!(" Metadata:");
1384 for d in &diff.metadata_diffs {
1385 println!(" - {:?}: {:?} -> {:?}", d.key, d.old_value, d.new_value);
1386 }
1387 }
1388 if !diff.resource_diffs.is_empty() {
1389 println!(" Resources:");
1390 for d in &diff.resource_diffs {
1391 match d {
1392 lib3mf_core::utils::diff::ResourceDiff::Added { id, type_name } => {
1393 println!(" + Added ID {}: {}", id, type_name)
1394 }
1395 lib3mf_core::utils::diff::ResourceDiff::Removed { id, type_name } => {
1396 println!(" - Removed ID {}: {}", id, type_name)
1397 }
1398 lib3mf_core::utils::diff::ResourceDiff::Changed { id, details } => {
1399 println!(" * Changed ID {}:", id);
1400 for det in details {
1401 println!(" . {}", det);
1402 }
1403 }
1404 }
1405 }
1406 }
1407 if !diff.build_diffs.is_empty() {
1408 println!(" Build Items:");
1409 for d in &diff.build_diffs {
1410 println!(" - {:?}", d);
1411 }
1412 }
1413 }
1414
1415 Ok(())
1416}
1417
1418fn load_model(path: &PathBuf) -> anyhow::Result<lib3mf_core::model::Model> {
1419 match open_model(path)? {
1420 ModelSource::Archive(_, model) => Ok(model),
1421 ModelSource::Raw(model) => Ok(model),
1422 }
1423}
1424
1425pub fn sign(
1441 _input: PathBuf,
1442 _output: PathBuf,
1443 _key: PathBuf,
1444 _cert: PathBuf,
1445) -> anyhow::Result<()> {
1446 anyhow::bail!(
1447 "Sign command not implemented: lib3mf-rs currently supports reading/verifying \
1448 signed 3MF files but does not support creating signatures.\n\n\
1449 Implementing signing requires:\n\
1450 - RSA signing with PEM private keys\n\
1451 - XML-DSIG structure creation and canonicalization\n\
1452 - OPC package modification with signature relationships\n\
1453 - X.509 certificate embedding\n\n\
1454 To create signed 3MF files, use the official 3MF SDK or other tools.\n\
1455 Verification of existing signatures works via: {} verify <file>",
1456 std::env::args()
1457 .next()
1458 .unwrap_or_else(|| "lib3mf-cli".to_string())
1459 );
1460}
1461
1462#[cfg(feature = "crypto")]
1479pub fn verify(file: PathBuf) -> anyhow::Result<()> {
1480 println!("Verifying signatures in {:?}...", file);
1481 let mut archiver = open_archive(&file)?;
1482
1483 let rels_data = archiver.read_entry("_rels/.rels").unwrap_or_default();
1485 if rels_data.is_empty() {
1486 println!("No relationships found. File is not signed.");
1487 return Ok(());
1488 }
1489
1490 let rels = opc::parse_relationships(&rels_data)?;
1491 let sig_rels: Vec<_> = rels.iter().filter(|r| r.rel_type == "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel/relationship/signature"
1492 || r.rel_type.ends_with("/signature") ).collect();
1494
1495 if sig_rels.is_empty() {
1496 println!("No signature relationships found.");
1497 return Ok(());
1498 }
1499
1500 println!("Found {} signatures to verify.", sig_rels.len());
1501
1502 let mut all_valid = true;
1504 let mut signature_count = 0;
1505 let mut failed_signatures = Vec::new();
1506
1507 for rel in sig_rels {
1508 println!("Verifying signature: {}", rel.target);
1509 signature_count += 1;
1510 let target_path = rel.target.trim_start_matches('/');
1512
1513 let sig_xml_bytes = match archiver.read_entry(target_path) {
1514 Ok(b) => b,
1515 Err(e) => {
1516 println!(" [ERROR] Failed to read signature part: {}", e);
1517 all_valid = false;
1518 failed_signatures.push(rel.target.clone());
1519 continue;
1520 }
1521 };
1522
1523 let sig_xml_str = String::from_utf8_lossy(&sig_xml_bytes);
1525 let mut sig_parser = lib3mf_core::parser::xml_parser::XmlParser::new(std::io::Cursor::new(
1527 sig_xml_bytes.clone(),
1528 ));
1529 let signature = match lib3mf_core::parser::crypto_parser::parse_signature(&mut sig_parser) {
1530 Ok(s) => s,
1531 Err(e) => {
1532 println!(" [ERROR] Failed to parse signature XML: {}", e);
1533 all_valid = false;
1534 failed_signatures.push(rel.target.clone());
1535 continue;
1536 }
1537 };
1538
1539 let signed_info_c14n = match lib3mf_core::utils::c14n::Canonicalizer::canonicalize_subtree(
1544 &sig_xml_str,
1545 "SignedInfo",
1546 ) {
1547 Ok(b) => b,
1548 Err(e) => {
1549 println!(" [ERROR] Failed to extract/canonicalize SignedInfo: {}", e);
1550 all_valid = false;
1551 failed_signatures.push(rel.target.clone());
1552 continue;
1553 }
1554 };
1555
1556 let mut content_map = BTreeMap::new();
1582 for ref_item in &signature.signed_info.references {
1583 let uri = &ref_item.uri;
1584 if uri.is_empty() {
1585 continue;
1586 } let part_path = uri.trim_start_matches('/');
1588 match archiver.read_entry(part_path) {
1589 Ok(data) => {
1590 content_map.insert(uri.clone(), data);
1591 }
1592 Err(e) => println!(" [WARNING] Could not read referenced part {}: {}", uri, e),
1593 }
1594 }
1595
1596 let resolver = |uri: &str| -> lib3mf_core::error::Result<Vec<u8>> {
1597 content_map.get(uri).cloned().ok_or_else(|| {
1598 lib3mf_core::error::Lib3mfError::Validation(format!("Content not found: {}", uri))
1599 })
1600 };
1601
1602 match lib3mf_core::crypto::verification::verify_signature_extended(
1603 &signature,
1604 resolver,
1605 &signed_info_c14n,
1606 ) {
1607 Ok(valid) => {
1608 if valid {
1609 println!(" [PASS] Signature is VALID.");
1610 if let Some(mut ki) = signature.key_info {
1612 if let Some(x509) = ki.x509_data.take() {
1613 if let Some(_cert_str) = x509.certificate {
1614 println!(
1615 " [INFO] Signed by X.509 Certificate (Trust check pending)"
1616 );
1617 }
1619 } else {
1620 println!(" [INFO] Signed by Raw Key (Self-signed equivalent)");
1621 }
1622 }
1623 } else {
1624 println!(" [FAIL] Signature is INVALID (Verification returned false).");
1625 all_valid = false;
1626 failed_signatures.push(rel.target.clone());
1627 }
1628 }
1629 Err(e) => {
1630 println!(" [FAIL] Verification Error: {}", e);
1631 all_valid = false;
1632 failed_signatures.push(rel.target.clone());
1633 }
1634 }
1635 }
1636
1637 if !all_valid {
1639 anyhow::bail!(
1640 "Signature verification failed for {} of {} signature(s): {:?}",
1641 failed_signatures.len(),
1642 signature_count,
1643 failed_signatures
1644 );
1645 }
1646
1647 println!(
1648 "\nAll {} signature(s) verified successfully.",
1649 signature_count
1650 );
1651 Ok(())
1652}
1653
1654#[cfg(not(feature = "crypto"))]
1662pub fn verify(_file: PathBuf) -> anyhow::Result<()> {
1663 anyhow::bail!(
1664 "Signature verification requires the 'crypto' feature to be enabled.\n\
1665 The CLI was built without cryptographic support."
1666 )
1667}
1668
1669pub fn encrypt(_input: PathBuf, _output: PathBuf, _recipient: PathBuf) -> anyhow::Result<()> {
1684 anyhow::bail!(
1685 "Encrypt command not implemented: lib3mf-rs currently supports reading/parsing \
1686 encrypted 3MF files but does not support creating encrypted packages.\n\n\
1687 Implementing encryption requires:\n\
1688 - AES-256-GCM content encryption\n\
1689 - RSA-OAEP key wrapping for recipients\n\
1690 - KeyStore XML structure creation\n\
1691 - OPC package modification with encrypted content types\n\
1692 - Encrypted relationship handling\n\n\
1693 To create encrypted 3MF files, use the official 3MF SDK or other tools.\n\
1694 Decryption of existing encrypted files is also not yet implemented."
1695 );
1696}
1697
1698pub fn decrypt(_input: PathBuf, _output: PathBuf, _key: PathBuf) -> anyhow::Result<()> {
1713 anyhow::bail!(
1714 "Decrypt command not implemented: lib3mf-rs currently supports reading/parsing \
1715 encrypted 3MF files but does not support decrypting content.\n\n\
1716 Implementing decryption requires:\n\
1717 - KeyStore parsing and key unwrapping\n\
1718 - RSA-OAEP private key operations\n\
1719 - AES-256-GCM content decryption\n\
1720 - OPC package reconstruction with decrypted parts\n\
1721 - Consumer authorization validation\n\n\
1722 To decrypt 3MF files, use the official 3MF SDK or other tools.\n\
1723 The library can parse KeyStore and encryption metadata from encrypted files."
1724 );
1725}