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 if !stats.vendor.plates.is_empty() {
263 println!("Vendor Data (Bambu):");
264 println!(" Plates: {}", stats.vendor.plates.len());
265 for plate in stats.vendor.plates {
266 println!(" - ID {}: {}", plate.id, plate.name.unwrap_or_default());
267 }
268 }
269
270 println!("Thumbnails:");
271 println!(
272 " Package Thumbnail: {}",
273 if stats.thumbnails.package_thumbnail_present {
274 "Yes"
275 } else {
276 "No"
277 }
278 );
279 println!(
280 " Object Thumbnails: {}",
281 stats.thumbnails.object_thumbnail_count
282 );
283
284 if stats.displacement.mesh_count > 0 || stats.displacement.texture_count > 0 {
286 println!("\nDisplacement:");
287 println!(" Meshes: {}", stats.displacement.mesh_count);
288 println!(" Textures: {}", stats.displacement.texture_count);
289 if stats.displacement.normal_count > 0 {
290 println!(" Vertex Normals: {}", stats.displacement.normal_count);
291 }
292 if stats.displacement.gradient_count > 0 {
293 println!(" Gradient Vectors: {}", stats.displacement.gradient_count);
294 }
295 if stats.displacement.total_triangle_count > 0 {
296 let coverage = 100.0 * stats.displacement.displaced_triangle_count as f64
297 / stats.displacement.total_triangle_count as f64;
298 println!(
299 " Displaced Triangles: {} of {} ({:.1}%)",
300 stats.displacement.displaced_triangle_count,
301 stats.displacement.total_triangle_count,
302 coverage
303 );
304 }
305 }
306 }
307 }
308 Ok(())
309}
310
311pub fn list(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
324 let source = open_model(&path)?;
325
326 let entries = match source {
327 ModelSource::Archive(mut archiver, _) => archiver
328 .list_entries()
329 .map_err(|e| anyhow::anyhow!("Failed to list entries: {}", e))?,
330 ModelSource::Raw(_) => vec![
331 path.file_name()
332 .and_then(|n| n.to_str())
333 .unwrap_or("model")
334 .to_string(),
335 ],
336 };
337
338 match format {
339 OutputFormat::Json => {
340 let tree = build_file_tree(&entries);
341 println!("{}", serde_json::to_string_pretty(&tree)?);
342 }
343 OutputFormat::Tree => {
344 print_tree(&entries);
345 }
346 OutputFormat::Text => {
347 for entry in entries {
348 println!("{}", entry);
349 }
350 }
351 }
352 Ok(())
353}
354
355pub fn rels(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
369 let mut archiver = open_archive(&path)?;
370
371 let rels_data = archiver.read_entry("_rels/.rels").unwrap_or_default();
373 let rels = if !rels_data.is_empty() {
374 opc::parse_relationships(&rels_data).unwrap_or_default()
375 } else {
376 Vec::new()
377 };
378
379 let types_data = archiver
381 .read_entry("[Content_Types].xml")
382 .unwrap_or_default();
383 let types = if !types_data.is_empty() {
384 opc::parse_content_types(&types_data).unwrap_or_default()
385 } else {
386 Vec::new()
387 };
388
389 match format {
390 OutputFormat::Json => {
391 #[derive(Serialize)]
392 struct OpcData {
393 relationships: Vec<lib3mf_core::archive::opc::Relationship>,
394 content_types: Vec<lib3mf_core::archive::opc::ContentType>,
395 }
396 let data = OpcData {
397 relationships: rels,
398 content_types: types,
399 };
400 println!("{}", serde_json::to_string_pretty(&data)?);
401 }
402 _ => {
403 println!("Relationships:");
404 for rel in rels {
405 println!(
406 " - ID: {}, Type: {}, Target: {}",
407 rel.id, rel.rel_type, rel.target
408 );
409 }
410 println!("\nContent Types:");
411 for ct in types {
412 println!(" - {:?}", ct);
413 }
414 }
415 }
416 Ok(())
417}
418
419pub fn dump(path: PathBuf, format: OutputFormat) -> anyhow::Result<()> {
432 let mut archiver = open_archive(&path)?;
433 let model_path = find_model_path(&mut archiver)
434 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
435 let model_data = archiver
436 .read_entry(&model_path)
437 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
438 let model = parse_model(std::io::Cursor::new(model_data))
439 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
440
441 match format {
442 OutputFormat::Json => {
443 println!("{}", serde_json::to_string_pretty(&model)?);
444 }
445 _ => {
446 println!("{:#?}", model);
447 }
448 }
449 Ok(())
450}
451
452pub fn extract(path: PathBuf, inner_path: String, output: Option<PathBuf>) -> anyhow::Result<()> {
466 let mut archiver = open_archive(&path)?;
467 let data = archiver
468 .read_entry(&inner_path)
469 .map_err(|e| anyhow::anyhow!("Failed to read entry '{}': {}", inner_path, e))?;
470
471 if let Some(out_path) = output {
472 let mut f = File::create(&out_path)
473 .map_err(|e| anyhow::anyhow!("Failed to create output file {:?}: {}", out_path, e))?;
474 f.write_all(&data)?;
475 println!("Extracted '{}' to {:?}", inner_path, out_path);
476 } else {
477 std::io::stdout().write_all(&data)?;
478 }
479 Ok(())
480}
481
482pub fn extract_by_resource_id(
496 path: PathBuf,
497 resource_id: u32,
498 output: Option<PathBuf>,
499) -> anyhow::Result<()> {
500 let mut archiver = open_archive(&path)?;
501 let model_path = find_model_path(&mut archiver)?;
502 let model_data = archiver.read_entry(&model_path)?;
503 let model = parse_model(std::io::Cursor::new(model_data))?;
504
505 let resource_id = lib3mf_core::model::ResourceId(resource_id);
506
507 if let Some(disp2d) = model.resources.get_displacement_2d(resource_id) {
509 let texture_path = &disp2d.path;
510 let archive_path = texture_path.trim_start_matches('/');
511 let data = archiver
512 .read_entry(archive_path)
513 .map_err(|e| anyhow::anyhow!("Failed to read texture '{}': {}", archive_path, e))?;
514
515 if let Some(out_path) = output {
516 let mut f = File::create(&out_path)?;
517 f.write_all(&data)?;
518 println!(
519 "Extracted displacement texture (ID {}) to {:?}",
520 resource_id.0, out_path
521 );
522 } else {
523 std::io::stdout().write_all(&data)?;
524 }
525 return Ok(());
526 }
527
528 Err(anyhow::anyhow!(
529 "No displacement texture resource found with ID {}",
530 resource_id.0
531 ))
532}
533
534pub fn copy(input: PathBuf, output: PathBuf) -> anyhow::Result<()> {
548 let mut archiver = open_archive(&input)?;
549 let model_path = find_model_path(&mut archiver)
550 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
551 let model_data = archiver
552 .read_entry(&model_path)
553 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
554 let mut model = parse_model(std::io::Cursor::new(model_data))
555 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
556
557 let all_files = archiver.list_entries()?;
559 for entry_path in all_files {
560 if entry_path == model_path
562 || entry_path == "_rels/.rels"
563 || entry_path == "[Content_Types].xml"
564 {
565 continue;
566 }
567
568 if entry_path.ends_with(".rels") {
570 if let Ok(data) = archiver.read_entry(&entry_path) {
571 if let Ok(rels) = lib3mf_core::archive::opc::parse_relationships(&data) {
572 model.existing_relationships.insert(entry_path, rels);
573 }
574 }
575 continue;
576 }
577
578 if let Ok(data) = archiver.read_entry(&entry_path) {
580 model.attachments.insert(entry_path, data);
581 }
582 }
583
584 let file = File::create(&output)
585 .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
586 model
587 .write(file)
588 .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
589
590 println!("Copied {:?} to {:?}", input, output);
591 Ok(())
592}
593
594fn open_archive(path: &PathBuf) -> anyhow::Result<ZipArchiver<File>> {
595 let file =
596 File::open(path).map_err(|e| anyhow::anyhow!("Failed to open file {:?}: {}", path, e))?;
597 ZipArchiver::new(file).map_err(|e| anyhow::anyhow!("Failed to open zip archive: {}", e))
598}
599
600fn build_file_tree(paths: &[String]) -> node::FileNode {
601 let mut root = node::FileNode::new_dir();
602 for path in paths {
603 let parts: Vec<&str> = path.split('/').collect();
604 root.insert(&parts);
605 }
606 root
607}
608
609fn print_tree(paths: &[String]) {
610 let mut tree: BTreeMap<String, node::Node> = BTreeMap::new();
613
614 for path in paths {
615 let parts: Vec<&str> = path.split('/').collect();
616 let mut current_level = &mut tree;
617
618 for (i, part) in parts.iter().enumerate() {
619 let _is_file = i == parts.len() - 1;
620 let node = current_level
621 .entry(part.to_string())
622 .or_insert_with(node::Node::new);
623 current_level = &mut node.children;
624 }
625 }
626
627 node::print_nodes(&tree, "");
628}
629
630fn print_model_hierarchy(model: &lib3mf_core::model::Model) {
631 let mut tree: BTreeMap<String, node::Node> = BTreeMap::new();
632
633 for (i, item) in model.build.items.iter().enumerate() {
634 let (obj_name, obj_type) = model
635 .resources
636 .get_object(item.object_id)
637 .map(|obj| {
638 (
639 obj.name
640 .clone()
641 .unwrap_or_else(|| format!("Object {}", item.object_id.0)),
642 obj.object_type,
643 )
644 })
645 .unwrap_or_else(|| {
646 (
647 format!("Object {}", item.object_id.0),
648 lib3mf_core::model::ObjectType::Model,
649 )
650 });
651
652 let name = format!(
653 "Build Item {} [{}] (type: {}, ID: {})",
654 i + 1,
655 obj_name,
656 obj_type,
657 item.object_id.0
658 );
659 let node = tree.entry(name).or_insert_with(node::Node::new);
660
661 add_object_to_tree(model, item.object_id, node);
663 }
664
665 node::print_nodes(&tree, "");
666}
667
668fn add_object_to_tree(
669 model: &lib3mf_core::model::Model,
670 id: lib3mf_core::model::ResourceId,
671 parent: &mut node::Node,
672) {
673 if let Some(obj) = model.resources.get_object(id) {
674 match &obj.geometry {
675 lib3mf_core::model::Geometry::Mesh(mesh) => {
676 let info = format!(
677 "Mesh: {} vertices, {} triangles",
678 mesh.vertices.len(),
679 mesh.triangles.len()
680 );
681 parent.children.insert(info, node::Node::new());
682 }
683 lib3mf_core::model::Geometry::Components(comps) => {
684 for (i, comp) in comps.components.iter().enumerate() {
685 let child_obj_name = model
686 .resources
687 .get_object(comp.object_id)
688 .and_then(|obj| obj.name.clone())
689 .unwrap_or_else(|| format!("Object {}", comp.object_id.0));
690
691 let name = format!(
692 "Component {} [{}] (ID: {})",
693 i + 1,
694 child_obj_name,
695 comp.object_id.0
696 );
697 let node = parent.children.entry(name).or_insert_with(node::Node::new);
698 add_object_to_tree(model, comp.object_id, node);
699 }
700 }
701 _ => {
702 parent
703 .children
704 .insert("Unknown Geometry".to_string(), node::Node::new());
705 }
706 }
707 }
708}
709
710fn print_model_hierarchy_resolved<A: ArchiveReader>(
711 resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
712) {
713 let mut tree: BTreeMap<String, node::Node> = BTreeMap::new();
714
715 let build_items = resolver.get_root_model().build.items.clone();
716
717 for (i, item) in build_items.iter().enumerate() {
718 let (obj_name, obj_id, obj_type) = {
719 let res = resolver
720 .resolve_object(item.object_id, None)
721 .unwrap_or(None);
722 match res {
723 Some((_model, obj)) => (
724 obj.name
725 .clone()
726 .unwrap_or_else(|| format!("Object {}", obj.id.0)),
727 obj.id,
728 obj.object_type,
729 ),
730 None => (
731 format!("Missing Object {}", item.object_id.0),
732 item.object_id,
733 lib3mf_core::model::ObjectType::Model,
734 ),
735 }
736 };
737
738 let name = format!(
739 "Build Item {} [{}] (type: {}, ID: {})",
740 i + 1,
741 obj_name,
742 obj_type,
743 obj_id.0
744 );
745 let node = tree.entry(name).or_insert_with(node::Node::new);
746
747 add_object_to_tree_resolved(resolver, obj_id, None, node);
749 }
750
751 node::print_nodes(&tree, "");
752}
753
754fn add_object_to_tree_resolved<A: ArchiveReader>(
755 resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
756 id: lib3mf_core::model::ResourceId,
757 path: Option<&str>,
758 parent: &mut node::Node,
759) {
760 let components = {
761 let resolved = resolver.resolve_object(id, path).unwrap_or(None);
762 if let Some((_model, obj)) = resolved {
763 match &obj.geometry {
764 lib3mf_core::model::Geometry::Mesh(mesh) => {
765 let info = format!(
766 "Mesh: {} vertices, {} triangles",
767 mesh.vertices.len(),
768 mesh.triangles.len()
769 );
770 parent.children.insert(info, node::Node::new());
771 None
772 }
773 lib3mf_core::model::Geometry::Components(comps) => Some(comps.components.clone()),
774 _ => {
775 parent
776 .children
777 .insert("Unknown Geometry".to_string(), node::Node::new());
778 None
779 }
780 }
781 } else {
782 None
783 }
784 };
785
786 if let Some(comps) = components {
787 for (i, comp) in comps.iter().enumerate() {
788 let next_path = comp.path.as_deref().or(path);
789 let (child_obj_name, child_obj_id) = {
790 let res = resolver
791 .resolve_object(comp.object_id, next_path)
792 .unwrap_or(None);
793 match res {
794 Some((_model, obj)) => (
795 obj.name
796 .clone()
797 .unwrap_or_else(|| format!("Object {}", obj.id.0)),
798 obj.id,
799 ),
800 None => (
801 format!("Missing Object {}", comp.object_id.0),
802 comp.object_id,
803 ),
804 }
805 };
806
807 let name = format!(
808 "Component {} [{}] (ID: {})",
809 i + 1,
810 child_obj_name,
811 child_obj_id.0
812 );
813 let node = parent.children.entry(name).or_insert_with(node::Node::new);
814 add_object_to_tree_resolved(resolver, child_obj_id, next_path, node);
815 }
816 }
817}
818
819mod node {
820 use serde::Serialize;
821 use std::collections::BTreeMap;
822
823 #[derive(Serialize)]
824 #[serde(untagged)]
825 pub enum FileNode {
826 File(Empty),
827 Dir(BTreeMap<String, FileNode>),
828 }
829
830 #[derive(Serialize)]
831 pub struct Empty {}
832
833 impl FileNode {
834 pub fn new_dir() -> Self {
835 FileNode::Dir(BTreeMap::new())
836 }
837
838 pub fn new_file() -> Self {
839 FileNode::File(Empty {})
840 }
841
842 pub fn insert(&mut self, path_parts: &[&str]) {
843 if let FileNode::Dir(children) = self {
844 if let Some((first, rest)) = path_parts.split_first() {
845 let entry = children
846 .entry(first.to_string())
847 .or_insert_with(FileNode::new_dir);
848
849 if rest.is_empty() {
850 if let FileNode::Dir(sub) = entry {
852 if sub.is_empty() {
853 *entry = FileNode::new_file();
854 } else {
855 }
859 }
860 } else {
861 entry.insert(rest);
863 }
864 }
865 }
866 }
867 }
868
869 #[derive(Serialize)] pub struct Node {
873 pub children: BTreeMap<String, Node>,
874 }
875
876 impl Node {
877 pub fn new() -> Self {
878 Self {
879 children: BTreeMap::new(),
880 }
881 }
882 }
883
884 pub fn print_nodes(nodes: &BTreeMap<String, Node>, prefix: &str) {
885 let count = nodes.len();
886 for (i, (name, node)) in nodes.iter().enumerate() {
887 let is_last = i == count - 1;
888 let connector = if is_last { "└── " } else { "├── " };
889 println!("{}{}{}", prefix, connector, name);
890
891 let child_prefix = if is_last { " " } else { "│ " };
892 let new_prefix = format!("{}{}", prefix, child_prefix);
893 print_nodes(&node.children, &new_prefix);
894 }
895 }
896}
897
898pub fn convert(input: PathBuf, output: PathBuf) -> anyhow::Result<()> {
929 let output_ext = output
930 .extension()
931 .and_then(|e| e.to_str())
932 .unwrap_or("")
933 .to_lowercase();
934
935 if output_ext == "stl" {
937 let file_res = File::open(&input);
940
941 let should_use_resolver = if let Ok(mut f) = file_res {
942 let mut magic = [0u8; 4];
943 f.read_exact(&mut magic).is_ok() && &magic == b"PK\x03\x04"
944 } else {
945 false
946 };
947
948 if should_use_resolver {
949 let mut archiver = open_archive(&input)?;
950 let model_path = find_model_path(&mut archiver)
951 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
952 let model_data = archiver
953 .read_entry(&model_path)
954 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
955 let model = parse_model(std::io::Cursor::new(model_data))
956 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
957
958 let resolver = lib3mf_core::model::resolver::PartResolver::new(&mut archiver, model);
959 let file = File::create(&output)
960 .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
961
962 let root_model = resolver.get_root_model().clone(); lib3mf_converters::stl::StlExporter::write_with_resolver(&root_model, resolver, file)
966 .map_err(|e| anyhow::anyhow!("Failed to export STL: {}", e))?;
967
968 println!("Converted {:?} to {:?}", input, output);
969 return Ok(());
970 }
971 }
972
973 let model = load_model(&input)?;
976
977 let file = File::create(&output)
979 .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
980
981 match output_ext.as_str() {
982 "3mf" => {
983 model
984 .write(file)
985 .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
986 }
987 "stl" => {
988 lib3mf_converters::stl::StlExporter::write(&model, file)
989 .map_err(|e| anyhow::anyhow!("Failed to export STL: {}", e))?;
990 }
991 "obj" => {
992 lib3mf_converters::obj::ObjExporter::write(&model, file)
993 .map_err(|e| anyhow::anyhow!("Failed to export OBJ: {}", e))?;
994 }
995 _ => return Err(anyhow::anyhow!("Unsupported output format: {}", output_ext)),
996 }
997
998 println!("Converted {:?} to {:?}", input, output);
999 Ok(())
1000}
1001
1002pub fn validate(path: PathBuf, level: String) -> anyhow::Result<()> {
1023 use lib3mf_core::validation::{ValidationLevel, ValidationSeverity};
1024
1025 let level_enum = match level.to_lowercase().as_str() {
1026 "minimal" => ValidationLevel::Minimal,
1027 "standard" => ValidationLevel::Standard,
1028 "strict" => ValidationLevel::Strict,
1029 "paranoid" => ValidationLevel::Paranoid,
1030 _ => ValidationLevel::Standard,
1031 };
1032
1033 println!("Validating {:?} at {:?} level...", path, level_enum);
1034
1035 let model = load_model(&path)?;
1036
1037 let report = model.validate(level_enum);
1039
1040 let errors: Vec<_> = report
1041 .items
1042 .iter()
1043 .filter(|i| i.severity == ValidationSeverity::Error)
1044 .collect();
1045 let warnings: Vec<_> = report
1046 .items
1047 .iter()
1048 .filter(|i| i.severity == ValidationSeverity::Warning)
1049 .collect();
1050
1051 if !errors.is_empty() {
1052 println!("Validation Failed with {} error(s):", errors.len());
1053 for item in &errors {
1054 println!(" [ERROR {}] {}", item.code, item.message);
1055 }
1056 std::process::exit(1);
1057 } else if !warnings.is_empty() {
1058 println!("Validation Passed with {} warning(s):", warnings.len());
1059 for item in &warnings {
1060 println!(" [WARN {}] {}", item.code, item.message);
1061 }
1062 } else {
1063 println!("Validation Passed.");
1064 }
1065
1066 Ok(())
1067}
1068
1069pub fn repair(
1090 input: PathBuf,
1091 output: PathBuf,
1092 epsilon: f32,
1093 fixes: Vec<RepairType>,
1094) -> anyhow::Result<()> {
1095 use lib3mf_core::model::{Geometry, MeshRepair, RepairOptions};
1096
1097 println!("Repairing {:?} -> {:?}", input, output);
1098
1099 let mut archiver = open_archive(&input)?;
1100 let model_path = find_model_path(&mut archiver)
1101 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
1102 let model_data = archiver
1103 .read_entry(&model_path)
1104 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
1105 let mut model = parse_model(std::io::Cursor::new(model_data))
1106 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
1107
1108 let mut options = RepairOptions {
1109 stitch_epsilon: epsilon,
1110 remove_degenerate: false,
1111 remove_duplicate_faces: false,
1112 harmonize_orientations: false,
1113 remove_islands: false,
1114 fill_holes: false,
1115 };
1116
1117 let has_all = fixes.contains(&RepairType::All);
1118 for fix in fixes {
1119 match fix {
1120 RepairType::Degenerate => options.remove_degenerate = true,
1121 RepairType::Duplicates => options.remove_duplicate_faces = true,
1122 RepairType::Harmonize => options.harmonize_orientations = true,
1123 RepairType::Islands => options.remove_islands = true,
1124 RepairType::Holes => options.fill_holes = true,
1125 RepairType::All => {
1126 options.remove_degenerate = true;
1127 options.remove_duplicate_faces = true;
1128 options.harmonize_orientations = true;
1129 options.remove_islands = true;
1130 options.fill_holes = true;
1131 }
1132 }
1133 }
1134
1135 if has_all {
1136 options.remove_degenerate = true;
1137 options.remove_duplicate_faces = true;
1138 options.harmonize_orientations = true;
1139 options.remove_islands = true;
1140 options.fill_holes = true;
1141 }
1142
1143 println!("Repair Options: {:?}", options);
1144
1145 let mut total_vertices_removed = 0;
1146 let mut total_triangles_removed = 0;
1147 let mut total_triangles_flipped = 0;
1148 let mut total_triangles_added = 0;
1149
1150 for object in model.resources.iter_objects_mut() {
1151 if let Geometry::Mesh(mesh) = &mut object.geometry {
1152 let stats = mesh.repair(options);
1153 if stats.vertices_removed > 0
1154 || stats.triangles_removed > 0
1155 || stats.triangles_flipped > 0
1156 || stats.triangles_added > 0
1157 {
1158 println!(
1159 "Repaired Object {}: Removed {} vertices, {} triangles. Flipped {}. Added {}.",
1160 object.id.0,
1161 stats.vertices_removed,
1162 stats.triangles_removed,
1163 stats.triangles_flipped,
1164 stats.triangles_added
1165 );
1166 total_vertices_removed += stats.vertices_removed;
1167 total_triangles_removed += stats.triangles_removed;
1168 total_triangles_flipped += stats.triangles_flipped;
1169 total_triangles_added += stats.triangles_added;
1170 }
1171 }
1172 }
1173
1174 println!("Total Repair Stats:");
1175 println!(" Vertices Removed: {}", total_vertices_removed);
1176 println!(" Triangles Removed: {}", total_triangles_removed);
1177 println!(" Triangles Flipped: {}", total_triangles_flipped);
1178 println!(" Triangles Added: {}", total_triangles_added);
1179
1180 let file = File::create(&output)
1182 .map_err(|e| anyhow::anyhow!("Failed to create output file: {}", e))?;
1183 model
1184 .write(file)
1185 .map_err(|e| anyhow::anyhow!("Failed to write 3MF: {}", e))?;
1186
1187 Ok(())
1188}
1189
1190pub fn benchmark(path: PathBuf) -> anyhow::Result<()> {
1203 use std::time::Instant;
1204
1205 println!("Benchmarking {:?}...", path);
1206
1207 let start = Instant::now();
1208 let mut archiver = open_archive(&path)?;
1209 let t_zip = start.elapsed();
1210
1211 let start_parse = Instant::now();
1212 let model_path = find_model_path(&mut archiver)
1213 .map_err(|e| anyhow::anyhow!("Failed to find model path: {}", e))?;
1214 let model_data = archiver
1215 .read_entry(&model_path)
1216 .map_err(|e| anyhow::anyhow!("Failed to read model data: {}", e))?;
1217 let model = parse_model(std::io::Cursor::new(model_data))
1218 .map_err(|e| anyhow::anyhow!("Failed to parse model XML: {}", e))?;
1219 let t_parse = start_parse.elapsed();
1220
1221 let start_stats = Instant::now();
1222 let stats = model
1223 .compute_stats(&mut archiver)
1224 .map_err(|e| anyhow::anyhow!("Failed to compute stats: {}", e))?;
1225 let t_stats = start_stats.elapsed();
1226
1227 let total = start.elapsed();
1228
1229 println!("Results:");
1230 println!(
1231 " System: {} ({} CPUs), SIMD: {}",
1232 stats.system_info.architecture,
1233 stats.system_info.num_cpus,
1234 stats.system_info.simd_features.join(", ")
1235 );
1236 println!(" Zip Open: {:?}", t_zip);
1237 println!(" XML Parse: {:?}", t_parse);
1238 println!(" Stats Calc: {:?}", t_stats);
1239 println!(" Total: {:?}", total);
1240 println!(" Triangles: {}", stats.geometry.triangle_count);
1241 println!(
1242 " Area: {:.2}, Volume: {:.2}",
1243 stats.geometry.surface_area, stats.geometry.volume
1244 );
1245
1246 Ok(())
1247}
1248
1249pub fn diff(file1: PathBuf, file2: PathBuf, format: &str) -> anyhow::Result<()> {
1264 println!("Comparing {:?} and {:?}...", file1, file2);
1265
1266 let model_a = load_model(&file1)?;
1267 let model_b = load_model(&file2)?;
1268
1269 let diff = lib3mf_core::utils::diff::compare_models(&model_a, &model_b);
1270
1271 if format == "json" {
1272 println!("{}", serde_json::to_string_pretty(&diff)?);
1273 } else if diff.is_empty() {
1274 println!("Models are identical.");
1275 } else {
1276 println!("Differences found:");
1277 if !diff.metadata_diffs.is_empty() {
1278 println!(" Metadata:");
1279 for d in &diff.metadata_diffs {
1280 println!(" - {:?}: {:?} -> {:?}", d.key, d.old_value, d.new_value);
1281 }
1282 }
1283 if !diff.resource_diffs.is_empty() {
1284 println!(" Resources:");
1285 for d in &diff.resource_diffs {
1286 match d {
1287 lib3mf_core::utils::diff::ResourceDiff::Added { id, type_name } => {
1288 println!(" + Added ID {}: {}", id, type_name)
1289 }
1290 lib3mf_core::utils::diff::ResourceDiff::Removed { id, type_name } => {
1291 println!(" - Removed ID {}: {}", id, type_name)
1292 }
1293 lib3mf_core::utils::diff::ResourceDiff::Changed { id, details } => {
1294 println!(" * Changed ID {}:", id);
1295 for det in details {
1296 println!(" . {}", det);
1297 }
1298 }
1299 }
1300 }
1301 }
1302 if !diff.build_diffs.is_empty() {
1303 println!(" Build Items:");
1304 for d in &diff.build_diffs {
1305 println!(" - {:?}", d);
1306 }
1307 }
1308 }
1309
1310 Ok(())
1311}
1312
1313fn load_model(path: &PathBuf) -> anyhow::Result<lib3mf_core::model::Model> {
1314 match open_model(path)? {
1315 ModelSource::Archive(_, model) => Ok(model),
1316 ModelSource::Raw(model) => Ok(model),
1317 }
1318}
1319
1320pub fn sign(
1336 _input: PathBuf,
1337 _output: PathBuf,
1338 _key: PathBuf,
1339 _cert: PathBuf,
1340) -> anyhow::Result<()> {
1341 anyhow::bail!(
1342 "Sign command not implemented: lib3mf-rs currently supports reading/verifying \
1343 signed 3MF files but does not support creating signatures.\n\n\
1344 Implementing signing requires:\n\
1345 - RSA signing with PEM private keys\n\
1346 - XML-DSIG structure creation and canonicalization\n\
1347 - OPC package modification with signature relationships\n\
1348 - X.509 certificate embedding\n\n\
1349 To create signed 3MF files, use the official 3MF SDK or other tools.\n\
1350 Verification of existing signatures works via: {} verify <file>",
1351 std::env::args()
1352 .next()
1353 .unwrap_or_else(|| "lib3mf-cli".to_string())
1354 );
1355}
1356
1357#[cfg(feature = "crypto")]
1374pub fn verify(file: PathBuf) -> anyhow::Result<()> {
1375 println!("Verifying signatures in {:?}...", file);
1376 let mut archiver = open_archive(&file)?;
1377
1378 let rels_data = archiver.read_entry("_rels/.rels").unwrap_or_default();
1380 if rels_data.is_empty() {
1381 println!("No relationships found. File is not signed.");
1382 return Ok(());
1383 }
1384
1385 let rels = opc::parse_relationships(&rels_data)?;
1386 let sig_rels: Vec<_> = rels.iter().filter(|r| r.rel_type == "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel/relationship/signature"
1387 || r.rel_type.ends_with("/signature") ).collect();
1389
1390 if sig_rels.is_empty() {
1391 println!("No signature relationships found.");
1392 return Ok(());
1393 }
1394
1395 println!("Found {} signatures to verify.", sig_rels.len());
1396
1397 let mut all_valid = true;
1399 let mut signature_count = 0;
1400 let mut failed_signatures = Vec::new();
1401
1402 for rel in sig_rels {
1403 println!("Verifying signature: {}", rel.target);
1404 signature_count += 1;
1405 let target_path = rel.target.trim_start_matches('/');
1407
1408 let sig_xml_bytes = match archiver.read_entry(target_path) {
1409 Ok(b) => b,
1410 Err(e) => {
1411 println!(" [ERROR] Failed to read signature part: {}", e);
1412 all_valid = false;
1413 failed_signatures.push(rel.target.clone());
1414 continue;
1415 }
1416 };
1417
1418 let sig_xml_str = String::from_utf8_lossy(&sig_xml_bytes);
1420 let mut sig_parser = lib3mf_core::parser::xml_parser::XmlParser::new(std::io::Cursor::new(
1422 sig_xml_bytes.clone(),
1423 ));
1424 let signature = match lib3mf_core::parser::crypto_parser::parse_signature(&mut sig_parser) {
1425 Ok(s) => s,
1426 Err(e) => {
1427 println!(" [ERROR] Failed to parse signature XML: {}", e);
1428 all_valid = false;
1429 failed_signatures.push(rel.target.clone());
1430 continue;
1431 }
1432 };
1433
1434 let signed_info_c14n = match lib3mf_core::utils::c14n::Canonicalizer::canonicalize_subtree(
1439 &sig_xml_str,
1440 "SignedInfo",
1441 ) {
1442 Ok(b) => b,
1443 Err(e) => {
1444 println!(" [ERROR] Failed to extract/canonicalize SignedInfo: {}", e);
1445 all_valid = false;
1446 failed_signatures.push(rel.target.clone());
1447 continue;
1448 }
1449 };
1450
1451 let mut content_map = BTreeMap::new();
1477 for ref_item in &signature.signed_info.references {
1478 let uri = &ref_item.uri;
1479 if uri.is_empty() {
1480 continue;
1481 } let part_path = uri.trim_start_matches('/');
1483 match archiver.read_entry(part_path) {
1484 Ok(data) => {
1485 content_map.insert(uri.clone(), data);
1486 }
1487 Err(e) => println!(" [WARNING] Could not read referenced part {}: {}", uri, e),
1488 }
1489 }
1490
1491 let resolver = |uri: &str| -> lib3mf_core::error::Result<Vec<u8>> {
1492 content_map.get(uri).cloned().ok_or_else(|| {
1493 lib3mf_core::error::Lib3mfError::Validation(format!("Content not found: {}", uri))
1494 })
1495 };
1496
1497 match lib3mf_core::crypto::verification::verify_signature_extended(
1498 &signature,
1499 resolver,
1500 &signed_info_c14n,
1501 ) {
1502 Ok(valid) => {
1503 if valid {
1504 println!(" [PASS] Signature is VALID.");
1505 if let Some(mut ki) = signature.key_info {
1507 if let Some(x509) = ki.x509_data.take() {
1508 if let Some(_cert_str) = x509.certificate {
1509 println!(
1510 " [INFO] Signed by X.509 Certificate (Trust check pending)"
1511 );
1512 }
1514 } else {
1515 println!(" [INFO] Signed by Raw Key (Self-signed equivalent)");
1516 }
1517 }
1518 } else {
1519 println!(" [FAIL] Signature is INVALID (Verification returned false).");
1520 all_valid = false;
1521 failed_signatures.push(rel.target.clone());
1522 }
1523 }
1524 Err(e) => {
1525 println!(" [FAIL] Verification Error: {}", e);
1526 all_valid = false;
1527 failed_signatures.push(rel.target.clone());
1528 }
1529 }
1530 }
1531
1532 if !all_valid {
1534 anyhow::bail!(
1535 "Signature verification failed for {} of {} signature(s): {:?}",
1536 failed_signatures.len(),
1537 signature_count,
1538 failed_signatures
1539 );
1540 }
1541
1542 println!(
1543 "\nAll {} signature(s) verified successfully.",
1544 signature_count
1545 );
1546 Ok(())
1547}
1548
1549#[cfg(not(feature = "crypto"))]
1557pub fn verify(_file: PathBuf) -> anyhow::Result<()> {
1558 anyhow::bail!(
1559 "Signature verification requires the 'crypto' feature to be enabled.\n\
1560 The CLI was built without cryptographic support."
1561 )
1562}
1563
1564pub fn encrypt(_input: PathBuf, _output: PathBuf, _recipient: PathBuf) -> anyhow::Result<()> {
1579 anyhow::bail!(
1580 "Encrypt command not implemented: lib3mf-rs currently supports reading/parsing \
1581 encrypted 3MF files but does not support creating encrypted packages.\n\n\
1582 Implementing encryption requires:\n\
1583 - AES-256-GCM content encryption\n\
1584 - RSA-OAEP key wrapping for recipients\n\
1585 - KeyStore XML structure creation\n\
1586 - OPC package modification with encrypted content types\n\
1587 - Encrypted relationship handling\n\n\
1588 To create encrypted 3MF files, use the official 3MF SDK or other tools.\n\
1589 Decryption of existing encrypted files is also not yet implemented."
1590 );
1591}
1592
1593pub fn decrypt(_input: PathBuf, _output: PathBuf, _key: PathBuf) -> anyhow::Result<()> {
1608 anyhow::bail!(
1609 "Decrypt command not implemented: lib3mf-rs currently supports reading/parsing \
1610 encrypted 3MF files but does not support decrypting content.\n\n\
1611 Implementing decryption requires:\n\
1612 - KeyStore parsing and key unwrapping\n\
1613 - RSA-OAEP private key operations\n\
1614 - AES-256-GCM content decryption\n\
1615 - OPC package reconstruction with decrypted parts\n\
1616 - Consumer authorization validation\n\n\
1617 To decrypt 3MF files, use the official 3MF SDK or other tools.\n\
1618 The library can parse KeyStore and encryption metadata from encrypted files."
1619 );
1620}