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